Skip to content

Instantly share code, notes, and snippets.

@hrules6872
Last active April 3, 2024 01:24
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hrules6872/3a1e6a5bf80f2ab5b84c101827f62c63 to your computer and use it in GitHub Desktop.
Save hrules6872/3a1e6a5bf80f2ab5b84c101827f62c63 to your computer and use it in GitHub Desktop.
LazyColum Drag&Drop implementation in Jetpack Compose
/*
* Copyright (c) 2023. Héctor de Isidro - hrules6872
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@Composable
fun <TYPE> DragDropLazyColum(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
items: List<TYPE>,
onDrag: (Int, Int) -> Unit,
onDragStart: () -> Unit = {},
onDragEnd: () -> Unit = {},
draggingModifier: Modifier = Modifier.shadow(elevation = 4.dp),
content: @Composable LazyItemScope.(modifier: Modifier, index: Int, item: TYPE) -> Unit
) {
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
val dragDropListState = rememberDragDropLazyListState(lazyListState = state, onDrag = onDrag)
LazyColumn(
modifier = modifier
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { pointerInputChange, offset ->
pointerInputChange.consume()
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)
onDragStart()
},
onDragEnd = {
dragDropListState.onDragInterrupted()
overscrollJob?.cancel()
onDragEnd()
},
onDragCancel = {
dragDropListState.onDragInterrupted()
overscrollJob?.cancel()
onDragEnd()
}
)
},
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
state = dragDropListState.lazyListState
) {
var isRecomposition = true
itemsIndexed(items) { index, item ->
val isDragging = index == dragDropListState.currentIndexOfDraggedItem
val offsetOrNull = dragDropListState.elementDisplacement.takeIf { isDragging }?.let {
// workaround to avoid flickering during recomposition
if (isRecomposition) {
isRecomposition = false
null
} else it
}
content(
modifier = Modifier
.zIndex(offsetOrNull?.let { 1f } ?: 0f)
.graphicsLayer { translationY = offsetOrNull ?: 0f }
.then(if (isDragging) draggingModifier else Modifier),
index = index,
item = item
)
}
}
}
/*
* Copyright (c) 2023. Héctor de Isidro - hrules6872
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@Composable
fun rememberDragDropLazyListState(
lazyListState: LazyListState = rememberLazyListState(),
onDrag: (Int, Int) -> Unit,
): DragDropLazyListState = remember { DragDropLazyListState(lazyListState = lazyListState, onDrag = onDrag) }
class DragDropLazyListState(
val lazyListState: LazyListState,
private val onDrag: (Int, Int) -> Unit
) {
private var draggedDistance by mutableFloatStateOf(0f)
private var initiallyDraggedElement by mutableStateOf<LazyListItemInfo?>(null)
private val initialOffsets: Pair<Int, Int>? get() = initiallyDraggedElement?.let { Pair(it.offset, it.offsetEnd) }
private val currentElement: LazyListItemInfo? get() = currentIndexOfDraggedItem?.let { lazyListState.getVisibleItemInfoFor(absoluteIndex = it) }
private var overscrollJob by mutableStateOf<Job?>(null)
var currentIndexOfDraggedItem by mutableStateOf<Int?>(null)
val elementDisplacement: Float?
get() = currentIndexOfDraggedItem
?.let { lazyListState.getVisibleItemInfoFor(absoluteIndex = it) }
?.let { item -> (initiallyDraggedElement?.offset ?: 0f).toFloat() + draggedDistance - item.offset }
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 -> onDrag.invoke(current, item.index) }
currentIndexOfDraggedItem = item.index
}
}
}
}
fun checkForOverScroll(): Float = 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
}
private val LazyListItemInfo.offsetEnd: Int get() = this.offset + this.size
private fun LazyListState.getVisibleItemInfoFor(absoluteIndex: Int): LazyListItemInfo? =
this.layoutInfo.visibleItemsInfo.getOrNull(absoluteIndex - (this.layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0))
@hrules6872
Copy link
Author

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