如何检测 LazyColumn 中的滚动行为?

发布于 2025-01-11 10:29:13 字数 2352 浏览 0 评论 0原文

我正在尝试检测三种情况:

1.- 用户垂直(向下)滚动并通知隐藏按钮

2.- 用户停止滚动并通知隐藏按钮

3.- 用户垂直(向上)滚动并通知显示按钮

4.- 用户位于列表底部,没有更多项目并通知显示按钮。

我尝试过的是:

第一种方法是使用nestedScrollConnection,如下所示

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),
            )

。 0 正在上升,否则正在下降,但其他我不知道如何获得它们。

我遵循的另一种方法是:

val scrollState = rememberLazyListState()
LazyColumn(
                modifier = Modifier
                    .padding(start = 16.dp, end = 16.dp, top = 16.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp),
                state = scrollState,

但我不知道如何获取它是否是最后一项,我可以获取滚动是否正在进行。

注意

Skizo 的答案是有效的,但是这有点奇怪,因为如果你慢慢向上滚动,Y 有时不是我想要的,然后再次隐藏它,有没有办法留下一些滚动来开始对此做出反应?例如,滚动 X 像素即可开始显示/隐藏。

我想要的是隐藏/显示一个浮动操作按钮,具体取决于滚动(场景是上面的场景)

我找到了这种方式,但它使用了偏移量,我想为 制作动画FloatActionButton 而不是像淡入/淡出那样从底部出现,我使用的是 动画可见性 我得到了这个工作淡入/淡出,但现在,我如何调整 github 上的代码以使用动画可见性?并且当用户结束滚动时添加此内容,从现在开始在代码中只是在滚动时

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.

Note

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

沐歌 2025-01-18 10:29:13

以下是实现这些目标的方法:

1.) 检测滚动方向(垂直向上或垂直向下)

@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
}

现在,只需创建一个 listState 变量并将其与此可组合项一起使用即可检索滚动方向。

val listState = rememberLazyListState()
val scrollingUp = listState.isScrollingUp()

然后,正如您所说,您希望在用户停止滚动时收到通知,因此您只需创建一个名为 restingScrollPosition 的变量,我将其命名为 rsp简称。现在您已经熟悉了所需的帮助器方法,所需要做的就是设计一种机制来根据当前滚动位置的值触发事件,如果特定数量的值相同,则触发相关代码时间(卷轴的定义是“静止”)。

所以,

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 */
    }
 }
}

我不认为我会在这里解释最后一段代码的工作原理,请自己分析以获得更好的理解,这并不难。

啊,是的,要实现最后一个目标,您只需对 API 使用一点虚张声势,特别是像 lazyListState.layoutInfo 这样的方法。它包含您需要的有关屏幕上当前可见项目的所有信息,因此您还可以使用它来实现您的微妙需求,即您希望在触发代码块之前允许滚动一定量。只需查看对象中的可用信息,您就应该能够启动它。

更新:

根据昨天添加的评论中提供的信息,这应该是实现,

您在层次结构中的某个位置有一个 FAB,并且您希望根据动画它的可见性>isScrollingUp,它绑定到层次结构中其他地方定义的Lazy Scroller,

在这种情况下,你可以看一下状态提升,这是声明式编程的一般最佳实践。

只需将 isScrollingUp() 输出提升到声明 FAB 的位置即可,或者如果您需要根据您的用例进行特定说明,请分享完整的代码。我需要整个家族能够帮助你解决这个问题。

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.

零度° 2025-01-18 10:29:13

几天前我也遇到了同样的问题,我按照你说的做了一些混合。

要显示或隐藏,然后使用 Y 向上或向下滚动就足够了。

val nestedScrollConnection = remember {
   object : NestedScrollConnection {
             override fun onPreScroll(
             available: Offset,
             source: NestedScrollSource
         ): Offset {
            val delta = available.y
            isVisible.targetState = delta > 0
            return Offset.Zero
        }
    }
}

并检测没有更多可以使用的物品

fun isLastItemVisible(lazyListState: LazyListState): Boolean {
 val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
 return lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
}

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
}
笨死的猪 2025-01-18 10:29:13

对于 1 和 3,如果您想在显示/隐藏某些内容之前指定特定的滚动距离,您可以创建一个具有自己状态的 NestedScrollConnection

这是一个可能会给您一些想法的实现示例。当达到 downThreshold 时,CustomScrollConnection.isScrollingDown 将返回 true,当达到 upThreshold 时,它将更改为 false已达到

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
    }
}

您可以在将实例传递给 nestedScroll 修饰符之前创建一个这样的实例。

val localDensity = LocalDensity.current

val customScrollConnection = remember(localDensity) {
    CustomScrollConnection(
        downThreshold = DOWN_SCROLL_THRESHOLD,
        upThreshold = UP_SCROLL_THRESHOLD,
        density = localDensity
    )
}

需要额外的代码才能将其与 rememberSaveable 一起使用。

对于 2,如果您使用的是 LazyListState,您可以查看 !lazyListState.isScrollInProgress 是否有帮助。

对于 4,您可以尝试使用 !lazyListState.canScrollForward。或者,在 NestedScrollConnection 的实现中,如果您重写 onPostScroll,则可以检查 consumed.y == 0f &&可用。 0f;这可能意味着您已触底。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文