18 Scroll 滚动容器与滑动优化:从单页到多段滚动
18 Scroll 滚动容器与滑动优化:从单页到多段滚动
前言

图:18 Scroll 滚动容器与滑动优化:从单页到多段滚动 运行效果截图(HarmonyOS NEXT)
在移动应用开发中,滚动容器是页面内容过多时的标准解决方案。ArkUI 的 Scroll 组件提供了功能完善的滚动能力——支持垂直/水平滚动、滚动条控制、边缘回弹、嵌套滚动协调等。
本文以"鹿鹿·笔迹心理分析"项目中两个核心页面——HomePage 首页(多段水平+垂直混合滚动)和 ReportDetailPage 报告详情页(长内容垂直滚动)——为例,深入解析 Scroll 容器的使用场景和优化策略。
鸿蒙官方·Scroll 组件:developer.huawei.com
项目源码仓库:harmony-app GitHub

图:垂直 Scroll 嵌套水平 Scroll——鸿蒙滚动容器的典型模式,两个滚动方向天然兼容
一、Scroll 基础
1.1 Scroll 的核心属性
Scroll 是一个容器组件,使其内容变得可滚动:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
scrollable | ScrollDirection | Vertical | 滚动方向 |
scrollBar | BarState | Auto | 滚动条显示策略 |
edgeEffect | EdgeEffect | EdgeEffect.Spring | 边缘效果 |
enableScroll | boolean | true | 是否启用滚动 |
// 垂直滚动(最常用)
Scroll() {
Column() {
// ... 长内容
}
}
// 水平滚动
Scroll(.horizontal) {
Row() {
// ... 水平排列的内容
}
}
// 关闭边缘回弹
Scroll() {
// ...
}
.edgeEffect(EdgeEffect.None)
1.2 项目中的 Scroll 使用概览
| 页面 | 滚动方向 | 滚动内容 | Scroll 数量 | 特性 |
|---|---|---|---|---|
| [HomePage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/HomePage.ets) | 垂直 + 水平 | 全页 + 最近报告列表 | 2 个(嵌套) | 嵌套滚动 |
| [ReportDetailPage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/ReportDetailPage.ets) | 垂直 | 4 张卡片 + 按钮 | 1 个 | 长内容滚动 |
| [DuetReportPage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/DuetReportPage.ets) | 垂直 | 全页报告内容 | 1 个 | 嵌套 Canvas |
| [WritePage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/WritePage.ets) | 垂直 | 手写板 + 工具栏 | 1 个 | 适老化 |
二、垂直滚动:长页面的标准方案
2.1 ReportDetailPage 的 Scroll 结构
[ReportDetailPage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/ReportDetailPage.ets) 使用单层 Scroll + Column 实现完整的报告详情滚动:
build() {
Scroll() { // ① 外层可滚动容器
Column() { // ② 内部垂直排列
// 状态栏
Row() { Text(时间); Blank(); Row(📶 100%) }
// 导航栏
Row() { Text('←'); Blank(); Text('笔 迹 解 读'); Blank(); Text('⋯') }
// ③ 来源卡片
Row() { Stack() + Column() + Text('✎') }
.opacity(this.srcOpacity)
.translate({ y: this.srcOffset })
// ④ 情绪卡片
Column() { Row() + Stack() + Text() }
.opacity(this.moodOpacity)
.translate({ y: this.moodOffset })
// ⑤ 雷达卡片
Column() { Text() + HandwritingRadar + Flex(tags) }
.opacity(this.radarOpacity)
.translate({ y: this.radarOffset })
// ⑥ AI 解读卡片
Column() { Text() + Text(summary) + Row(buttons) }
.opacity(this.aiOpacity)
.translate({ y: this.aiOffset })
}
.width('100%')
.padding({ left: AppSpacing.L, right: AppSpacing.L }) // 左右留白
.backgroundColor(AppColors.BG)
}
.width('100%')
.height('100%')
}
页面高度计算: Scroll 的高度由父容器决定(height: '100%'),内部 Column 的高度由所有子组件的总和决定。当 Column 高度 > Scroll 高度时,Scroll 自动启用滚动。
2.2 Scroll 与入场动画的配合
Scroll 内部的入场动画状态需要与滚动行为兼容:
// 每张卡片独立控制入场动画状态
@State srcOpacity: number = 0 // 来源卡片透明度
@State srcOffset: number = 20 // 来源卡片 Y 偏移
@State moodOpacity: number = 0 // 情绪卡片透明度
@State moodOffset: number = 20 // 情绪卡片 Y 偏移
@State radarOpacity: number = 0 // 雷达卡片透明度
@State radarOffset: number = 20 // 雷达卡片 Y 偏移
@State aiOpacity: number = 0 // AI 卡片透明度
@State aiOffset: number = 20 // AI 卡片 Y 偏移
// 入场动画链(5 段串行)
private startEntranceAnimation(): void {
animateTo({}, () => { this.srcOpacity = 1; this.srcOffset = 0 })
animateTo({}, () => { this.moodOpacity = 1; this.moodOffset = 0 })
animateTo({}, () => { this.radarOpacity = 1; this.radarOffset = 0 })
animateTo({}, () => { this.aiOpacity = 1; this.aiOffset = 0 })
animateTo({}, () => { this.btnOpacity = 1 })
}
注意:Scroll 内部元素的
translate动画对滚动行为没有影响——滚动时 Scroll 滚动的是内部的整个 Column,而非单个卡片。
三、水平滚动:横向滚动的实现
3.1 HomePage 的嵌套滚动
[HomePage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/HomePage.ets) 实现了外层垂直滚动 + 内层水平滚动的嵌套模式:
build() {
Scroll() { // ① 外层:垂直滚动
Column() {
// 欢迎区 + Hero + 子入口(垂直排列)
Column() { ... }
// ② 内层:水平滚动(最近报告)
Column() {
Row() {
Text('最近分析')
Blank()
Text('查看全部 →').fontSize(12).fontColor(AppColors.PRIMARY)
}
.width('100%')
// 水平 Scroll
Scroll(.horizontal) {
Row({ space: 12 }) {
ForEach(this.recentItems, (item: RecentItem, index: number) => {
this.RecentSwipeCard(item, index)
}, (item: RecentItem) => item.reportId.toString())
}
.padding({ left: AppSpacing.L, right: AppSpacing.L })
}
.scrollBar(BarState.Off) // 隐藏滚动条
.scrollable(ScrollDirection.Horizontal)
.edgeEffect(EdgeEffect.Spring)
}
// 灵感卡片
Column() { ... }
}
}
}
3.2 水平 Scroll 的关键参数
Scroll(.horizontal) // 设置水平方向
.scrollBar(BarState.Off) // 隐藏滚动条(UI 更简洁)
.scrollable(ScrollDirection.Horizontal) // 明确滚动方向
.edgeEffect(EdgeEffect.Spring) // 边缘回弹(默认)
水平 Scroll 的布局约束:
- Scroll 宽度固定(父容器宽度)
- 内部 Row 宽度 = 所有子卡片宽度总和 + spacing
- Row 宽度超出 Scroll 宽度时,Scroll 启用水平滚动
- 每个卡片宽度固定(如 120vp),一行放不下自动出现滚动
提示:水平 Scroll + Row 是最常见的横向滑动实现。也可以使用
Swiper组件实现整页横向翻页。
四、边缘回弹与滚动条
4.1 edgeEffect 边缘效果
edgeEffect 控制用户滚动到内容边界时的视觉效果:
| 模式 | 效果 | 适用场景 |
|---|---|---|
EdgeEffect.Spring | 弹簧回弹效果(默认) | 大多数滚动列表 |
EdgeEffect.None | 无效果,到达边界立即停止 | 精确分页内容 |
EdgeEffect.Fade | 边缘渐隐效果 | 图片浏览、画廊 |
// 默认弹簧回弹
Scroll() { ... }
.edgeEffect(EdgeEffect.Spring) // 滚到底时"弹一下"
// 关闭回弹
Scroll() { ... }
.edgeEffect(EdgeEffect.None) // 到边界立即停止
4.2 scrollBar 滚动条控制
// 自动显示(默认)— 滚动时显示,闲置后隐藏
Scroll() { ... }
.scrollBar(BarState.Auto)
// 始终隐藏
Scroll() { ... }
.scrollBar(BarState.Off)
// 始终显示
Scroll() { ... }
.scrollBar(BarState.On)
项目中基于 UI 美观的权衡:
| 页面 | scrollBar | 理由 |
|---|---|---|
| HomePage 垂直 | 默认 Auto | 需要暗示用户可滚动 |
| HomePage 水平 | Off | 水平滚动条遮挡内容 |
| ReportDetailPage | 默认 Auto | 标准的页面滚动 |
| DuetReportPage | 默认 Auto | 长报告内容 |
五、嵌套滚动协调
5.1 母子 Scroll 的嵌套问题
当垂直 Scroll 内嵌套水平 Scroll 时,触摸事件分配逻辑如下:
- 用户垂直滑动 → 外层 Scroll 响应 → 页面上下滚动
- 用户在水平 Scroll 区域垂直滑动 → 仍然由外层 Scroll 响应
- 用户在水平 Scroll 区域水平滑动 → 内层 Scroll 响应 → 横向滑动
// 嵌套 Scroll 的标准模式
Scroll() { // 母 Scroll(垂直)
Column() {
// 固定的普通内容
Text('固定的内容')
// 内层水平滚动
Scroll(.horizontal) { // 子 Scroll(水平)
Row() {
ForEach(this.items, (item) => {
Card(item)
})
}
}
// 更多内容
Text('底部内容')
}
}
5.2 嵌套滚动的注意事项
问题一:内层 Scroll 高度不固定
// ❌ 内层 Scroll 未指定高度
Scroll(.horizontal) {
Row() {
// 卡片列表
}
}
// 此时内层 Scroll 高度为 0,不会显示
// ✅ 内层 Scroll 需要指定高度
Scroll(.horizontal) {
Row() {
ForEach(this.recentItems, (item) => {
this.RecentSwipeCard(item, index)
})
}
.height(180) // 卡片区域固定高度
}
问题二:触摸冲突
// 如果子 Scroll 的滚动方向与父 Scroll 一致,会出现冲突
Scroll() { // 垂直
Column() {
Scroll() { // 也垂直 — 冲突!
Column() {
// ...
}
}
}
}
// ✅ 解决方案:内层 Scroll 使用不同方向(如水平)
// 或使用 scrollable 限制子 Scroll 的方向
5.3 使用 NestedScroll 协调
对于更复杂的嵌套场景,可以使用 nestedScroll 属性:
Scroll() {
// ...
}
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST, // 父容器优先
scrollBackward: NestedScrollMode.SELF_FIRST // 自身优先
})
六、Scroll 的性能优化
6.1 避免过度嵌套
Scroll 内部的 UI 组件应该尽量扁平化:
// ❌ 过度嵌套:每层 Column 都增加了布局计算量
Scroll() {
Column() {
Row() {
Column() {
Text('内容')
}
}
}
}
// ✅ 扁平化
Scroll() {
Column() {
Text('内容')
}
}
6.2 使用 column 替代多层嵌套
// ✅ 推荐:单层 Column + 多个 Child
Scroll() {
Column() {
Header()
HeroSection()
RecentSection() // 内部可以再嵌套水平 Scroll
InspireSection()
TabBar()
}
}
6.3 合理使用懒加载
对于超长列表(如 ArchivePage 的历史档案),结合 LazyForEach 可以大幅提升首屏加载性能:
Scroll() {
Column() {
LazyForEach(new ArchiveDataSource(items), (item: ArchiveItem) => {
ArchiveCard({ item })
}, (item: ArchiveItem) => item.id.toString())
}
}
七、Scroll 与底部安全区域
7.1 安全区域适配
在鸿蒙手机上,底部导航栏区域需要额外留白:
Scroll() {
Column() {
// ... 所有内容
}
.padding({ bottom: 24 }) // 底部安全区域留白
}
7.2 底部内容可见性
如果底部有操作按钮,需要确保其不被 Scroll 裁剪:
Scroll() {
Column() {
// ... 卡片内容
// 底部操作按钮
Row() {
Button('生成更多分析').width('45%')
Button('保存报告').width('45%')
}
.padding({ top: 16, bottom: 32 }) // 底部充足留白
}
}
八、Scroll 高级用法
8.1 滚动到指定位置
通过 scrollTo 方法可以编程控制滚动位置:
// 声明 ScrollController
private scroller: Scroller = new Scroller()
build() {
Scroll(this.scroller) { // 传入 controller
Column() {
// ...
}
}
}
// 滚动到顶部
private scrollToTop(): void {
this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: { duration: 300 } })
}
// 滚动到指定子组件
private scrollToCard(index: number): void {
this.scroller.scrollTo({ xOffset: 0, yOffset: index * cardHeight, animation: { duration: 300 } })
}
8.2 滚动事件监听
Scroll() {
Column() { ... }
}
.onScroll((x: number, y: number) => {
hilog.info(0x0000, 'Scroll', '滚动位置: (%{public}d, %{public}d)', x, y)
})
.onScrollStart(() => {
hilog.info(0x0000, 'Scroll', '开始滚动')
})
.onScrollStop(() => {
hilog.info(0x0000, 'Scroll', '停止滚动')
})
8.3 滚动方向判断
@State lastScrollY: number = 0
@State isScrollingUp: boolean = false
.onScroll((_: number, y: number) => {
this.isScrollingUp = y < this.lastScrollY
this.lastScrollY = y
if (this.isScrollingUp) {
// 上滑 — 显示导航栏
} else {
// 下滑 — 隐藏导航栏
}
})
总结
本文从 Scroll 组件的基础属性出发,通过项目中两个核心页面的实战代码,全面解析了 ArkUI 滚动容器的使用方法:
- 垂直滚动:ReportDetailPage 的长内容页面,Scroll + Column 标准模式
- 水平滚动:HomePage 的最近报告横向滑动,Scroll + Row + ForEach
- 嵌套滚动:垂直 Scroll 内嵌水平 Scroll,天然兼容互不干扰
- 边缘回弹:默认 Spring 模式,到达边界回弹提示用户
- 滚动条控制:水平 Scroll 通常隐藏滚动条,垂直 Scroll 保持 Auto
Scroll 是实现流畅用户体验的基础组件,合理使用可以大幅提升长内容页面的可浏览性。
下一篇文章将进入第四模块:自定义组件与组件化设计,从 HmTitleBar 开始讲解组件复用的最佳实践。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- Scroll 组件参考
- Scroller 滚动控制器
- NestedScroll 嵌套滚动
- 边缘回弹 EdgeEffect
- [HomePage 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/HomePage.ets)
- [ReportDetailPage 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/ReportDetailPage.ets)
- [DuetReportPage 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/DuetReportPage.ets)
- ArkUI 布局开发指南
- HarmonyOS 官方示例
更多推荐
所有评论(0)