For a LazyRow
, or Column
, how to I know whether the user has scrolled left or right ( or up or... you know). We do not need callbacks in compose for stuff like that, since mutableStateOf
objects always anyway trigger recompositions so I just wish to know a way to store it in a variable. Okay so there's lazyRowState.firstVisibleItemScrollOffset
, which can be used to mesaure it in a way, but I can't find a way to store its value first, and then subtract the current value to retrieve the direction (based on positive or negative change). Any ideas on how to do that, thanks
How to retrieve the scrolling direction for LazyRow
Asked Answered
Currently there is no built-in function to get this info from LazyListState
.
You can use something like:
@Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Then just use listState.isScrollingUp()
to get the info about the scroll.
This snippet is used in a google codelab.
Oh wait! It's perfect! Didn't see the conditional logic. Thanks! –
Isocrates
Anything wrong with my implementation below by the way? Seems kinda simpler –
Isocrates
This extensions functions can be very handy for you:
fun LazyListState.isFirstItemVisible() = firstVisibleItemIndex == 0
@Composable
fun LazyListState.isScrollingDown(): Boolean {
val offset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) { derivedStateOf { (firstVisibleItemScrollOffset - offset) > 0 } }.value
}
@Composable
fun LazyListState.isScrollingUp(): Boolean {
val offset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) { derivedStateOf { (firstVisibleItemScrollOffset - offset) < 0 } }.value
}
Got it
{ //Composable Scope
val lazyRowState = rememberLazyListState()
val pOffset = remember { lazyRowState.firstVisibleItemScrollOffset }
val direc = lazyRowState.firstVisibleItemScrollOffset - pOffset
val scrollingRight /*or Down*/ = direc > 0 // Tad'aa
}
I was playing with this a bit. I was not sure, but now I know that the scrolloffset is a per-item value, so it resets when you cross to boundary to the next item. Which means that this will give false reads when crossing the item boundary. Means you have to take both into acccount, item and offset. –
Chaetognath
Thanks for the insight, seems reasonable (TLDR tho, sorry). –
Isocrates
For LazyColumn with specifying the minimum detection step:
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import kotlinx.coroutines.delay
import kotlin.math.absoluteValue
@Composable
fun LazyListState.verticalDebouncedScrollState(
minOffsetForDetection: Int = 10
): State<VerticalScrollDirection> = produceState(
initialValue = VerticalScrollDirection.None
) {
var previousScrollOffset = firstVisibleItemScrollOffset
var previousItemIndex = firstVisibleItemIndex
while (true) {
delay(300L)
val capturedItemIndex = firstVisibleItemIndex
val capturedScrollOffset = firstVisibleItemScrollOffset
val offsetDelta = capturedScrollOffset - previousScrollOffset
val itemIndexDelta = capturedItemIndex - previousItemIndex
value = when {
offsetDelta.absoluteValue < minOffsetForDetection && itemIndexDelta == 0 -> VerticalScrollDirection.None
itemIndexDelta > 0 -> VerticalScrollDirection.Up
itemIndexDelta == 0 && offsetDelta > 0 -> VerticalScrollDirection.Up
else -> VerticalScrollDirection.Down
}
previousScrollOffset = capturedScrollOffset
previousItemIndex = capturedItemIndex
}
}
enum class VerticalScrollDirection {
None, Up, Down
}
Due to false positives when the firstVisibleItemIndex
is changed, I ended up with the following version:
@Composable
fun LazyListState.isScrollingUp(): Boolean {
var previousItemIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }
var scrollingUp by remember(this) { mutableStateOf(true) }
return remember(this) {
derivedStateOf {
if (previousItemIndex == firstVisibleItemIndex) {
scrollingUp = firstVisibleItemScrollOffset - previousScrollOffset <= 0
} else {
previousItemIndex = firstVisibleItemIndex
}
previousScrollOffset = firstVisibleItemScrollOffset
scrollingUp
}
}.value
}
The scroll state is cached and preserved while firstVisibleItemIndex
is changed.
© 2022 - 2024 — McMap. All rights reserved.
1.0.1
? – Isocrates