嵌套滚动是一种系统,其中包含在彼此内部的多个滚动组件通过对单个滚动手势做出反应并传递其滚动增量(更改)来协同工作。
嵌套滚动系统允许在可滚动且按层次结构链接(通常通过共享同一父项)的组件之间进行协调。 此系统会链接滚动容器,并允许与在它们之间传播和共享的滚动增量进行交互。
Compose 提供了多种处理可组合项之间嵌套滚动的方法。 例如,在一个列表中嵌套另一个列表就是一种典型的嵌套滚动,而一种更 复杂的情况是收起 工具栏。
自动嵌套滚动
简单的嵌套滚动无需您执行任何操作。启动滚动操作的手势会自动从子级传播到父级,这样一来,当子级无法进一步滚动时,手势就会由其父元素处理。
部分
Compose 组件和修饰符原生支持自动嵌套滚动,包括:
verticalScroll、
horizontalScroll、
scrollable、
Lazy API 和 TextField。这意味着,当用户滚动嵌套组件的内部子级时,之前的修饰符会将滚动增量传播到支持嵌套滚动的父级。
以下示例展示了在容器内应用了
verticalScroll
修饰符的元素,该容器本身也应用了 verticalScroll
修饰符。
@Composable private fun AutomaticNestedScroll() { val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White) Box( modifier = Modifier .background(Color.LightGray) .verticalScroll(rememberScrollState()) .padding(32.dp) ) { Column { repeat(6) { Box( modifier = Modifier .height(128.dp) .verticalScroll(rememberScrollState()) ) { Text( "Scroll here", modifier = Modifier .border(12.dp, Color.DarkGray) .background(brush = gradient) .padding(24.dp) .height(150.dp) ) } } } } }
使用 nestedScroll 修饰符
如果您需要在多个元素之间创建高级协调滚动,
the
nestedScroll
修饰符可以通过定义嵌套滚动层次结构来提供更大的灵活性。如上一部分所述,某些组件内置了嵌套滚动支持。但是,对于不可自动滚动的可组合项(例如 Box 或 Column),此类组件上的滚动增量不会在嵌套滚动系统中传播,并且增量不会到达 NestedScrollConnection 或父组件。如要解决此问题,您可以使用 nestedScroll 为其他组件(包括自定义组件)提供此类支持。
嵌套滚动周期
嵌套滚动周期是指滚动增量的流动,这些增量通过嵌套滚动系统中的所有组件(或节点)在层次结构树中上下分派,例如通过使用可滚动组件和修饰符或 nestedScroll。
嵌套滚动周期的阶段
当可滚动组件检测到触发事件(例如手势)时,在实际滚动操作触发之前,生成的增量会发送到嵌套滚动系统并经历三个阶段:滚动前、节点使用和滚动后。
在第一个滚动前阶段,接收到触发事件增量的组件会将这些事件向上分派到层次结构树中的最顶层父项。然后,增量事件会向下冒泡,这意味着增量将从最根层的父项向下传播到启动嵌套滚动周期的子项。
这样,嵌套滚动父项(使用 nestedScroll 或可滚动修饰符的可组合项)就有机会在节点本身使用增量之前对增量执行某些操作。
在节点使用阶段,节点本身将使用其父项未使用的任何增量。此时,滚动移动实际上已完成并且可见。
在此阶段,子项可以选择使用剩余滚动的所有或部分。任何剩余内容都将发送回上层,以进入滚动后阶段。
最后,在滚动后阶段,节点本身未使用的任何内容都将再次发送给其祖先以供使用。
滚动后阶段的工作方式与滚动前阶段类似,任何父项都可以选择使用或不使用。
与滚动类似,当拖动手势完成时,用户的意图可能会转换为用于轻拂(使用动画滚动)可滚动容器的速度。轻拂也是嵌套滚动周期的一部分,并且拖动事件生成的速度会经历类似的阶段:轻拂前、节点使用和轻拂后。请注意,轻拂动画仅与触摸手势相关联,不会由其他事件(例如无障碍功能或硬件滚动)触发。
参与嵌套滚动周期
参与周期意味着拦截、使用和报告沿层次结构使用的增量。Compose 提供了一组工具来影响嵌套滚动系统的工作方式以及如何直接与其交互,例如,当您需要在可滚动组件开始滚动之前对滚动增量执行某些操作时。
如果嵌套滚动周期是一个对节点链起作用的系统,则
nestedScroll
修饰符是一种拦截和插入这些更改的方式,并
影响链中传播的数据(滚动增量)。此修饰符可以放置在层次结构中的任何位置,并且它会与树中的嵌套滚动修饰符实例进行通信,以便通过此渠道共享信息。此修饰符的构建块是 NestedScrollConnection 和 NestedScrollDispatcher。
NestedScrollConnection
提供了一种响应嵌套滚动周期阶段并影响嵌套滚动系统的方式。它由四个回调方法组成,每个方法代表一个使用阶段:滚动前/后和轻拂前/后:
val nestedScrollConnection = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { println("Received onPreScroll callback.") return Offset.Zero } override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { println("Received onPostScroll callback.") return Offset.Zero } }
每个回调还提供有关正在传播的增量的信息:该特定阶段的 available 增量,以及在先前阶段中使用的 consumed 增量。如果您希望在任何时候停止向上层传播增量,可以使用嵌套滚动连接来执行此操作:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
所有回调都提供有关
NestedScrollSource
类型的信息。
NestedScrollDispatcher
初始化嵌套滚动周期。使用调度程序并调用其方法会触发该周期。可滚动容器具有内置的调度程序,该调度程序会将手势期间捕获的增量发送到系统中。因此,自定义嵌套滚动的大多数用例都涉及使用 NestedScrollConnection 而不是调度程序,以响应已存在的增量而不是发送新的增量。
如需了解更多用法,请参阅
NestedScrollDispatcherSample
。
在滚动时调整图片大小
当用户滚动时,您可以创建动态视觉效果,其中图片的大小会根据滚动位置而变化。
根据滚动位置调整图片大小
此代码段演示了如何根据
垂直滚动位置调整 LazyColumn 中的图片大小。当用户向下滚动时,图片会缩小;当用户向上滚动时,图片会放大,并保持在定义的最小和最大大小范围内:
@Composable fun ImageResizeOnScrollExample( modifier: Modifier = Modifier, maxImageSize: Dp = 300.dp, minImageSize: Dp = 100.dp ) { var currentImageSize by remember { mutableStateOf(maxImageSize) } var imageScale by remember { mutableFloatStateOf(1f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Calculate the change in image size based on scroll delta val delta = available.y val newImageSize = currentImageSize + delta.dp val previousImageSize = currentImageSize // Constrain the image size within the allowed bounds currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize) val consumed = currentImageSize - previousImageSize // Calculate the scale for the image imageScale = currentImageSize / maxImageSize // Return the consumed scroll amount return Offset(0f, consumed.value) } } } Box(Modifier.nestedScroll(nestedScrollConnection)) { LazyColumn( Modifier .fillMaxWidth() .padding(15.dp) .offset { IntOffset(0, currentImageSize.roundToPx()) } ) { // Placeholder list items items(100, key = { it }) { Text( text = "Item: $it", style = MaterialTheme.typography.bodyLarge ) } } Image( painter = ColorPainter(Color.Red), contentDescription = "Red color image", Modifier .size(maxImageSize) .align(Alignment.TopCenter) .graphicsLayer { scaleX = imageScale scaleY = imageScale // Center the image vertically as it scales translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f } ) } }
代码要点
- 此代码使用
NestedScrollConnection拦截滚动事件。 onPreScroll根据滚动增量计算图片大小的变化。currentImageSize状态变量存储图片的当前大小,该大小介于minImageSize和maxImageSize. imageScale之间,派生自currentImageSize。LazyColumn根据currentImageSize进行偏移。Image使用graphicsLayer修饰符来应用计算出的 缩放比例。graphicsLayer中的translationY可确保图片在缩放时保持垂直居中。
结果
上面的代码段会在滚动时产生缩放图片的效果:
嵌套滚动互操作性
当您尝试在可滚动的可组合项中嵌套可滚动的 View 元素时,可能会遇到问题,反之亦然。最明显的问题是,当您滚动子项并到达其开始或结束边界时,您希望父项接管滚动。但是,这种预期行为可能不会发生,或者可能无法按预期工作。
此问题是由可滚动的可组合项中内置的预期行为而导致。
可滚动的可组合项有“默认嵌套滚动”规则,这意味着任何可滚动容器都必须通过 NestedScrollConnection 作为父项参与嵌套滚动链,并通过 NestedScrollDispatcher 作为子项参与嵌套滚动链。然后,当子项位于边界上时,子项将为父项推动嵌套滚动。例如,此规则允许 Compose Pager 和 Compose LazyRow 良好地配合工作。然而,当使用 ViewPager2 或 RecyclerView 完成互操作性滚动时,由于不会实现 NestedScrollingParent3,因此无法做到由子项到父项的连续滚动。
如要在可滚动的 View 元素与可滚动的可组合项之间实现双向嵌套的嵌套滚动互操作 API,您可以在下列场景中使用嵌套滚动互操作 API 来缓解这些问题。
包含子项 ComposeView 的协作式父项 View
协作式父项 View 已实现
NestedScrollingParent3
,因此能够从参与协作的嵌套
子级可组合项接收滚动增量。ComposeView 在这种情况下将充当子项,并且
需要(间接)实现
NestedScrollingChild3。
协作式父项的一个示例是 androidx.coordinatorlayout.widget.CoordinatorLayout。
如果您需要在可滚动的 View 父级
容器与嵌套的可滚动子级可组合项之间实现嵌套滚动互操作性,可以使用
rememberNestedScrollInteropConnection()。
rememberNestedScrollInteropConnection() 会允许并记住 NestedScrollConnection,后者支持在实现 NestedScrollingParent3 的 View 父项和 Compose 子项之间实现嵌套滚动互操作性。此方法应与
nestedScroll
修饰符结合使用。由于嵌套滚动在 Compose 端默认处于启用状态,因此您
可以使用此连接在 View 端实现嵌套滚动,并在
Views 和可组合项之间添加必要的粘合逻辑。
一个常见的用例是使用 CoordinatorLayout、CollapsingToolbarLayout 和子级可组合项,如以下示例所示:
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="100dp" android:fitsSystemWindows="true"> <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <!--...--> </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_width="match_parent" android:layout_height="match_parent"/> </androidx.coordinatorlayout.widget.CoordinatorLayout>
在您的 Activity 或 Fragment 中,您需要设置子级可组合项和所需的
required
NestedScrollConnection:
open class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<ComposeView>(R.id.compose_view).apply { setContent { val nestedScrollInterop = rememberNestedScrollInteropConnection() // Add the nested scroll connection to your top level @Composable element // using the nestedScroll modifier. LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) { items(20) { item -> Box( modifier = Modifier .padding(16.dp) .height(56.dp) .fillMaxWidth() .background(Color.Gray), contentAlignment = Alignment.Center ) { Text(item.toString()) } } } } } } }
包含子项 AndroidView 的父级可组合项
此场景涵盖了当您的父级可组合项包含子级 AndroidView 时,对 Compose 端嵌套滚动互操作 API 的实现。AndroidView 会实现
NestedScrollDispatcher,
因为它充当 Compose 滚动父项的子项;还会实现
NestedScrollingParent3
,因为它充当 View 滚动子项的父项。然后,Compose 父项将能够从嵌套的可滚动子级 View 接收嵌套滚动增量。
下面的示例展示了在此场景中如何使用 Compose 收起工具栏来实现嵌套滚动互操作性:
@Composable private fun NestedScrollInteropComposeParentWithAndroidChildExample() { val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() } val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } // Sets up the nested scroll connection between the Box composable parent // and the child AndroidView containing the RecyclerView val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Updates the toolbar offset based on the scroll to enable // collapsible behaviour val delta = available.y val newOffset = toolbarOffsetHeightPx.value + delta toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f) return Offset.Zero } } } Box( Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection) ) { TopAppBar( title = {}, modifier = Modifier .height(ToolbarHeight) .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) } ) AndroidView( { context -> LayoutInflater.from(context) .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply { with(findViewById<RecyclerView>(R.id.main_list)) { layoutManager = LinearLayoutManager(context, VERTICAL, false) adapter = NestedScrollInteropAdapter() } }.also { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(it, true) } }, // ... ) } } private class NestedScrollInteropAdapter : Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() { val items = (1..10).map { it.toString() } override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): NestedScrollInteropViewHolder { return NestedScrollInteropViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.list_item, parent, false) ) } override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) { // ... } class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) { fun bind(item: String) { // ... } } // ... }
此示例展示了如何将该 API 与 scrollable 修饰符结合使用:
@Composable fun ViewInComposeNestedScrollInteropExample() { Box( Modifier .fillMaxSize() .scrollable( rememberScrollableState { // View component deltas should be reflected in Compose // components that participate in nested scrolling it }, Orientation.Vertical ) ) { AndroidView( { context -> LayoutInflater.from(context) .inflate(R.layout.list_item, null) .apply { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(this, true) } } ) } }
最后,此示例展示了如何将嵌套滚动互操作 API 与
BottomSheetDialogFragment
结合使用,以实现成功的拖动和关闭操作:
class BottomSheetFragment : BottomSheetDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false) rootView.findViewById<ComposeView>(R.id.compose_view).apply { setContent { val nestedScrollInterop = rememberNestedScrollInteropConnection() LazyColumn( Modifier .nestedScroll(nestedScrollInterop) .fillMaxSize() ) { item { Text(text = "Bottom sheet title") } items(10) { Text( text = "List item number $it", modifier = Modifier.fillMaxWidth() ) } } } return rootView } } }
请注意,
rememberNestedScrollInteropConnection()
会在您附加到的元素中安装
NestedScrollConnection
。NestedScrollConnection 负责将增量从 Compose 级别传输到 View 级别。这使元素能够参与嵌套滚动,但不会自动启用元素滚动。对于不可自动滚动的可组合项(例如 Box 或 Column),此类组件上的滚动增量不会在嵌套滚动系统中传播,并且增量不会到达 rememberNestedScrollInteropConnection() 提供的 NestedScrollConnection,因此这些增量不会到达父级 View 组件。如要解决此问题,请确保将可滚动的修饰符也设置为这些类型的嵌套可组合项。如需了解详情,请参阅上文中关于嵌套
滚动的部分。
包含子项 ComposeView 的非协作式父项 View
非协作式 View 无法在 View 端实现必要的 NestedScrolling 接口。请注意,这意味着这类 Views 不能直接支持嵌套滚动互操作性。非协作式 Views 是 RecyclerView 和 ViewPager2。
其他资源
为你推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 了解手势
- 将
CoordinatorLayout迁移到 Compose - 在 Compose 中使用 View