Skip to content

Instantly share code, notes, and snippets.

@darylsze
Created January 11, 2023 15:59
Show Gist options
  • Save darylsze/0e4e507714f2ceac3857248be6f6d1c0 to your computer and use it in GitHub Desktop.
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
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)
)
}
}
}
}
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
)
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)
}
}
}
}
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
}
@darylsze
Copy link
Author

CleanShot.2023-01-11.at.23.51.50.mp4

demo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment