返回

Compose 中如何实现 BottomSheet 嵌套滚动?

Android

Compose 中 BottomSheet 如何实现嵌套滚动?

在 Android 开发中,BottomSheet 是一个常用的 UI 组件,它可以从屏幕底部滑出,展示更多内容或提供操作选项。在使用 Jetpack Compose 构建 UI 时,开发者常常希望 BottomSheet 内部的内容可以滚动,并在滚动到顶部后,将滚动事件传递给外部的 BottomSheet,从而实现更自然的交互体验。

然而,仅仅将内容包裹在 Column 中并使用 verticalScroll 并不能完全满足需求。特别是在内容高度超过 BottomSheet 可见区域,且在不同屏幕尺寸的设备上,可能会导致滚动冲突,影响用户体验。

本文将深入探讨如何在 Compose 中实现 BottomSheet 的嵌套滚动,以解决滚动冲突,打造更流畅的用户体验。

滚动冲突:从何而来?

让我们先来分析一下,为什么简单的 verticalScroll 无法满足 BottomSheet 的嵌套滚动需求。

假设我们有一个 BottomSheet,其中包含一个列表,代码如下:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyBottomSheet(
    onClose: () -> Unit,
    onAcceptButton: () -> Unit,
    onRejectButton: () -> Unit
) {
    val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
        bottomSheetState = BottomSheetState(BottomSheetValue.Expanded)
    )
    BottomSheetScaffold(
        scaffoldState = bottomSheetScaffoldState,
        sheetContent = {
            Surface {
                Column(
                    modifier = Modifier.weight(1f, false),
                ) { 
                    // 列表内容
                    Column(Modifier.verticalScroll(rememberScrollState())) {
                        for (i in 1..50) {
                            Text(text = "Item $i", modifier = Modifier.padding(16.dp))
                        }
                    }
                }
                // 底部操作按钮
                StickyBottom(onAcceptButton = onAcceptButton, onRejectButton = onRejectButton)
            }
        },
        // ... 其他 BottomSheetScaffold 参数 ...
    ) {
        // ... BottomSheetScaffold  内容...
    }
}

上述代码中,BottomSheet 的内容被放置在一个 Column 中,列表使用 verticalScroll(rememberScrollState()) 实现滚动。

这种方式在列表内容高度不超过 BottomSheet 可见区域时表现正常。然而,当列表内容高度超过 BottomSheet 可见区域时,问题就出现了:

  1. 在较小的设备上: 当用户向上滚动列表,滚动到顶部后继续滚动,会导致 BottomSheet 自身被拖动,而不是继续滚动列表内容。这是因为 verticalScroll 并没有处理滚动事件传递的机制,它仅仅是允许内容在垂直方向上滚动。
  2. 在较大的设备上: 由于 BottomSheet 的高度足够容纳列表内容,不会出现滚动冲突。

nestedScroll: 解决滚动冲突的关键

为了解决上述滚动冲突问题,Compose 提供了 nestedScroll Modifier。nestedScroll 允许我们在可滚动组件之间建立联系,实现嵌套滚动的效果。

让我们对之前的代码进行修改,引入 nestedScroll

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyBottomSheet(
    onClose: () -> Unit,
    onAcceptButton: () -> Unit,
    onRejectButton: () -> Unit
) {
    val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
        bottomSheetState = BottomSheetState(BottomSheetValue.Expanded)
    )
    val scrollState = rememberScrollState()
    BottomSheetScaffold(
        scaffoldState = bottomSheetScaffoldState,
        sheetContent = {
            Surface {
                Column(
                    modifier = Modifier
                        .nestedScroll(rememberNestedScrollConnection()) // 注意这里
                        .weight(1f, false),
                ) { 
                    // 列表内容
                    Column(Modifier.verticalScroll(scrollState)) {
                        for (i in 1..50) {
                            Text(text = "Item $i", modifier = Modifier.padding(16.dp))
                        }
                    }
                }
                // 底部操作按钮
                StickyBottom(onAcceptButton = onAcceptButton, onRejectButton = onRejectButton)
            }
        },
        // ... 其他 BottomSheetScaffold 参数 ...
    ) {
        // ... BottomSheetScaffold  内容...
    }
}

代码解析:

  1. 引入 nestedScroll: 我们在 Column 的 Modifier 中添加了 nestedScroll(rememberNestedScrollConnection())。这将使 Column 参与到嵌套滚动系统中。
  2. rememberNestedScrollConnection: 该函数创建一个 NestedScrollConnection,用于处理 Column 与其父级可滚动组件(这里是 BottomSheet)之间的滚动事件传递。

nestedScroll 工作原理

使用 nestedScroll 后,当用户在 Column 上进行滚动操作时,NestedScrollConnection 会将滚动事件传递给 BottomSheet。

  • 如果 Column 尚未滚动到顶部NestedScrollConnection消费滚动事件 ,使列表内容滚动。
  • 如果 Column 已经滚动到顶部NestedScrollConnection 会将滚动事件传递给 BottomSheet ,BottomSheet 就会接管滚动事件,自身向上滑动或跟随滑动。

通过这种方式,nestedScroll 巧妙地解决了 BottomSheet 和其内部可滚动内容之间的滚动冲突,实现了更符合预期的嵌套滚动效果。

总结

在 Compose 中实现 BottomSheet 的嵌套滚动,关键在于使用 nestedScroll Modifier 建立父子组件之间的滚动联系,并利用 NestedScrollConnection 处理滚动事件的传递。

nestedScroll 不仅可以用于 BottomSheet,还适用于其他需要嵌套滚动的场景。开发者可以根据实际需求,灵活运用 nestedScroll,打造更加流畅、自然的 Android 应用交互体验。

常见问题解答

1. nestedScrollverticalScroll 有什么区别?

  • verticalScroll 仅仅是允许组件内容在垂直方向上滚动,不处理滚动事件传递。
  • nestedScroll 允许多个可滚动组件之间建立联系,实现嵌套滚动,并处理滚动事件的传递。

2. 为什么需要使用 rememberNestedScrollConnection

rememberNestedScrollConnection 用于创建一个 NestedScrollConnection 对象,该对象负责处理嵌套滚动过程中父子组件之间的滚动事件传递。

3. 除了 BottomSheet,还有哪些场景可以使用 nestedScroll

任何需要嵌套滚动的场景都可以使用 nestedScroll,例如:

  • LazyColumnLazyRow 内部嵌套另一个可滚动组件。
  • 自定义可滚动组件需要支持嵌套滚动。

4. nestedScroll 会影响性能吗?

nestedScroll 本身不会对性能造成显著影响。但是,如果嵌套滚动的层级过多,或者滚动内容过于复杂,就需要关注性能问题,并进行适当的优化。

5. 还有其他方法可以实现 BottomSheet 的嵌套滚动吗?

除了使用 nestedScroll,还可以通过自定义布局和触摸事件处理来实现 BottomSheet 的嵌套滚动。但是,这种方法实现起来比较复杂,需要开发者具备更深入的 Android 触摸事件机制的理解。