8

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 Answers1

3

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

z.g.y
  • 5,512
  • 4
  • 10
  • 36
  • 1
    Thanks for the suggestion! But it doesn't work correctly. First, it breaks the element placement animation, second, there is a lag in the scrolling animation which starts to scroll and then goes back. – Andrei Marshalov Dec 01 '22 at 11:22
  • 1
    @AndreiMarshalov apologies for the useless answer, this is a bit painful though, it seems there's no way (or an easy way) to override or disable the ItemProvider to prevent animate/focusing the first index during transition, but since animateItemPlacement needs a unique ID for the items to get animated, I updated my answer sacrificing the index == 0's animation, though I'm not sure if this will solve the issue or just going to introduce more bugs.. but please check.. – z.g.y Dec 01 '22 at 16:25
  • 1
    Ok, thanks again, that makes sense. I'm sure it could be done somehow without these kinds of tradeoffs because I've seen it work like this in the native Notes app on MIUI. I'll try to find out the solution and will post it here if I would succeed. – Andrei Marshalov Dec 02 '22 at 10:02
  • Thank you for the upvote, I will also get back to this if I find something! see yah :) – z.g.y Dec 02 '22 at 10:31
  • 1
    Unfortunately the problem isn't necessarily with index 0, its with whichever index is the first visible item on the screen – Alex Baker Apr 03 '23 at 13:40