How to prevent LazyColumn from autoscrolling if the first item was moved

1.6k Views Asked by At

I use Jetpack Compose UI to build a simple TODO app. The idea is to have a list of tasks that could be checked or unchecked, and checked tasks should go to the end of the list.

Everything is working fine except when I check the first visible item on the screen it moves down along with the scroll position.

I believe that has something to do with LazyListState, there is such function:

/**
 * 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)
}

So I would like to disable this kind of behavior but I didn't find a way.


Below is a simple code to reproduce the problem and a screencast. The problem in this screencast appears when I try to check "Item 0", "Item 1" and "Item 4", but it works as I expect when checking "Item 7" and "Item 8".

It also behaves the same if you check or uncheck any item that is currently the first visible item, not only if the item is first in the whole list.

class MainActivity : ComponentActivity() {

    @OptIn(ExperimentalFoundationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {

                    val checkItems by remember { mutableStateOf(generate(20)) }

                    LazyColumn() {
                        items(
                            items = checkItems.sortedBy { it.checked.value },
                            key = { item -> item.id }
                        ) { entry ->

                            Row(
                                modifier = Modifier.animateItemPlacement(),
                                verticalAlignment = Alignment.CenterVertically,
                            ) {
                                Checkbox(
                                    checked = entry.checked.value,
                                    onCheckedChange = {
                                        checkItems.find { it == entry }
                                            ?.apply { this.checked.value = !this.checked.value }
                                    }
                                )
                                Text(text = entry.text)
                            }
                        }
                    }
                }
            }
        }
    }
}

data class CheckItem(val id: String, var checked: MutableState<Boolean> = mutableStateOf(false), var text: String = "")

fun generate(count: Int): List<CheckItem> =
    (0..count).map { CheckItem(it.toString(), mutableStateOf(it % 2 == 0), "Item $it") }
1

There are 1 best solutions below

5
z.g.y On

Since animateItemPlacement requires a unique ID/key for the Lazy item to get animated, maybe sacrificing the first item, setting its key using its index position (no animation) will prevent the issue

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

enter image description here