滚动

滚动修饰符

verticalScrollhorizontalScroll 修饰符提供一种最简单的方法,可让用户在元素内容边界大于最大尺寸约束时滚动元素。利用 verticalScrollhorizontalScroll 修饰符,您无需转换或偏移内容。

@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 的组件和修饰符: verticalScrollhorizontalScroll, 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 修饰符定义嵌套滚动层次结构,为您提供更大的灵活性。如 某些组件具有内置的嵌套滚动 联系。不过,对于不可自动滚动的可组合项,例如 BoxColumn 时,此类组件上的滚动增量不会在 嵌套滚动系统后,增量不会到达 NestedScrollConnection,也不会 父级组件。如需解决此问题,您可以使用 nestedScroll 授予 对其他组件(包括自定义组件)的支持。

嵌套滚动循环

嵌套滚动周期是上下分派的滚动增量流 通过作为嵌套结构的一部分(或节点)的所有组件(或节点) 例如使用可滚动的组件和修饰符,或 nestedScroll

嵌套滚动循环的阶段

当可滚动元素检测到触发事件(例如手势)时 组件之前,甚至在实际滚动操作被触发之前, 系统会将增量值发送到嵌套滚动系统并经历三个阶段: 滚动前、节点消耗和后滚动

嵌套滚动的阶段
周期

在第一个预滚动阶段,接收触发器事件的组件 增量会将这些事件通过层次结构树向上分派到最顶层 。然后,增量事件会以气泡形式显示,也就是说,增量事件 从最根根的父元素向下传播到启动 嵌套滚动循环。

滚动前阶段 - 调度
向上

这会提供嵌套滚动父级(使用 nestedScroll 或 可滚动修饰符)使用 节点本身就可以使用它

滚动前阶段 - 弹出提示
向下

在节点使用阶段,节点本身会使用 由其父级使用这是滚动移动真正完成时 可见。

节点消耗
阶段

在此阶段,子级可以选择消耗全部或部分剩余流量 滚动。剩下的所有内容都将被重新发送回滚动后阶段。

最后,在滚动后阶段,节点本身没有消耗任何内容 将被再次发送到其祖先实体以供使用。

滚动后阶段 - 调度
向上

滚动后阶段的运作方式与滚动前阶段类似 的父母可以选择或不购买。

滚动后阶段 - 弹出提示
向下

与滚动类似,当拖动手势完成时,用户的意图可能是 会转换成用于快速滑动(使用动画滚动) 可滚动容器。快速滑动也是嵌套滚动循环的一部分,并且 拖动事件生成的速度也会经历类似的阶段:预快、 节点消耗和滑动后等因素请注意,投掷动画仅与 且不会被其他事件(例如 a11y 或 硬件滚动。

参与嵌套滚动循环

参与该循环意味着拦截、使用和报告 层次结构中的增量使用。Compose 提供了一系列工具 影响嵌套滚动系统的工作方式以及如何直接交互 例如,当您需要对滚动增量执行某些操作时 可滚动的组件甚至会开始滚动

如果嵌套滚动循环是一个对节点链执行操作的系统, nestedScroll 修饰符可以拦截并插入到这些更改中, 影响在链中传播的数据(滚动增量)。这个 修饰符可以放在层次结构的任何位置,它会与 嵌套的滚动修饰符实例位于树中,以便通过 这个频道此修饰符的构建块是 NestedScrollConnectionNestedScrollDispatcher

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 良好地配合工作。然而,当使用 ViewPager2RecyclerView 完成互操作性滚动时,由于不会实现 NestedScrollingParent3,因此无法做到由子项到父项的连续滚动。

如要在可滚动的 View 元素与可滚动的可组合项之间实现双向嵌套的嵌套滚动互操作 API,您可以在下列场景中使用嵌套滚动互操作 API 来缓解这些问题。

包含子级 ComposeView 的协作式父级 View

协作式父级 View 已实现 NestedScrollingParent3 从而能够从 可组合子项。在这种情况下,ComposeView 将充当子元素, 需要(间接)实施 NestedScrollingChild3。 合作型父级的一个例子是 androidx.coordinatorlayout.widget.CoordinatorLayout

如果您需要在可滚动的 View 父级容器与嵌套的可滚动子级可组合项之间实现嵌套滚动互操作性,可以使用 rememberNestedScrollInteropConnection()

rememberNestedScrollInteropConnection() 会允许并记住 NestedScrollConnection,后者支持在实现 NestedScrollingParent3View 父项和 Compose 子项之间实现嵌套滚动互操作性。此方法应与 nestedScroll 修饰符结合使用。由于嵌套滚动在 Compose 端默认处于启用状态,因此您需要 可以使用此连接在 View 端启用嵌套滚动,并添加 Views 和可组合项之间必要的粘合逻辑。

一个常见的用例是使用 CoordinatorLayoutCollapsingToolbarLayout 和 可组合子项,如下例所示:

<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() {
    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 级别。这样一来, 参与嵌套滚动的元素,但未启用 自动滚动元素。不可滚动的可组合项 例如 BoxColumn,因此滚动此类组件的增量不会 传播到嵌套滚动系统中,并且增量不会到达 NestedScrollConnection,由rememberNestedScrollInteropConnection()提供, 因此这些增量不会到达父级 View 组件。要解决此问题, 请务必将可滚动修饰符设置为这些类型的 可组合项。您可以参阅上文关于嵌套 可滚动查看更详细的说明, 信息。

非协作式父级 View,包含子级 ComposeView

非协作式 View 无法在 View 端实现必要的 NestedScrolling 接口。请注意,这意味着这类 Views 不能直接支持嵌套滚动互操作性。非协作式 ViewsRecyclerViewViewPager2