Reputation: 1031
I'm trying to detect three scenarios :
1.- User scroll vertically (down) and notify to hide a button
2.- User stop scrolls and notify to hide button
3.- User scroll vertically (up) and notify to show the button
4.- User is in the bottom of the list and there are no more items and notify to show the button.
What I've tried is :
First approach is to use nestedScrollConnection
as follows
val isVisible = remember { MutableTransitionState(false) }
.apply { targetState = true }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
return Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
isVisible.targetState = false
return super.onPostScroll(consumed, available, source)
}
}
}
LazyColumn(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
.nestedScroll(nestedScrollConnection),
verticalArrangement = Arrangement.spacedBy(16.dp),
)
What I've tried is when y > 0 is going up, else is going down, but the others I don't know how to get them.
Another approach I followed is :
val scrollState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, top = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
state = scrollState,
But I don't know how to get if it's last item or not, I can get if the scroll is in progress.
Answer from Skizo works but with this it's a bit weird because if you scroll up slowly the Y sometimes is not what I want and then hide it again, is there any way to leave some scroll to start reacting to this? For instance, scroll X pixels to start showing / hiding.
What I want is to hide/show is a Float Action Button depending on the scroll (the scenarios are the ones from above)
I've found this way, but it is using the offset and I'd like to animate the FloatActionButton
instead of appearing from the bottom like a fade in/fade out I was using the Animation Visibility and I got this working with fade in/fade out but now, how can I adapt the code from github to use Animation Visibility
? And also add this when the user ends scrolling that from now in the code is just while scrolling
Upvotes: 5
Views: 5469
Reputation: 96
For 1 and 3, if you want to specify a specific scroll distance before showing/hiding something, you can create a NestedScrollConnection
with its own state.
Here is an example of an implementation that may give you some ideas. CustomScrollConnection.isScrollingDown
would return true
when downThreshold
is reached, and it would change to false
when upThreshold
is reached.
private class CustomScrollConnection(
downThreshold: Dp = 0.dp,
upThreshold: Dp = 0.dp,
density: Density
) : NestedScrollConnection {
var isScrollingDown by mutableStateOf(false)
private set
private val downThresholdPx = with(density) { -downThreshold.toPx() }
private val upThresholdPx = with(density) { upThreshold.toPx() }
private var isScrollingDownDelta = 0f
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
updateState(delta)
return Offset.Zero
}
private fun updateState(delta: Float) {
val isChangingDirections = isScrollingDownDelta.sign != delta.sign
isScrollingDownDelta = if (isChangingDirections) {
delta
} else {
isScrollingDownDelta + delta
}.coerceIn(downThresholdPx, upThresholdPx)
val isUpScroll = isScrollingDownDelta == upThresholdPx && delta > 0
val isDownScroll = isScrollingDownDelta == downThresholdPx && delta < 0
val continueDownScroll = isScrollingDown && !isUpScroll
isScrollingDown = continueDownScroll || isDownScroll
}
}
And you can create an instance like this before passing it to the nestedScroll
modifier.
val localDensity = LocalDensity.current
val customScrollConnection = remember(localDensity) {
CustomScrollConnection(
downThreshold = DOWN_SCROLL_THRESHOLD,
upThreshold = UP_SCROLL_THRESHOLD,
density = localDensity
)
}
Additional code would be needed to use it with rememberSaveable
.
For 2, if you are using LazyListState
, you can see if !lazyListState.isScrollInProgress
helps.
For 4, you can try using !lazyListState.canScrollForward
. Or, in an implementation of NestedScrollConnection
, if you override onPostScroll
, you can check if consumed.y == 0f && available.y < 0f
; this could mean you reached the bottom.
Upvotes: 0
Reputation: 6835
Here's how you'd go about achieving these,
1.) Detect The Scrolling Direction (Vertically Up, or Vertically Down)
@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
}
Now, just create a listState variable and use it with this Composable to retrieve the scrolling dierection.
val listState = rememberLazyListState()
val scrollingUp = listState.isScrollingUp()
Then, as you say, you'd like to get notified if the user stops scrolling, so for that you can just create a variable known as restingScrollPosition
, I'll name it rsp
for short. Now that you are familiar with the required helper methods, all that is required is devising a mechanism to trigger an event based on the value of the current scroll position that triggers the code in concern if the value has been the same for a particular amount of time (the definition of the scroll being "at rest").
So, here it is
var rsp by remember { mutableStateOf(listState.firstVisibleItemScrollOffset) }
var isScrolling by remember { mutableStateOf(false) } // to keep track of the scroll-state
LaunchedEffect(key = listState.firstVisibleItemScrollOffset){ //Recomposes every time the key changes
/*This block will also be executed
on the start of the program,
so you might want to handle
that with the help of another variable.*/
isScrolling = true // If control reaches here, we're scrolling
launch{
isScrolling = false
delay(100) //If there's no scroll after a hundred seconds, update rsp
if(!isScrolling){
rsp = listState.firstVisibleItemScrollOffset
/* Execute your trigger here,
this denotes the scrolling has stopped */
}
}
}
I don't think I would be explaining the workings of the last code here, please analyze it yourself to gain a better understanding, it's not difficult.
Ah yes to achieve the last objective, you can just use a little swashbuckling with the APIs, specifically methods like lazyListState.layoutInfo
. It has all the info you'll require about the items currently visible on-screen, and so you can also use it to implement your subtle need where you wish to allow for a certain amount to be scrolled before triggering the codeblock. Just have a look at the availannle info in the object and you should be able to start it up.
UPDATE:
Based on the info provided in the comments added below as of yesterday, this should be the implementation,
You have a FAB somewhere in your heirarchy, and you wish to animate it's visibility based on the isScrollingUp
, which is bound to the Lazy Scroller defined somewhere else in the heirarchy,
In that case, you can take a look at state-hoisting, which is a general best practice for declarative programming.
Just hoist the isScrollingUp()
output up to the point where your FAB is declared, or please share the complete code if you need specific instructions based on your use-case. I would require the entire heirarchy to be able to help you out with this.
Upvotes: 5
Reputation: 20626
I've faced same problem some days ago and I did a mix of what you say.
To show or hide then scrolling up or down with the Y
is enough.
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
val delta = available.y
isVisible.targetState = delta > 0
return Offset.Zero
}
}
}
And to detect there's no more items you can use
fun isLastItemVisible(lazyListState: LazyListState): Boolean {
val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
return lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
}
Upvotes: 1