Blog Infos
Author
Published
Topics
,
Author
Published

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.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..
// let current LazyListState.scrollBy() not be interrupted
if (overscrollJob?.isActive == true)
return
// launch LazyListState.scrollBy only if overscrolled offset != 0
checkForOverScroll()
.takeIf { offset -> offset != 0f }
?.let { offset -> overscrollJob = scope.launch { lazyListState.scrollBy(offset) } }
?: run { overscrollJob?.cancel() }
}
)
)
view raw Overscroll.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(
items: List<T>,
onMove: (fromIndex: Int, toIndex: Int) -> Unit
) {
..
onDrag = {
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull {..}
?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
}
}
}
@Composable
fun Screen() {
val list = listOf(..).toMutableStateList()
ReorderList(
items = list,
onMove = { fromIndex, toIndex -> list.move(fromIndex, toIndex) }
)
}
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.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..
// let current LazyListState.scrollBy() not be interrupted
if (overscrollJob?.isActive == true)
return
// launch LazyListState.scrollBy only if overscrolled offset != 0
checkForOverScroll()
.takeIf { offset -> offset != 0f }
?.let { offset -> overscrollJob = scope.launch { lazyListState.scrollBy(offset) } }
?: run { overscrollJob?.cancel() }
}
)
)
view raw Overscroll.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(
items: List<T>,
onMove: (fromIndex: Int, toIndex: Int) -> Unit
) {
..
onDrag = {
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull {..}
?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
}
}
}
@Composable
fun Screen() {
val list = listOf(..).toMutableStateList()
ReorderList(
items = list,
onMove = { fromIndex, toIndex -> list.move(fromIndex, toIndex) }
)
}

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.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..
// let current LazyListState.scrollBy() not be interrupted
if (overscrollJob?.isActive == true)
return
// launch LazyListState.scrollBy only if overscrolled offset != 0
checkForOverScroll()
.takeIf { offset -> offset != 0f }
?.let { offset -> overscrollJob = scope.launch { lazyListState.scrollBy(offset) } }
?: run { overscrollJob?.cancel() }
}
)
)
view raw Overscroll.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(
items: List<T>,
onMove: (fromIndex: Int, toIndex: Int) -> Unit
) {
..
onDrag = {
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull {..}
?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
}
}
}
@Composable
fun Screen() {
val list = listOf(..).toMutableStateList()
ReorderList(
items = list,
onMove = { fromIndex, toIndex -> list.move(fromIndex, toIndex) }
)
}
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.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..
// let current LazyListState.scrollBy() not be interrupted
if (overscrollJob?.isActive == true)
return
// launch LazyListState.scrollBy only if overscrolled offset != 0
checkForOverScroll()
.takeIf { offset -> offset != 0f }
?.let { offset -> overscrollJob = scope.launch { lazyListState.scrollBy(offset) } }
?: run { overscrollJob?.cancel() }
}
)
)
view raw Overscroll.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(
items: List<T>,
onMove: (fromIndex: Int, toIndex: Int) -> Unit
) {
..
onDrag = {
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull {..}
?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
}
}
}
@Composable
fun Screen() {
val list = listOf(..).toMutableStateList()
ReorderList(
items = list,
onMove = { fromIndex, toIndex -> list.move(fromIndex, toIndex) }
)
}
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.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..
// let current LazyListState.scrollBy() not be interrupted
if (overscrollJob?.isActive == true)
return
// launch LazyListState.scrollBy only if overscrolled offset != 0
checkForOverScroll()
.takeIf { offset -> offset != 0f }
?.let { offset -> overscrollJob = scope.launch { lazyListState.scrollBy(offset) } }
?: run { overscrollJob?.cancel() }
}
)
)
view raw Overscroll.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(
items: List<T>,
onMove: (fromIndex: Int, toIndex: Int) -> Unit
) {
..
onDrag = {
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull {..}
?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
}
}
}
@Composable
fun Screen() {
val list = listOf(..).toMutableStateList()
ReorderList(
items = list,
onMove = { fromIndex, toIndex -> list.move(fromIndex, toIndex) }
)
}
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.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..
// let current LazyListState.scrollBy() not be interrupted
if (overscrollJob?.isActive == true)
return
// launch LazyListState.scrollBy only if overscrolled offset != 0
checkForOverScroll()
.takeIf { offset -> offset != 0f }
?.let { offset -> overscrollJob = scope.launch { lazyListState.scrollBy(offset) } }
?: run { overscrollJob?.cancel() }
}
)
)
view raw Overscroll.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(
items: List<T>,
onMove: (fromIndex: Int, toIndex: Int) -> Unit
) {
..
onDrag = {
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull {..}
?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
}
}
}
@Composable
fun Screen() {
val list = listOf(..).toMutableStateList()
ReorderList(
items = list,
onMove = { fromIndex, toIndex -> list.move(fromIndex, toIndex) }
)
}
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)`.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..
// let current LazyListState.scrollBy() not be interrupted
if (overscrollJob?.isActive == true)
return
// launch LazyListState.scrollBy only if overscrolled offset != 0
checkForOverScroll()
.takeIf { offset -> offset != 0f }
?.let { offset -> overscrollJob = scope.launch { lazyListState.scrollBy(offset) } }
?: run { overscrollJob?.cancel() }
}
)
)
view raw Overscroll.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(
items: List<T>,
onMove: (fromIndex: Int, toIndex: Int) -> Unit
) {
..
onDrag = {
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull {..}
?.also { item ->
currentIndexOfDraggedItem?.let { current ->
onMove.invoke(current, item.index)
}
currentIndexOfDraggedItem = item.index
}
}
}
}
@Composable
fun Screen() {
val list = listOf(..).toMutableStateList()
ReorderList(
items = list,
onMove = { fromIndex, toIndex -> list.move(fromIndex, toIndex) }
)
}

Job Offers

Job Offers


    Senior Android Engineer

    Busuu
    Madrid
    • Full Time
    apply now

    Senior Mobile Systems SDK Engineer

    Sauce Labs
    Remote
    • Full Time
    apply now

    Android Engineer

    American Express
    Phoenix, USA
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

, ,

Automated migration of Android apps to Bazel build system

Migrating large projects that consist of hundreds or thousands of modules and being maintained by a large team, from Gradle to Bazel might be challenging. I would like to discuss the process of automation of…
Watch Video

Automated migration of Android apps to Bazel build system

Pavlo Stavytskyi
Software Engineer
Lyft

Automated migration of Android apps to Bazel build system

Pavlo Stavytskyi
Software Engineer
Lyft

Automated migration of Android apps to Bazel build system

Pavlo Stavytskyi
Software Engineer
Lyft

Jobs

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.

fun checkForOverScroll(): Float {
return initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
val viewPortStart = lazyListState.layoutInfo.viewportStartOffset
val viewPortEnd = lazyListState.layoutInfo.viewportEndOffset
when {
draggedDistance > 0 -> (endOffset - viewPortEnd).takeIf { diff -> diff > 0 }
draggedDistance < 0 -> (startOffset - viewPortStart).takeIf { diff -> diff < 0 }
else -> null
}
} ?: 0f
}
onDrag = { offset ->
..
initiallyDraggedElement?.let {
val startOffset = it.offset + draggedDistance
val endOffset = it.offsetEnd + draggedDistance
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot { item ->
item.offsetEnd < startOffset || item.offset > endOffset
}
}
}
}
onDragEnd = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
},
onDragCancel = {
draggedDistance = 0f
currentIndexOfDraggedItem = null
initiallyDraggedElement = null
overscrollJob?.cancel()
}
view raw DragCancel.kt hosted with ❤ by GitHub
@Composable
fun ReorderList(..) {
val lazyListState = rememberLazyListState()
// used to obtain initial offsets on drag start
var initiallyDraggedElement by remember {mutableStateOf<LazyListItemInfo?>(null) }
var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
?.also {
currentIndexOfDraggedItem = it.index
initiallyDraggedElement = it
}
},
)
},
state = lazyListState
)
}
var elementDisplacement by remember { mutableStateOf(0f) }
var currentIndexOfDraggedItem by remember { .. }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
draggedDistance += offset.y
}
),
) {
itemsIndexed(items) { index, item ->
Box(modifier = Modifier
.graphicsLayer {
// only move the element if that is where Drag started
translationY = draggedDistance
.takeIf { index == currentIndexOfDraggedItem } ?: 0f
}
)
}
}
onDrag = { offset ->
..
currentElementItemInfo?.let { hovered ->
lazyListState.layoutInfo.visibleItemsInfo
.filterNot {..}
.firstOrNull { item ->
val delta = startOffset - hovered.offset
when {
delta > 0 -> (endOffset > item.offsetEnd)
else -> (startOffset < item.offset)
}
}
}
}
@Composable
fun ReorderableList(models: List<T>) {
val lazyListState = rememberLazyListState()
val calculatedOffset = remember { mutableStateOf<Float>() }
LazyColumn(
modifier = ..
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consumeAllChanges()
// compute calculatedOffset
..
},
onDragStart = { offset -> .. },
onDragEnd = { .. },
onDragCancel = { .. }
)
},
state = lazyListState
) {
items(models) {
Box(modifier = Modifier
.graphicsLayer(translationY = calculatedOffset ?: 0f)
)
}
}
}
view raw Overall.kt hosted with ❤ by GitHub
// since LazyListState.scrollBy() is a suspend function
val scope = rememberCoroutineScope()
var overscrollJob by remember { mutableStateOf<Job?>(null) }
LazyColumn(
modifier = ..
detectDragGesturesAfterLongPress(
onDrag = {
..