Basic Drag-n-Drop in Jetpack Compose

Suraj Sau
ProAndroidDev
Published in
4 min readAug 31, 2021

--

Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).

Jetpack Compose v1.0 has been out for quite some time now 🎉

At the time of writing this article in v1.0, Compose’s RecyclerView counterpart LazyColumn・LazyRow don’t have an implementation for Drag and drop, though Support Drag and Drop is very well a part of the Compose Roadmap.

https://developer.android.com/jetpack/androidx/compose-roadmap

In the meantime, I tried implementing basic Drag-n-Drop with the existing APIs.

Drag-n-Drop in LazyList

Check this gist for the detailed and entire implementation of Drag-n-Drop.

In this article I’ll walk you over the key portions of code to help you understand the implementation.

Drag on Long-press

LazyListacts similar to RecyclerView, where reordering of data doesn’t reorder the View instances, instead recycles to reuse them.

So in this implementation, instead of applying Long-press gesture detector to the Modifier of each individual list element, which may require complex handling of View-Data mapping, we’ll add it to the Modifier of the LazyList itself. We can then translate the captured drag event of the LazyList onto the individual elements.

Determining the element to be dragged

With LazyColumn’s LazyListState keeping track of the visible elements (check LazyListState.visibleItemsInfo), we determine the element to be dragged by checking whether the drag’s start offset (in onDragStartcallback) lies within the bounds of any of the elements.

This will help us ‘remember’ the initial offset from where drag event had begun throughout further recompositions.

With the initial offsets and element to be dragged determined in onDragStart handled, let’s move onto onDrag when the actual drag happens.

Translate LazyList’s onDrag offset to an element

While Dragging

Once the dragged element has been identified, we now determine the element over which the dragged element is currently hovering on. We then shift it up or down (based on drag direction) and shift the item in the List` or `Array` also to trigger recomposition of the `LazyList`.

Translating the dragged element

With the drag event’s initial offset determined, we can translate the drag event’s offset to the dragged element by the cumulative sum of offset value from the onDrag callback.

Identifying currently hovered items

As an element is dragged, it’ll be crossing over the other elements and based on the direction, once the element crosses a particular “threshold” of the element it is currently crossing (or hovering upon), reordering of the List will be triggered.

The elements which fall beyond the top or bottom of the dragged element are not considered as hovered upon.

Determining the element to be replaced

Once the list of hovered elements are determined we then choose the first element, based on drag direction, whose the top or bottom (the threshold in our case) has been crossed over by the dragged element.
This element will be the one that the dragged element will be replacing.

Trigger reordering of List

With the to-be-replaced element determined, we can trigger a recomposition of the LazyColumn with the List of updated positions.

Handling overscroll

In any typical Drag-n-Drop reordering list, it is expected that the list autoscrolls when an element reaches either visibile thresholds of the screen.

To implement this, we first check if the dragged element has crossed either of the visible thresholds and calculate the overscolled offset. If there’s a non-zero offset, we can then initiate a scroll by calling `LazyListState.scrollBy(offset)`.

Checking for any overscroll

In case of a positive draggedDistance which indicates drag in the downward direction, if the ‘bottom’ of the dragged element crosses the visible bottom threshold (check viewPortEndOffset) of the LazyList we can confirm that the element has over-scrolled beyond the visible bottom.

On the other hand, a negative value we would be looking for if the ‘top’ of the dragged element crosses the visible top threshold (check viewPortStartOffset) of the LazyList.

Drag Interrupted

Reset variables on interrupting the Drag

On cancelling or ending the drag, all the remembered values are to be reset. so that the dragged element settles at its latest position in the LazyColumn

Conclusion

While this might not be the most optimal solution in terms of composition optimisation for Drag-n-Drop. Hopefully this gave a little insight into the APIs of LazyList・LazyRow provided in v1.0.

Meanwhile, I wait curiously to see how the Compose team implements the Drag-n-Drop support 😁

Happy coding 💻 and stay safe during these trying times 😷

--

--