滚动修饰符
verticalScroll
和 horizontalScroll
修饰符提供一种最简单的方法,可让用户在元素内容边界大于最大尺寸约束时滚动元素。利用 verticalScroll
和 horizontalScroll
修饰符,您无需转换或偏移内容。
@Composable private fun ScrollBoxes() { Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .verticalScroll(rememberScrollState()) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }
ScrollState
可让您更改滚动位置或获取当前状态。如需使用默认参数创建此列表,请使用 rememberScrollState()
。
@Composable private fun ScrollBoxesSmooth() { // Smoothly scroll 100px on first composition val state = rememberScrollState() LaunchedEffect(Unit) { state.animateScrollTo(100) } Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .padding(horizontal = 8.dp) .verticalScroll(state) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }
可滚动的修饰符
通过
scrollable
修饰符与滚动修饰符不同,scrollable
会检测
滚动手势并捕获增量,但不会偏移其内容
。系统会通过
ScrollableState
,此修饰符才能正常运行。
构建 ScrollableState
时,您必须提供 consumeScrollDelta
函数,该函数将在每个滚动步骤中调用(通过手势输入,平滑)
滚动或快速滑动)。该函数必须返回
滚动距离所消耗的量,以确保事件
在存在具有 scrollable
的嵌套元素时传播
修饰符。
以下代码段可检测手势并显示偏移量的数值,但不会偏移任何元素:
@Composable private fun ScrollableSample() { // actual composable state var offset by remember { mutableStateOf(0f) } Box( Modifier .size(150.dp) .scrollable( orientation = Orientation.Vertical, // Scrollable state: describes how to consume // scrolling delta and update offset state = rememberScrollableState { delta -> offset += delta delta } ) .background(Color.LightGray), contentAlignment = Alignment.Center ) { Text(offset.toString()) } }
嵌套滚动
嵌套滚动是一种包含多个滚动组件的系统 相互配合,可以对单个滚动手势做出响应, 并传达其滚动增量(变化)。
通过嵌套滚动系统, 可滚动且分层关联(通常通过共享同一个父级实现)。 此系统关联了滚动容器,并允许与滚动互动 各个对象之间传播和共享的增量。
Compose 提供了多种方法来处理可组合项之间的嵌套滚动。 嵌套滚动的一个典型示例是另一个列表中的列表, 复杂情况是 工具栏。
自动嵌套滚动
简单的嵌套滚动无需您执行任何操作。启动滚动操作的手势会自动从子级传播到父级,这样一来,当子级无法进一步滚动时,手势就会由其父元素处理。
某些 Google Cloud 产品支持自动嵌套滚动,
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
修饰符
如果您需要在多个元素之间创建高级协调滚动,
该
nestedScroll
修饰符定义嵌套滚动层次结构,为您提供更大的灵活性。如
某些组件具有内置的嵌套滚动
联系。不过,对于不可自动滚动的可组合项,例如
Box
或 Column
时,此类组件上的滚动增量不会在
嵌套滚动系统后,增量不会到达 NestedScrollConnection
,也不会
父级组件。如需解决此问题,您可以使用 nestedScroll
授予
对其他组件(包括自定义组件)的支持。
嵌套滚动循环
嵌套滚动周期是上下分派的滚动增量流
通过作为嵌套结构的一部分(或节点)的所有组件(或节点)
例如使用可滚动的组件和修饰符,或
nestedScroll
。
嵌套滚动循环的阶段
当可滚动元素检测到触发事件(例如手势)时 组件之前,甚至在实际滚动操作被触发之前, 系统会将增量值发送到嵌套滚动系统并经历三个阶段: 滚动前、节点消耗和后滚动
在第一个预滚动阶段,接收触发器事件的组件 增量会将这些事件通过层次结构树向上分派到最顶层 。然后,增量事件会以气泡形式显示,也就是说,增量事件 从最根根的父元素向下传播到启动 嵌套滚动循环。
这会提供嵌套滚动父级(使用 nestedScroll
或
可滚动修饰符)使用
节点本身就可以使用它
在节点使用阶段,节点本身会使用 由其父级使用这是滚动移动真正完成时 可见。
在此阶段,子级可以选择消耗全部或部分剩余流量 滚动。剩下的所有内容都将被重新发送回滚动后阶段。
最后,在滚动后阶段,节点本身没有消耗任何内容 将被再次发送到其祖先实体以供使用。
滚动后阶段的运作方式与滚动前阶段类似 的父母可以选择或不购买。
与滚动类似,当拖动手势完成时,用户的意图可能是 会转换成用于快速滑动(使用动画滚动) 可滚动容器。快速滑动也是嵌套滚动循环的一部分,并且 拖动事件生成的速度也会经历类似的阶段:预快、 节点消耗和滑动后等因素请注意,投掷动画仅与 且不会被其他事件(例如 a11y 或 硬件滚动。
参与嵌套滚动循环
参与该循环意味着拦截、使用和报告 层次结构中的增量使用。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
了解更多用法
嵌套滚动互操作性
当您尝试将可滚动的 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 中,您需要设置子级可组合项和
必需
NestedScrollConnection
:
open class MainActivity : ComponentActivity() { @OptIn(ExperimentalComposeUiApi::class) 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(
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(android.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
,因此滚动此类组件的增量不会
传播到嵌套滚动系统中,并且增量不会到达
NestedScrollConnection
,由rememberNestedScrollInteropConnection()
提供,
因此这些增量不会到达父级 View
组件。要解决此问题,
请务必将可滚动修饰符设置为这些类型的
可组合项。您可以参阅上文关于嵌套
可滚动查看更详细的说明,
信息。
非协作式父级 View
,包含子级 ComposeView
非协作式 View 无法在 View
端实现必要的 NestedScrolling
接口。请注意,这意味着这类 Views
不能直接支持嵌套滚动互操作性。非协作式 Views
是 RecyclerView
和 ViewPager2
。
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 了解手势
- 将
CoordinatorLayout
迁移到 Compose - 在 Compose 中使用 View