Skip to content

Instantly share code, notes, and snippets.

@surajsau
Last active January 14, 2024 13:05
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save surajsau/f5342f443352195208029e98b0ee39f3 to your computer and use it in GitHub Desktop.
Save surajsau/f5342f443352195208029e98b0ee39f3 to your computer and use it in GitHub Desktop.
Drag-n-Drop implementation in Jetpack Compose
@Composable
fun DragDropList(
items: List<ReorderItem>,
onMove: (Int, Int) -> Unit,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
val dragDropListState = rememberDragDropListState(onMove = onMove)
LazyColumn(
modifier = modifier
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
dragDropListState.onDrag(offset)
if (overscrollJob?.isActive == true)
return@detectDragGesturesAfterLongPress
dragDropListState.checkForOverScroll()
.takeIf { it != 0f }
?.let { overscrollJob = scope.launch { dragDropListState.lazyListState.scrollBy(it) } }
?: run { overscrollJob?.cancel() }
},
onDragStart = { offset -> dragDropListState.onDragStart(offset) },
onDragEnd = { dragDropListState.onDragInterrupted() },
onDragCancel = { dragDropListState.onDragInterrupted() }
)
},
state = dragDropListState.lazyListState
) {
itemsIndexed(items) { index, item ->
Column(
modifier = Modifier
.composed {
val offsetOrNull =
dragDropListState.elementDisplacement.takeIf {
index == dragDropListState.currentIndexOfDraggedItem
}
Modifier
.graphicsLayer {
translationY = offsetOrNull ?: 0f
}
}
.background(Color.White, shape = RoundedCornerShape(4.dp))
.fillMaxWidth()
) { Text(text = "Item ${item.id}") }
}
}
}
@Composable
fun rememberDragDropListState(
lazyListState: LazyListState = rememberLazyListState(),
onMove: (Int, Int) -> Unit,
): ReorderableListState {
return remember { DragDropListState(lazyListState = lazyListState, onMove = onMove) }
}
class DragDropListState(
val lazyListState: LazyListState,
private val onMove: (Int, Int) -> Unit
) {
var draggedDistance by mutableStateOf(0f)
// used to obtain initial offsets on drag start
var initiallyDraggedElement by mutableStateOf<LazyListItemInfo?>(null)
var currentIndexOfDraggedItem by mutableStateOf<Int?>(null)
val initialOffsets: Pair<Int, Int>?
get() = initiallyDraggedElement?.let { Pair(it.offset, it.offsetEnd) }
val elementDisplacement: Float?
get() = currentIndexOfDraggedItem
?.let { lazyListState.getVisibleItemInfoFor(absoluteIndex = it) }
?.let { item -> (initiallyDraggedElement?.offset ?: 0f).toFloat() + draggedDistance - item.offset }
val currentElement: LazyListItemInfo?
get() = currentIndexOfDraggedItem?.let {
lazyListState.getVisibleItemInfoFor(absoluteIndex = it)
}
var overscrollJob by mutableStateOf<Job?>(null)
fun onDragStart(offset: Offset) {
lazyListState.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
}
fun onDragInterrupted() {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
fun onDrag(offset: Offset) {
draggedDistance += offset.y
initialOffsets?.let { (topOffset, bottomOffset) ->
val startOffset = topOffset + draggedDistance
val endOffset = bottomOffset + draggedDistance
currentElement?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item -> item.offsetEnd < startOffset || item.offset > endOffset || hovered.index == item.index }
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
?.also { item ->
currentIndexOfDraggedItem?.let { current -> onMove.invoke(current, item.index) }
currentIndexOfDraggedItem = item.index
}
}
}
}
fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
return@let when {
draggedDistance > 0 -> (endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - lazyListState.layoutInfo.viewportStartOffset).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
}
/*
LazyListItemInfo.index is the item's absolute index in the list
Based on the item's "relative position" with the "currently top" visible item,
this returns LazyListItemInfo corresponding to it
*/
fun LazyListState.getVisibleItemInfoFor(absoluteIndex: Int): LazyListItemInfo? {
return this.layoutInfo.visibleItemsInfo.getOrNull(absoluteIndex - this.layoutInfo.visibleItemsInfo.first().index)
}
/*
Bottom offset of the element in Vertical list
*/
val LazyListItemInfo.offsetEnd: Int
get() = this.offset + this.size
/*
Moving element in the list
*/
fun <T> MutableList<T>.move(from: Int, to: Int) {
if (from == to)
return
val element = this.removeAt(from) ?: return
this.add(to, element)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment