Last active
January 8, 2025 03:32
-
-
Save surajsau/f5342f443352195208029e98b0ee39f3 to your computer and use it in GitHub Desktop.
Drag-n-Drop implementation in Jetpack Compose
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
@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}") } | |
} | |
} | |
} |
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
@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 | |
} | |
} |
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
/* | |
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