Last active
November 29, 2024 12:20
-
-
Save hector6872/3a1e6a5bf80f2ab5b84c101827f62c63 to your computer and use it in GitHub Desktop.
LazyColum Drag&Drop implementation in Jetpack Compose
This file contains hidden or 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
/* | |
* Copyright (c) 2023. Héctor de Isidro - hector6872 | |
* | |
* 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 | |
) | |
} | |
} | |
} |
This file contains hidden or 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
/* | |
* Copyright (c) 2023. Héctor de Isidro - hector6872 | |
* | |
* 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)) |
reverseLayout = true 将导致错误顺序
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Based on https://gist.github.com/surajsau/f5342f443352195208029e98b0ee39f3 by @surajsau