How to scroll a lazy column by 100 px every 1 seco...
# compose
k
How to scroll a lazy column by 100 px every 1 second? If I use this code, the
animateScrollBy
can be cancelled if I scroll the lazy column at the same time, and then the entire LaunchedEffect will be cancelled.
Copy code
val scrollState = rememberLazyListState()

LaunchedEffect(Unit) {
    while (true) {
        delay(1000L)
        scrollState.animateScrollBy(100f)
    }
}
My other solution that works is to use other LaunchedEffect to trigger the scroll. But it's still using a random number. Is there any better solution?
Copy code
val scrollState = rememberLazyListState()
var randomNumber by remember { mutableStateOf(0) }

LaunchedEffect(Unit) {
    while (true) {
        delay(1000L)
        randomNumber = Random.nextInt()
    }
}

LaunchedEffect(randomNumber) {
    scrollState.animateScrollBy(100f)
}
l
The object you pass to LaunchEffect will trigger a recomposition if it changes, so if you pass the scroll state instead of Unit, you can retrigger the while loop when the state becomes idle
So instead of the while loop, you would just have the delay and the animateScrollBy() call. The scroll will trigger a change of state, that in turn will trigger the launchedEffect to start again
k
Do you mean like this? The
scrollState
is not changed after scrolling, so it won't trigger the
LaunchedEffect
again.
Copy code
val scrollState = rememberLazyListState()

LaunchedEffect(scrollState) {
    delay(1000L)
    scrollState.animateScrollBy(100f)
}
l
There is a
isScrollInProgress
property that you can use to trigger it
Copy code
val scrollState = rememberLazyListState()

LaunchedEffect(scrollState.isScrollInProgress) {
    if (scrollState.isScrollInProgress) return 
    delay(1000L)
    scrollState.animateScrollBy(100f)
}
a
isScrollInProgress
returns true for programmatical scroll as well so I don't think it will work. Simply catching the
CancellationException
should be enough:
Copy code
LaunchedEffect(scrollState) {
    while (true) {
        delay(1000L)
        runCatching { scrollState.animateScrollBy(100f) }
    }
}
👍 2
k
@Luis Thanks for helping, but that doesn't work because
animateScrollBy
will change the
isScrollInProgress
state into true. I can remove the
if (scrollState.isScrollInProgress) return
statement, but the delay will be run twice before it's actually scrolling
l
Yes that is the point. I haven’t tested it, maybe the return is missing a @launchedEffect or something. But in my head it goes: 1. First composition, isScrollInProgress is false so it delays and does animateScrollBY 2. inScrollInProgress changes to true, which triggers a recomposition of LaunchedEffect 3. Because inScrollInProgress is true, the return statement makes it so it doesn’t do anything (and the scroll completes) 4. When the scroll completes, isScrollInProgress goes to false, triggers a recomposition, and the delay + animateScrollBy() go again
Is the scroll interrupted by change in scroll state?
a
animateScrollBy
is a suspend fun. It'll be canceled when the coroutine scope is canceled.
☝️ 1
l
Ah I see. Yeh then your solution makes sense!
k
@Albert Chang thanks, it works. However I've heard that you can't try catch any exception inside a coroutine because it'll catch the
CancellationException
which will break the coroutine cancellations. I'll try to cancel the launched effect when
animateScrollBy
is still running https://twitter.com/Zhuinden/status/1389063911702470656
@Albert Chang You shouldn't
runCatching
on a coroutine block, because the code inside
LaunchedEffect
will still run even after it's cancelled. https://gist.github.com/kefasjwiryadi/6c042a6874598ddee88e476ea4523793
a
You can add a check:
Copy code
LaunchedEffect(scrollState) {
    while (isActive) {
        delay(1000L)
        runCatching { scrollState.animateScrollBy(100f) }
    }
}
🙏 1
k
I've found another solution so that we don't need to use try catch. Is this acceptable? Apparently if the child's
launch
get cancelled, the parent's (LaunchedEffect) scope doesn't get cancelled.
Copy code
LaunchedEffect(scrollState) {
    while (true) {
        delay(1000L)
        launch {
            scrollState.animateScrollBy(100f)
        }
    }
}
l
Good one! I easily forget that we can spawn coroutines on other coroutines 😅 I would think its fine, the while loop gets cancelled when the scrollstate changes right?
k
Yeah it should be.
a
Note that this is not completely the same as my version. In my version, a new scroll starts 1 second after the previous scroll finishes, while in your version it starts 1 second after the start of the previous scroll.
👍 1
k
Yeah I understand that, the coroutine doesn't get suspended on
launch
👍 1
m
scrollState
implements
ScrollableState
interface, which supports priority based scroll. It's not available out of the box in
animateScrollTo
, since disrupting and preventing user input is not something that should be easily accessible from the API design perspective. But it's doable if you really want it. What you are looking for is
suspend fun ScrollableState::scroll(priority:MutatePriority)
where you can choose the right priority and prevent user input and implement your own animation like so: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]undation/gestures/ScrollExtensions.kt;l=35?q=ScrollExtensions
k
Thanks, but I don't actually want to prevent user input. It's ok that the "auto scroll" be paused while the user is scrolling, but what I don't want is to cancel the "auto scroll" completely
190 Views