<@UJN92AYA0> :wave: I have a reversed `LazyColumn`...
# compose
n
@Andrey Kulikov đź‘‹ I have a reversed
LazyColumn
, and its last item is at the bottom of the current screen. The content is dynamically increasing, causing the
LazyColumn
to automatically scroll to the bottom. However, I do not want it to scroll, I want it to remain in its original position. Is there a good way to achieve this?
LazyColumn's default behavior stops autoscrolling when I move a distance (like maybe after more than one item), is there a way to stop autoscrolling when I detect a gesture swipe? 🤔
a
can you share a video of the current behavior so we can understand it better?
n
Screen_Recording_20240619_211625_ChatGPT.mp4
A simple example is this, when the last item is changing dynamically, the auto-scroll will be canceled when the user swipes some distance, and it will continue to auto-scroll if the user swipes to the bottom again
a
important thing to understand is how the scroll position is represented in lazy lists. it is a pair of first visible item index, and first visible item scroll offset. when the size of first visible item changes we will still stay on the same offset delta. it is exactly what happens in your case, as the layout logic is reversed and we calculate the scroll offset from the bottom. so we stay on the same offset from the bottom on this item. if you scroll more so now another item is the first visible one, this item is not being resized, so you don’t see such jumps
based on your explanation I am not really sure you even need “reverseLayout = true”. it seems like you want scroll position to be calculated based by the top item, not the bottom one
if all you needed is to align the items to the bottom, when you have not enough items to fill the whole viewport, then there is a “verticalArrangement” param on LazyColumn
and you can always programmatically scroll via LazyListState.requestScrollTo() api if you need to scroll to some item when, for example, user sends new message
n
Because imePadding doesn't take effect when
reverseLayout = false,
reference: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1715074009880069?thread_ts=1715063833.799509&amp;cid=CJLTWPH7S because of this issue, that's why I use
reversedLayout = true
🤔 but yes, I also think that reversedLayout is not needed in my case, but I need imePadding because it allows the LazyColumn item to move with the ime when it is opened
a
it seems like here you indeed rely on bottom item being a scroll position anchor as on every animation step you want to stay on the same 0th scroll offset for the bottom item
overall it seems like your use case is “when you scrolled to the very bottom, you want to stay on the very bottom”. if we achieve it, the fact that bottom item is resizing will be irrelevant
for that we will keep it as reverseLayout = true. and then do one of two things. if you are on Compose 1.7 already, you can use a new api as described here: https://x.com/and_kulikov/status/1775574133763350728. if you are on older compose you can workaround it by adding extra empty item { } as the very first item in the LazyColumn. it will make sure we always stay on this empty item, as it will be a first visible item, not the next one which is being resized
n
Screen_Recording_20240619_215500_ChatGPT.mp4,Screen_Recording_20240619_215611_ChatGPT.mp4
When I enable
reverseLayout
, this API scrolls the current list to the bottom of the "last item". However, what I need is for the list to remain fixed at the position I scroll to, and not continue scrolling. But it seems like you understand the behavior from the first video? However, I also need the effect shown in the second video, where the automatic scrolling stops after I manually scroll the list
Let me explain more clearly: In the second video, when I scroll through the list, the entire list remains static, regardless of any changes in item height. 🤯
a
yeah, you have a slightly conflicting set of requirements. in some cases you kinda want top item to be an anchor, but in other cases the bottom one
n
yes yes pretty much, is there an easy way to fix a situation like this? 🤔 🥹
Here are the issues I've summarized for two different scenarios when
reverseLayout
is either
false
or `true`: 1.
reverseLayout = false
In this scenario:
Copy code
Column {
  LazyColumn(...)
  TextField(Modifier.imePadding()) // The items within the LazyColumn cannot move together with the IME (as previously discussed)
}
In this case, we can use:
Copy code
if (!state.canScrollForward) state.requestScrollToItem(lastIndex)
to achieve automatic scrolling and to cancel automatic scrolling (e.g., after some distance has been scrolled). 2.
reverseLayout = true
In this scenario, the
TextField
can sync well with the
LazyColumn
during the IME opened/closed, but the issue is that after scrolling some distance, we cannot cancel automatic scrolling
The only solution I can think of at the moment is to not use the
reverseLayout
, so that it doesn't automatically scroll to the bottom. Instead, I can manually scroll the list when the item height changes. However, this approach would cause the
imePadding
effect to become ineffective. So, I'm not quite sure how to proceed from here... 🥲
a
essentially what imePadding is doing is changing the height for your LazyColumn
we can theoretically add a layout modifier on LazyColumn right after imePadding(). there we will see what are the new constraints.maxHeight, if it is not the same as the previous value, and if lazyListState.canScrollForward() is false (so we are on the very bottom), then we will manually call lazyListState.requestScrollTo(itemCount) before proceeding with measuring the passed Measurable. then during lazy list measuring it will try to scroll to the itemCount item, so it will use the very last possible scroll position (as we requested to make the next to last item visible). hacky, but could unblock you. after that you can file a feature request for us
a
Can you just calculate the delta of IME bottom inset and call
LazyListState.scrollBy()
with that value? Maybe something like this (not verified):
Copy code
val listState = rememberLazyListState()
val imeInsets = WindowInsets.ime
val density = LocalDensity.current
LaunchedEffect(listState, imeInsets, density) {
    var prev = imeInsets.getBottom(density)
    snapshotFlow { imeInsets.getBottom(density) }.collect {
        val delta = it - prev
        if (delta != 0) {
            prev = it
            listState.scrollBy(delta.toFloat())
        }
    }
}
a
you can try. coroutines might add extra latency here making it look differently
n
@Albert Chang I just tried it. This method will make LazyColumn move too much offset when ime is closed.
a
yeah, that why I proposed trying a layout modifier and state.requestScrollTo(), as this is a not suspend function and we can control when do we scroll
🙏 1
s
For the solution suggested here https://kotlinlang.slack.com/archives/CJLTWPH7S/p1718804924289859?thread_ts=1718800385.145409&amp;cid=CJLTWPH7S I notice that this snippet seems to also have the side effect that when you come back into a screen with a LazyLayout, this side effect seems to trigger and you end up scrolling to the top again instead if just staying at your remembered scroll position. Is this an expected side effect?
I just changed the if check to:
Copy code
if (!lazyListState.canScrollBackward && lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty()) {
As a hack to "skip" the first frame where there are no items in the list, which I assumed is what made
!lazyListState.canScrollBackward
evaluate to true, and it seems to work fine for my super limited testing right now. Would still love to hear your thoughts on this though