Created
January 11, 2023 15:59
-
-
Save darylsze/0e4e507714f2ceac3857248be6f6d1c0 to your computer and use it in GitHub Desktop.
Make lazy grid to be able to drag and drop, with section header that not moveable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.os.Build | |
import androidx.annotation.RequiresApi | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.PaddingValues | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.lazy.grid.GridCells | |
import androidx.compose.foundation.lazy.grid.GridItemSpan | |
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid | |
import androidx.compose.foundation.lazy.grid.items | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.unit.dp | |
@RequiresApi(Build.VERSION_CODES.N) | |
@Composable | |
fun Test() { | |
val state = rememberReorderableState() | |
LazyVerticalGrid( | |
modifier = Modifier.background(Color.Black), | |
columns = GridCells.Fixed(4), | |
state = state.gridState, | |
contentPadding = PaddingValues(horizontal = 8.dp), | |
verticalArrangement = Arrangement.spacedBy(4.dp), | |
horizontalArrangement = Arrangement.spacedBy(4.dp), | |
) { | |
items(state.data.value, key = { it.key }, span = { | |
when (it) { | |
is IItem.Item -> GridItemSpan(1) | |
is IItem.Banner -> GridItemSpan(4) | |
} | |
}) { item -> | |
if (item is IItem.Item) { | |
Box( | |
Modifier | |
.reorderable(state, item) | |
.background(item.color) | |
.size(50.dp) | |
) | |
} | |
if (item is IItem.Banner) { | |
Box( | |
Modifier | |
.background(item.color) | |
.size(200.dp) | |
) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.ui.graphics.Color | |
sealed class IItem( | |
open val key: Int | |
) { | |
data class Item( | |
val color: Color, | |
val text: String, | |
override val key: Int | |
): IItem(key) | |
data class Banner( | |
override val key: Int, | |
val color: Color | |
): IItem(key) | |
} | |
val banner = IItem.Banner( | |
key = 5, | |
color = Color.Green | |
) | |
val item1 = IItem.Item( | |
color = Color.Blue, | |
text = "1", | |
key = 0 | |
) | |
val item2 = IItem.Item( | |
color = Color.Red, | |
text = "2", | |
key = 1 | |
) | |
val item3 = IItem.Item( | |
color = Color.Yellow, | |
text = "3", | |
key = 2 | |
) | |
val item4 = IItem.Item( | |
color = Color.Gray, | |
text = "4", | |
key = 3 | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.foundation.gestures.detectDragGestures | |
import androidx.compose.foundation.lazy.grid.LazyGridState | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.graphicsLayer | |
import androidx.compose.ui.input.pointer.consumeAllChanges | |
import androidx.compose.ui.input.pointer.pointerInput | |
import androidx.compose.ui.unit.IntOffset | |
import androidx.compose.ui.zIndex | |
import kotlinx.coroutines.flow.MutableStateFlow | |
import kotlinx.coroutines.flow.collectLatest | |
@Composable | |
fun rememberReorderableState(): ReorderableState { | |
var offset = remember { mutableStateOf(IntOffset(0, 0)) } | |
var draggingItem = remember { | |
mutableStateOf<IItem.Item?>(null) | |
} | |
val gridState by remember { mutableStateOf(LazyGridState()) } | |
val data = remember { | |
mutableStateOf( | |
listOf( | |
item1, | |
item2, | |
item3, | |
item4.copy(key = 10), | |
item4.copy(key = 11), | |
item4.copy(key = 12), | |
item4.copy(key = 13), | |
banner, | |
item4, | |
item4.copy(key = 14), | |
item4.copy(key = 6), | |
item4.copy(key = 7), | |
item4.copy(key = 8), | |
item4.copy(key = 9), | |
) | |
) | |
} | |
val coroutineScope = rememberCoroutineScope() | |
val swapFlow = MutableStateFlow(-1) | |
val reorderableState by remember { | |
mutableStateOf( | |
ReorderableState( | |
coroutineScope = coroutineScope, | |
gridState, data, draggingItem, offset | |
) | |
) | |
} | |
LaunchedEffect(key1 = swapFlow) { | |
swapFlow.collectLatest { potentialSwapIndex -> | |
if (potentialSwapIndex!=-1) { | |
data.value = data.value.toMutableList().apply { | |
val targetIndex = indexOfFirst { it.key==draggingItem.value!!.key } | |
offset.value = IntOffset.Zero | |
add(potentialSwapIndex, removeAt(targetIndex)) | |
draggingItem.value = data.value[potentialSwapIndex] as IItem.Item | |
} | |
} | |
} | |
} | |
LaunchedEffect(key1 = offset.value, key2 = draggingItem.value) { | |
if (draggingItem.value!=null) { | |
val potentialSwapIndex = reorderableState.findPotentialSwapIndex( | |
draggingItem.value!!, | |
gridState, | |
offset.value.x, | |
offset.value.y | |
) | |
if (potentialSwapIndex!=null) { | |
swapFlow.emit(potentialSwapIndex) | |
} | |
gridState.layoutInfo.visibleItemsInfo.forEach { | |
} | |
} | |
} | |
return reorderableState | |
} | |
fun Modifier.reorderable(state: ReorderableState, currentItem: IItem.Item): Modifier { | |
val draggingItem: IItem.Item? = state.draggingItem.value | |
val offsetX = state.offset.value.x | |
val offsetY = state.offset.value.y | |
val isDragging = draggingItem!=null && draggingItem.key==currentItem.key | |
val draggingModifier = if (isDragging) { | |
this | |
.zIndex(1f) | |
.graphicsLayer { | |
translationX = offsetX.toFloat() | |
translationY = offsetY.toFloat() | |
} | |
} else { | |
this | |
} | |
return this | |
.then(draggingModifier) | |
.pointerInput(currentItem.key) { | |
while (true) { | |
detectDragGestures( | |
onDragEnd = { | |
state.updateDraggingItem(null) | |
state.resetOffset() | |
} | |
) { change, dragAmount -> | |
change.consumeAllChanges() | |
state.updateOffset( | |
IntOffset( | |
dragAmount.x.toInt(), | |
dragAmount.y.toInt() | |
) | |
) | |
state.updateDraggingItem(currentItem) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo | |
import androidx.compose.foundation.lazy.grid.LazyGridState | |
import androidx.compose.runtime.MutableState | |
import androidx.compose.ui.unit.IntOffset | |
import kotlinx.coroutines.CoroutineScope | |
import kotlinx.coroutines.launch | |
import kotlin.math.absoluteValue | |
class ReorderableState( | |
val coroutineScope: CoroutineScope, | |
val gridState: LazyGridState, | |
val data: MutableState<List<IItem>>, | |
val draggingItem: MutableState<IItem.Item?>, | |
val offset: MutableState<IntOffset> | |
) { | |
private val nonDraggables = data.value.filter { it !is IItem.Item } | |
fun resetOffset() { | |
coroutineScope.launch { | |
offset.value = IntOffset.Zero | |
} | |
} | |
fun updateOffset(newOffset: IntOffset) { | |
coroutineScope.launch { | |
offset.value = offset.value.let { | |
IntOffset( | |
x = it.x + newOffset.x, | |
y = it.y + newOffset.y | |
) | |
} | |
} | |
} | |
fun updateDraggingItem(newItem: IItem.Item?) { | |
coroutineScope.launch { | |
draggingItem.value = newItem | |
} | |
} | |
fun findPotentialSwapIndex( | |
draggingItem: IItem.Item, | |
gridState: LazyGridState, | |
x: Int, | |
y: Int | |
): Int? { | |
val draggedItemInfo = | |
gridState.layoutInfo.visibleItemsInfo.first { it.key==draggingItem.key } | |
val targets = findTargets( | |
x, | |
y, | |
selected = draggedItemInfo, | |
gridState.layoutInfo.visibleItemsInfo, | |
skips = nonDraggables | |
) | |
var target: LazyGridItemInfo? = null | |
var highScore = -1 | |
val height = draggedItemInfo.size.height | |
val width = draggedItemInfo.size.width | |
val threshold = 0.45 | |
targets.forEach { item -> | |
val draggedRight = draggedItemInfo.right + x | |
val draggedLeft = draggedItemInfo.left + x | |
val draggedTop = draggedItemInfo.top + y | |
val draggedBottom = draggedItemInfo.bottom + y | |
val x_overlap = | |
Math.max(0, Math.min(item.right, draggedRight) - Math.max(item.left, draggedLeft)) | |
val y_overlap = | |
Math.max(0, Math.min(item.bottom, draggedBottom) - Math.max(item.top, draggedTop)) | |
val overlapArea = (x_overlap * y_overlap).absoluteValue | |
if (overlapArea > (height * width * threshold) && overlapArea > highScore) { | |
highScore = overlapArea | |
target = item | |
} | |
} | |
return target?.index ?: -1 | |
} | |
fun findTargets( | |
x: Int, | |
y: Int, | |
selected: LazyGridItemInfo, | |
allItems: List<LazyGridItemInfo>, | |
skips: List<IItem> | |
): List<LazyGridItemInfo> { | |
val skipKeys = skips.map { it.key } | |
val targets = mutableListOf<LazyGridItemInfo>() | |
val left = selected.left + x | |
val right = selected.right + x | |
val top = selected.top + y | |
val bottom = selected.bottom + y | |
allItems.forEach { item -> | |
if (item.index==selected.index | |
|| skipKeys.contains(item.key) | |
|| item.bottom < top | |
|| item.right < left | |
|| item.left > right | |
|| item.top > bottom | |
) return@forEach | |
targets.add(item) | |
} | |
return targets | |
} | |
} | |
private val LazyGridItemInfo.bottom: Int | |
get() { | |
return offset.y + size.height | |
} | |
private val LazyGridItemInfo.top: Int | |
get() { | |
return offset.y | |
} | |
private val LazyGridItemInfo.left: Int | |
get() { | |
return offset.x | |
} | |
private val LazyGridItemInfo.right: Int | |
get() { | |
return offset.x + size.width | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
CleanShot.2023-01-11.at.23.51.50.mp4
demo