How can I detect when they user is scrolling up or down in a LazyColumn? I'm trying to hide an element on the screen when the user scrolls down and show it again when the user begins to scroll upwards.
This can be done by comparing lazyListState.firstVisibleItemIndex
and lazyListState.firstVisibleItemScrollOffset
with their previous values.
You can encapsulate this logic like LazyListState does
class DirectionalLazyListState(
private val lazyListState: LazyListState
) {
private var positionY = lazyListState.firstVisibleItemScrollOffset
private var visibleItem = lazyListState.firstVisibleItemIndex
val scrollDirection by derivedStateOf {
if (lazyListState.isScrollInProgress.not()) {
ScrollDirection.None
} else {
val firstVisibleItemIndex = lazyListState.firstVisibleItemIndex
val firstVisibleItemScrollOffset =
lazyListState.firstVisibleItemScrollOffset
// We are scrolling while first visible item hasn't changed yet
if (firstVisibleItemIndex == visibleItem) {
val direction = if (firstVisibleItemScrollOffset > positionY) {
ScrollDirection.Down
} else {
ScrollDirection.Up
}
positionY = firstVisibleItemScrollOffset
direction
} else {
val direction = if (firstVisibleItemIndex > visibleItem) {
ScrollDirection.Down
} else {
ScrollDirection.Up
}
positionY = firstVisibleItemScrollOffset
visibleItem = firstVisibleItemIndex
direction
}
}
}
}
Call constructor via remember
@Composable
fun rememberDirectionalLazyListState(
lazyListState: LazyListState,
): DirectionalLazyListState {
return remember {
DirectionalLazyListState(lazyListState)
}
}
And use it
val lazyListState = rememberLazyListState()
val directionalLazyListState = rememberDirectionalLazyListState(
lazyListState
)
Enum class
enum class ScrollDirection {
Up, Down, None
}
Full demo
@Preview
@Composable
private fun ScrollDirectionSample() {
val lazyListState = rememberLazyListState()
val directionalLazyListState = rememberDirectionalLazyListState(
lazyListState
)
val text by remember {
derivedStateOf {
"isScrollInProgress: ${lazyListState.isScrollInProgress}\n" +
"firstVisibleItemIndex: ${lazyListState.firstVisibleItemIndex}\n" +
"firstVisibleItemScrollOffset: ${lazyListState.firstVisibleItemScrollOffset}"
}
}
val color = when (directionalLazyListState.scrollDirection) {
ScrollDirection.Up -> Color.Green
ScrollDirection.Down -> Color.Blue
else -> Color.Black
}
Column {
Text(text, fontSize = 16.sp)
Text(
"Direction: ${directionalLazyListState.scrollDirection}",
fontSize = 24.sp,
color = color,
fontWeight = FontWeight.Bold
)
LazyColumn(
state = lazyListState,
modifier = Modifier.fillMaxSize()
) {
items(50) {
Text(
text = "Row $it",
fontSize = 22.sp,
color = Color.White,
modifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(8.dp)
)
}
}
}
}
The one above can't determine whether finger is moving or down or idle while it's pressed because isScrollInProgress
returns true when pressed whether pointer is not moving or not. You can add a timeout to set to none if user doesn't move pointer while pointer is pressed or isScrollInProgress
is true.
@Stable
class DirectionalLazyListState(
private val lazyListState: LazyListState,
private val coroutineScope: CoroutineScope
) {
private var positionY = lazyListState.firstVisibleItemScrollOffset
private var visibleItem = lazyListState.firstVisibleItemIndex
private var currentTime = System.currentTimeMillis()
var scrollDirection by mutableStateOf(ScrollDirection.None)
init {
coroutineScope.launch {
while (isActive) {
delay(120)
if (System.currentTimeMillis() - currentTime > 120) {
scrollDirection = ScrollDirection.None
}
}
}
snapshotFlow {
val scrollInt = if (lazyListState.isScrollInProgress) 20000 else 10000
val visibleItemInt = lazyListState.firstVisibleItemIndex * 10
scrollInt + visibleItemInt + lazyListState.firstVisibleItemScrollOffset
}
.onEach {
if (lazyListState.isScrollInProgress.not()) {
scrollDirection = ScrollDirection.None
} else {
currentTime = System.currentTimeMillis()
val firstVisibleItemIndex = lazyListState.firstVisibleItemIndex
val firstVisibleItemScrollOffset =
lazyListState.firstVisibleItemScrollOffset
// We are scrolling while first visible item hasn't changed yet
if (firstVisibleItemIndex == visibleItem) {
val direction = if (firstVisibleItemScrollOffset > positionY) {
ScrollDirection.Down
} else {
ScrollDirection.Up
}
positionY = firstVisibleItemScrollOffset
scrollDirection = direction
} else {
val direction = if (firstVisibleItemIndex > visibleItem) {
ScrollDirection.Down
} else {
ScrollDirection.Up
}
positionY = firstVisibleItemScrollOffset
visibleItem = firstVisibleItemIndex
scrollDirection = direction
}
}
}
.launchIn(coroutineScope)
}
// val scrollDirection by derivedStateOf {
// if (lazyListState.isScrollInProgress.not()) {
// ScrollDirection.None
// } else {
// val firstVisibleItemIndex = lazyListState.firstVisibleItemIndex
// val firstVisibleItemScrollOffset =
// lazyListState.firstVisibleItemScrollOffset
//
// // We are scrolling while first visible item hasn't changed yet
// if (firstVisibleItemIndex == visibleItem) {
// val direction = if (firstVisibleItemScrollOffset > positionY) {
// ScrollDirection.Down
// } else {
// ScrollDirection.Up
// }
// positionY = firstVisibleItemScrollOffset
//
// direction
// } else {
//
// val direction = if (firstVisibleItemIndex > visibleItem) {
// ScrollDirection.Down
// } else {
// ScrollDirection.Up
// }
// positionY = firstVisibleItemScrollOffset
// visibleItem = firstVisibleItemIndex
// direction
// }
// }
// }
}