Reputation: 1469
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?
Upvotes: 9
Views: 5682
Reputation: 11
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.
Upvotes: 1
Reputation: 6197
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.
Upvotes: 2