Previous Part 1
Hello again. Let's continue working with LazyColumn. Starter code (from the end of the previous part): Part 1 code
Now we have to solve issues and make a code more readable.
Step 1
link to commit
I should to do some refactoring because it's too uncomfortable to work with so wide code lines.
Now we have LazyColumn with this modifier:
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
dragAndDropListState.onDrag(offset)
if (overscrollJob?.isActive == true) return@detectDragGesturesAfterLongPress
dragAndDropListState
.checkOverscroll()
.takeIf { it != 0f }
?.let {
overscrollJob = coroutineScope.launch {
dragAndDropListState.lazyListState.scrollBy(it)
}
} ?: kotlin.run { overscrollJob?.cancel() }
},
onDragStart = { offset ->
dragAndDropListState.onDragStart(offset)
},
onDragEnd = { dragAndDropListState.onDragInterrupted() },
onDragCancel = { dragAndDropListState.onDragInterrupted() }
)
},
state = dragAndDropListState.lazyListState
)
And we have ItemCard with this modifier:
ItemCard(
userEntityUi = user,
modifier = Modifier
.composed {
val offsetOrNull =
dragAndDropListState.elementDisplacement.takeIf {
index == dragAndDropListState.currentIndexOfDraggedItem
}
Modifier.graphicsLayer {
translationY = offsetOrNull ?: 0f
}
}
)
So, let's make two extensions for our both composable functions
fun Modifier.dragContainer(
dragAndDropListState: DragAndDropListState,
overscrollJob: Job?,
coroutineScope: CoroutineScope
): Modifier {
var coroutineJob = overscrollJob
return this.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
dragAndDropListState.onDrag(offset)
if (overscrollJob?.isActive == true) return@detectDragGesturesAfterLongPress
dragAndDropListState
.checkOverscroll()
.takeIf { it != 0f }
?.let {
coroutineJob = coroutineScope.launch {
dragAndDropListState.lazyListState.scrollBy(it)
}
} ?: kotlin.run { coroutineJob?.cancel() }
},
onDragStart = { offset ->
dragAndDropListState.onDragStart(offset)
},
onDragEnd = { dragAndDropListState.onDragInterrupted() },
onDragCancel = { dragAndDropListState.onDragInterrupted() }
)
}
}
@Composable
fun LazyItemScope.DraggableItem(
dragAndDropListState: DragAndDropListState,
index: Int,
content: @Composable LazyItemScope.(Modifier) -> Unit
) {
val draggingModifier = Modifier
.composed {
val offsetOrNull =
dragAndDropListState.elementDisplacement.takeIf {
index == dragAndDropListState.currentIndexOfDraggedItem
}
Modifier.graphicsLayer {
translationY = offsetOrNull ?: 0f
}
}
content(draggingModifier)
}
And now our LazyColumn looks much more better:
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.dragContainer(
dragAndDropListState = dragAndDropListState,
overscrollJob = overscrollJob,
coroutineScope = coroutineScope
),
state = dragAndDropListState.lazyListState
) {
itemsIndexed(users) { index, user ->
DraggableItem(
dragAndDropListState = dragAndDropListState,
index = index
) { modifier ->
ItemCard(
userEntityUi = user,
modifier = modifier
)
}
}
}
Step 2
link to commit
Next we gonna remove the logic of scrolling list from Modifier.dragContainer to rememberDragAndDropListState. And for this task we gonna use Channels. The Channel is an awesome interface what can help us pass data between coroutines.
- Add a variable to the DragAndDropListState class
val scrollChannel = Channel<Float>()
- Change the checkOverscroll() function. We don't need to get Float as a result of function, just send it to the channel.
private fun checkOverscroll() {
val overscroll = initialDraggingElement?.let {
val startOffset = it.offset + draggingDistance
val endOffset = it.offsetEnd + draggingDistance
return@let when {
draggingDistance > 0 -> {
(endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf { diff -> diff > 0 }
}
draggingDistance < 0 -> {
(startOffset - lazyListState.layoutInfo.viewportStartOffset).takeIf { diff -> diff < 0 }
}
else -> null
}
} ?: 0f
if (overscroll != 0f) {
scrollChannel.trySend(overscroll)
}
}
- Inside of onDrag() I add checkOverscroll():
currentElement?.let { current ->
val targetElement = lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset || current.index == item.index
}
.firstOrNull { item ->
val delta = startOffset - current.offset
when {
delta < 0 -> item.offset > startOffset
else -> item.offsetEnd < endOffset
}
}
if (targetElement == null) {
checkOverscroll()
}
targetElement
}?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
It looks not so pretty good but we'll refactor that later.
- We created the channel and sent a value there, now we need to receive that value and scroll list to this difference.
@Composable
fun rememberDragAndDropListState(
lazyListState: LazyListState,
onMove: (Int, Int) -> Unit
): DragAndDropListState {
val state = remember { DragAndDropListState(lazyListState, onMove) }
LaunchedEffect(state) {
while (true) {
val diff = state.scrollChannel.receive()
state.lazyListState.scrollBy(diff)
}
}
return state
}
So, now Modifier.dragContainer looks this:
fun Modifier.dragContainer(
dragAndDropListState: DragAndDropListState,
): Modifier {
return this.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
dragAndDropListState.onDrag(offset)
},
onDragStart = { offset ->
dragAndDropListState.onDragStart(offset)
},
onDragEnd = { dragAndDropListState.onDragInterrupted() },
onDragCancel = { dragAndDropListState.onDragInterrupted() }
)
}
}
Step 3
link to commit
Let's continue to do refactoring and now we are working with the DragAndDropListState class overall. And here I think I should add comments and javadoc to code.
- comments to variables
class DragAndDropListState(
val lazyListState: LazyListState,
private val onMove: (Int, Int) -> Unit
) {
// channel for emitting scroll events
val scrollChannel = Channel<Float>()
// the index of item that is being dragged
var currentIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
// the item that is being dragged
private val currentElement: LazyListItemInfo?
get() = currentIndexOfDraggedItem?.let { lazyListState.getVisibleItemInfo(it) }
// the initial index of item that is being dragged
private var initialDraggingElement by mutableStateOf<LazyListItemInfo?>(null)
// the initial offset of the item when it is dragged
private val initialOffsets: Pair<Int, Int>?
get() = initialDraggingElement?.let { Pair(it.offset, it.offsetEnd) }
// distance that has been dragged
private var draggingDistance by mutableFloatStateOf(0f)
// calculates the vertical displacement of the dragged element
// relative to its original position in the LazyList.
val elementDisplacement: Float?
get() = currentIndexOfDraggedItem
?.let { lazyListState.getVisibleItemInfo(it) }
?.let { itemInfo ->
(initialDraggingElement?.offset
?: 0f).toFloat() + draggingDistance - itemInfo.offset
}
- also add javadoc to functions
/**
* Starts the dragging operation when the user initiates a drag gesture.
*
* This function determines which item in the LazyList is being dragged based on the
* starting offset of the drag gesture. It then stores the information about the
* dragged item and its index for use during the drag operation.
* */
fun onDragStart(offset: Offset) {
lazyListState.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
initialDraggingElement = it
currentIndexOfDraggedItem = it.index
}
}
/**
* Called when a drag operation is interrupted or canceled.
*
* Resets the state of the ongoing drag by clearing the dragged element reference,
* the dragged item index, and the dragging distance. This is invoked when a drag
* is canceled, fails, or completes.
*/
fun onDragInterrupted() {
initialDraggingElement = null
currentIndexOfDraggedItem = null
draggingDistance = 0f
}
- and refactoring onDrag() function for more readability
fun onDrag(offset: Offset) {
draggingDistance += offset.y
initialOffsets?.let { (top, bottom) ->
val startOffset = top.toFloat() + draggingDistance
val endOffset = bottom.toFloat() + draggingDistance
val middleOffset = (startOffset + endOffset) / 2f
currentElement?.let { current ->
val targetElement = lazyListState.layoutInfo.visibleItemsInfo.find {
middleOffset.toInt() in it.offset..it.offsetEnd && current.index != it.index
}
if (targetElement != null) {
currentIndexOfDraggedItem?.let {
onMove.invoke(it, targetElement.index)
}
currentIndexOfDraggedItem = targetElement.index
} else {
checkOverscroll()
}
}
}
}
Looks nice! Simple and understandable :)
I realized that the article has been happened too large. So, next Part 3 (place for link) will be about fixing issues.
Top comments (0)