How to use LazyColumn's animateItemPlacement() without autoscrolling on changes?
Asked Answered
T

2

9

I am using a LazyColumn in a checklist like style. The list shows all to-be-done items first and all done items last. Tapping on an item toggles whether it is done.

Here is an MWE of what I am doing:

data class TodoItem(val id: Int, val label: String, var isDone: Boolean)

@Composable
fun TodoCard(item: TodoItem, modifier: Modifier, onClick: () -> Unit) {
    val imagePainterDone = rememberVectorPainter(Icons.Outlined.Done)
    val imagePainterNotDone = rememberVectorPainter(Icons.Outlined.Add)

    Card(
        modifier
            .padding(8.dp)
            .fillMaxWidth()
            .clickable {
                onClick()
            }) {
        Row {
            Image(
                if (item.isDone) imagePainterDone else imagePainterNotDone,
                null,
                modifier = Modifier.size(80.dp)
            )
            Text(text = item.label)
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExampleColumn() {
    val todoItems = remember {
        val list = mutableStateListOf<TodoItem>()
        for (i in 0..20) {
            list.add(TodoItem(i, "Todo $i", isDone = false))
        }
        list
    }
    val sortedTodoItems by remember {
        derivedStateOf { todoItems.sortedWith(compareBy({ it.isDone }, { it.id })) }
    }

    LazyColumn {
        items(sortedTodoItems, key = {it.label}) { item ->
            TodoCard(item = item, modifier = Modifier.animateItemPlacement()) {
                val index = todoItems.indexOfFirst { it.label == item.label }
                if (index < 0) return@TodoCard
                todoItems[index] = todoItems[index].copy(isDone = !todoItems[index].isDone)
            }
        }
    }
}

This work well except for one side effect introduced with Modifier.animateItemPlacement(): When toggling the first currently visible list element, the LazyListState will scroll to follow the element.

Which is not what I want (I would prefer it to stay at the same index instead).

I found this workaround, suggesting to scroll back to the first element if it changes, but this only solves the problem if the first item of the column is the first one to be currently displayed. If one scrolls down such that the third element is the topmost visible and taps that element, the column will still scroll to follow it.

Is there any way to decouple automatic scrolling from item placement animations? Both seem to rely on the LazyColumn's key parameter?

Traffic answered 3/12, 2022 at 17:16 Comment(1)
For anybody facing the same issue: There is a ticket on Google's issue tracker. It seems to be very low priority and inactive, so I would not expect a solution anytime soon.Traffic
W
2

I haven't tried to compile/run your code, but it looks like your'e having a similar issue like this, and it looks like because of this in LazyListState

/**
 * When the user provided custom keys for the items we can try to detect when there were
 * items added or removed before our current first visible item and keep this item
 * as the first visible one even given that its index has been changed.
 */
internal fun updateScrollPositionIfTheFirstItemWasMoved(itemsProvider: LazyListItemsProvider) {
    scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
}

I suggested a possible work around where the first item's animation will be sacrificed by setting its key as its index instead of a unique identifier of the backing data.

itemsIndexed(
        items = checkItems.sortedBy { it.checked.value },
        key = { index, item -> if (index == 0) index else item.id }
   ) { index, entry ->
        ...
   }

Though I haven't regressed any use-case with this kind of set-up of Lazy keys yet nor could solve your issue, but you can consider trying it.

Weatherbeaten answered 3/12, 2022 at 17:25 Comment(1)
Thanks for your insights. This seems to be exactly the internal function I would like to deactivate. :-) Your workaround solves the same edge case like the answer I found before asking the question (the first item being modified). Suppose I am scrolling down until the third item is the first to be visible. If I tap it now, it will move down the visible section of the column will scroll to follow it.Traffic
P
1

I've been able to circumvent this behavior by adding a dummy item like this:

LazyRow(...){
    item(key = "0") {
    }
    itemsIndexed(...)
    { .... ->
       ...
    }
}

You can still use keys.

Phocomelia answered 13/11, 2023 at 4:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.