滚动容器怎么选 · AndroidX 源码指南
AAndroidX 源码指南
Compose Foundation
Compose 底层 · androidx.compose.foundation

滚动容器怎么选1.13.0-alpha01

区分 verticalScroll、LazyColumn、scrollable 与 nestedScroll。

最后更新 2026-08-01

内容规模决定容器

// 短内容:整体滚动,子项全部组合
Column(
    Modifier.verticalScroll(rememberScrollState())
) {
    settings.forEach { SettingRow(it) }
}

// 长内容:视口驱动,按需组合
LazyColumn {
    items(messages) { MessageRow(it) }
}

选择规则

  • 短表单/设置页(几十项以内)→ verticalScroll
  • 长列表/动态数据 → LazyColumn
  • 网格 → LazyVerticalGrid
  • 横向 → 对应 horizontalScroll / LazyRow

四层抽象

API负责什么
verticalScroll / horizontalScroll内容测量、位移、手势、语义、滚动状态
Lazy 列表/网格视口驱动的按需内容(只组合可见项)
scrollable消费滚动增量,但不自动移动内容
nestedScroll父子滚动前后分发与 fling 协调

关键区别scrollable 只是”告诉你滚了多少”,不会移动任何内容——自定义控件加它必须自己根据 state 改变布局/绘制。verticalScroll 是”帮你滚”。

ScrollState 与 LazyListState

状态适用
ScrollStateverticalScroll/horizontalScroll
LazyListState / LazyGridStateLazy 容器(可访问组合中的项)

命令式滚动

ScrollState / LazyListState 都提供挂起的动画滚动 API,必须从协程调用:

val scope = rememberCoroutineScope()
val listState = rememberLazyListState()

// 回到顶部(动画)
scope.launch { listState.animateScrollToItem(0) }

// 立即跳转
scope.launch { listState.scrollToItem(50) }

// 滚动到具体 index(Lazy 专用)
scope.launch { listState.animateScrollToItem(index = itemIndex) }

响应式滚动(nestedScroll)

val nestedScrollConnection = remember {
    object : NestedScrollConnection {
        override suspend fun onPreFling(available: Velocity, state: ScrollState): Velocity {
            // 子滚动前先消费:返回已消费部分
            return Velocity.Zero
        }

        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource,
        ): Offset {
            // 子滚动后:处理剩余位移
            return Offset.Zero
        }
    }
}

Column(
    Modifier.nestedScroll(nestedScrollConnection)
) {
    // 子滚动容器
    LazyColumn { ... }
}

适合:AppBar 折叠、视差、无限加载触发器。

滚动位置与派生状态

val listState = rememberLazyListState()
val showTop by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

// 当前可见项
val firstVisible = listState.firstVisibleItemIndex

常见陷阱

  • 长列表用 verticalScroll:全部组合,性能崩。
  • scrollable 当滚动用:它不移动内容,需自己处理。
  • scrollTo 不在协程:非挂起调用会编译错;在 scope.launch 里。
  • nestedScroll 连接忘传:只写在子容器上不生效,父要接 connection。
  • 重建状态丢失:滚动位置用 rememberLazyListState(配合 rememberSaveable 保留跨配置)。

复制即用

@Composable
fun ScrollableList(messages: List<Message>) {
    val listState = rememberLazyListState()
    val scope = rememberCoroutineScope()

    Box {
        LazyColumn(state = listState) {
            items(messages, key = { it.id }) { MessageRow(it) }
        }

        val showTop by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 0 }
        }
        if (showTop) {
            FloatingActionButton(
                onClick = { scope.launch { listState.animateScrollToItem(0) } },
                modifier = Modifier.align(Alignment.BottomEnd),
            ) { Text("↑") }
        }
    }
}

要点

  • 短内容 verticalScroll,长内容 LazyColumn;scrollable 不自动移动内容。
  • 命令式滚动从协程调用 animateScrollToItem/scrollToItem。
  • nestedScroll 连接父滚动的分发包;fling 协调用 onPreFling。
  • 滚动位置派生状态用 derivedStateOf 避免重组。

相关页面