SDK 版本 :HarmonyOS NEXT 6.1.1(API 24)
核心组件 :Scroll · Column · ForEach · Scroller
布局范式:固定头 + 滚动体 + 固定尾(三明治结构)




一、引言
在移动端应用开发中,可滚动的纵向列表是最常见、最核心的交互模式之一。无论是社交媒体 feed、电商商品列表、系统设置页、新闻资讯流,还是聊天记录、操作日志,几乎所有内容型页面都依赖「滚动」来完成信息承载。
鸿蒙 ArkTS 框架为开发者提供了两套并行的滚动方案:
| 方案 | 适用场景 | 特点 |
|---|---|---|
| Scroll + Column | 内容类型单一、条目数量可控的页面 | 布局直观、控制精细、全量渲染 |
| List | 大量数据、无限列表、性能敏感 | 按需渲染(LazyForEach)、自带回收 |
本教程聚焦于 Scroll + Column 的组合方案。虽然 List 在超长列表场景下性能更优,但 Scroll + Column 的布局灵活性远超 List------它允许在滚动区域内自由混排卡片、图表、按钮、表单等异形组件,而不会受到 List 子项结构一致性的约束。
我们将从一个完整的实战项目出发,深入剖析 Scroll 组件的每一个配置项、Scroll 与 Column 之间的协作机制、Scroller 对象的编程式控制,以及状态驱动的滚动联动逻辑。读完本文,你将掌握在 HarmonyOS NEXT 上构建任意可滚动页面的能力。
二、布局架构概述
2.1 三明治布局模式
我们的示例页面采用了经典的三明治布局:
┌─────────────────────────────────────┐
│ 固定顶栏(标题 + 返回) │ ← 永不滚动
├─────────────────────────────────────┤
│ │
│ Scroll 滚动区域 │
│ ┌─ 原理说明卡 ──────────────────┐ │
│ │ 滚动方向 │ 位置 │ 总高度 │ │ ← 实时状态
│ └────────────────────────────────┘ │
│ ┌─ 滚动配置项演示 ──────────────┐ │
│ │ scrollable · edgeEffect ·... │ │ ← 配置说明
│ └────────────────────────────────┘ │
│ ┌─ 系统运行日志 ────────────────┐ │
│ │ 15+ 条日志 + 实时追加 │ │ ← 溢出触发滚动
│ └────────────────────────────────┘ │
│ ┌─ 数据刷新演示 ────────────────┐ │
│ │ 追加 + 刷新计数 │ │ ← 动态数据
│ └────────────────────────────────┘ │
│ ┌─ 使用注意事项 ────────────────┐ │
│ │ 6 条避坑要点 │ │
│ └────────────────────────────────┘ │
│ │
├─────────────────────────────────────┤
│ 固定底栏(滚动控制按钮) │ ← 永不滚动
└─────────────────────────────────────┘
2.2 为什么选择三明治结构?
这种布局模式的核心优势在于:
- 关键操作常驻:顶栏的标题、底栏的控制按钮不随内容滚动,用户在任何位置都能快速操作
- 内容区最大化:Scroll 区域通过 layoutWeight(1) 撑满剩余空间,动态适应不同屏幕尺寸
- 视觉层次清晰:固定层与滚动层的分离,让用户直觉地理解哪些内容可滚动、哪些不可滚动
2.3 组件层次与职责
Column(全屏骨架,固定)
├── Column(固定顶栏:标题) ← 第1层:固定
├── Divider(分割线)
├── Scroll(可滚动区域) ← 第2层:滚动
│ └── Column(内部容器) ← 第3层:内容
│ ├── Column(原理说明卡)
│ ├── Divider
│ ├── Column(配置项列表)
│ ├── Divider
│ ├── Column(日志列表)
│ ├── Divider
│ ├── Column(刷新演示)
│ ├── Divider
│ ├── Column(注意事项)
│ └── Column(底部占位)
├── Divider(分割线)
└── Column(固定底栏:按钮) ← 第4层:固定
每一层各司其职:最外层的 Column 提供全屏背景与主轴方向;Scroll 提供滚动机制;Scroll 内部的 Column 作为内容容器,自然撑高触发滚动;上下两个固定 Column 提供常驻 UI。
三、核心组件详解
3.1 Scroll 组件
Scroll 是 ArkTS 中用于包裹可滚动内容的容器组件。它的核心职责是:当子组件的内容尺寸超出自身尺寸时,通过滚动的方式让用户能够查看被隐藏的部分。
3.1.1 基本用法
typescript
// 最简形式 ------ 创建一个垂直滚动的容器
Scroll() {
Column() {
// 任意数量的子组件
Text('内容1')
Text('内容2')
// ... 更多内容
}
}
.width('100%')
.layoutWeight(1)
一定要记住的重要规则:
- Scroll 只能有一个直接子组件。这意味着你需要用一个 Column(或 Flex、Row 等布局容器)将所有内容包裹起来
- Scroll 内部的 Column 不要设置固定高度,否则 Scroll 无法正确计算滚动区域,内容溢出时不会滚动
- Scroll 自身必须有高度约束,否则它会自由扩展而不会产生滚动
3.1.2 全部 API 配置项
| API | 类型 | 默认值 | 说明 |
|---|---|---|---|
.scrollable() |
ScrollDirection | Vertical | 滚动方向:Vertical / Horizontal / Both / None |
.edgeEffect() |
EdgeEffect | Spring | 边缘效果:Spring 弹簧回弹 / Fade 渐变消失 |
.scrollBar() |
BarState | Auto | 滚动条状态:Auto 按需显隐 / On 始终显示 / Off 始终隐藏 |
.onScroll() |
callback | --- | 滚动中回调(已废弃,推荐 onDidScroll) |
.onDidScroll() |
callback | --- | 滚动中回调,返回当前 x/y 偏移量 |
.onScrollStart() |
callback | --- | 滚动开始时触发 |
.onScrollStop() |
callback | --- | 滚动停止时触发 |
.onScrollEdge() |
callback | --- | 滚动到边缘时触发 |
3.1.3 滚动方向控制
typescript
Scroll() {
// 纵向内容
}
.scrollable(ScrollDirection.Vertical) // 仅垂直滚动
ScrollDirection 枚举有 4 个值:
Vertical(默认):仅垂直方向滚动,最常用Horizontal:仅水平方向滚动,适用于横向轮播None:禁用滚动,内容溢出时裁剪Both:双向滚动,适用于地图或超大画布
使用建议 :绝大多数列表场景使用 Vertical;如果是轮播图或横向标签栏使用 Horizontal;Both 场景极少,仅在自定义画布等特殊需求时使用。
3.1.4 边缘效果(EdgeEffect)
typescript
Scroll() { ... }
.edgeEffect(EdgeEffect.Spring) // 弹簧回弹效果
EdgeEffect 有两个值:
- Spring(默认):当用户拉到内容边界之外时产生弹性反馈,松手后内容回弹到边界位置。这种效果给用户一种「摸得到边」的自然感觉,推荐用于大多数场景
- Fade:在内容末尾处产生渐变淡出效果,视觉上像是内容「隐入」边界
实际体验差异:Spring 效果更符合直觉,用户能感知到「已经到尽头了」;Fade 效果更平滑但反馈较弱。推荐首选 Spring。
3.1.5 滚动条控制
typescript
Scroll() { ... }
.scrollBar(BarState.Auto) // 滚动条自动显隐
BarState 有三个值:
- Auto(默认):内容可滚动时显示滚动条,静止 2 秒后自动隐藏;拖动时重新显示
- On:滚动条始终显示,占据 4dp 宽度
- Off:完全隐藏滚动条,用户仍可通过手势滚动
使用建议:大多数场景用 Auto 即可;如果需要类似 iOS 的「无干扰滚动体验」,可设置为 Off;On 适合数据表格等需要精确定位的场景。
3.1.6 滚动事件回调
Scroll 提供了三个重要的事件回调:
typescript
Scroll() { ... }
.onDidScroll((xOffset: number, yOffset: number) => {
// 【高频回调】滚动正在发生
// xOffset: 水平方向已滚动的像素值
// yOffset: 垂直方向已滚动的像素值(正值 = 向下)
// 作用:实时更新 UI,如显示/隐藏回到顶部按钮
if (yOffset > 200) {
// 显示回到顶部按钮
}
})
.onScrollStart(() => {
// 【一次】滚动开始时触发
// 作用:记录滚动开始状态
})
.onScrollStop(() => {
// 【一次】滚动停止时触发
// 作用:触发懒加载或数据埋点
})
性能注意 :onDidScroll 是高频回调(每帧调用多次),回调中应避免执行耗时操作或触发 UI 频繁刷新。如果需要更新 @State 变量,确保条件判断足够精细,不要每帧都 setState。
3.2 Scroller 对象
Scroller 是 Scroll 的「遥控器」,通过它可以在不依赖用户手势的情况下控制滚动位置。
3.2.1 创建 Scroller
typescript
// 方式1:最常用 ------ 在组件中声明
@State
scroller: Scroller = new Scroller();
// 方式2:全局共享 ------ 仅在极端特殊场景下使用
// 实际项目中绝大多数场景用方式1即可
3.2.2 关联到 Scroll
typescript
Scroll(this.scroller) { // 将 scroller 传入 Scroll 构造函数
// ...
}
3.2.3 编程式滚动 API
Scroller 提供三个核心方法:
scrollTo() ------ 滚动到指定位置
typescript
scroller.scrollTo({
xOffset: 0, // 水平目标位置(像素)
yOffset: 500, // 垂直目标位置(像素)
animation: { // 可选:动画参数
duration: 400, // 动画时长(毫秒)
curve: Curve.EaseInOut // 动画曲线
}
});
scrollEdge() ------ 滚动到边缘
typescript
// 滚动到底部
scroller.scrollEdge(Edge.Bottom);
// 滚动到顶部
scroller.scrollEdge(Edge.Top);
scrollPage() ------ 滚动一页(翻页效果)
typescript
// 向后翻一页(相当于 Page Down)
scroller.scrollPage({ next: true });
// 向前翻一页(相当于 Page Up)
scroller.scrollPage({ next: false });
3.2.4 使用示例
typescript
// 回到顶部(带动画)
scrollToTop() {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: { duration: 300, curve: Curve.EaseOut }
});
}
// 滚动到底部(直接跳转)
scrollToBottom() {
this.scroller.scrollEdge(Edge.Bottom);
}
3.3 Column 与 Scroll 的协作
这是本教程最核心的部分。Column 和 Scroll 的配合看似简单,但存在几个关键约束:
3.3.1 单向子组件约束
Scroll 只能有一个直接子组件。下面这两种写法是有区别的:
typescript
// ❌ 错误 ------ Scroll 有多个直接子组件
Scroll() {
Text('内容1'); // 第1个直接子组件
Text('内容2'); // 第2个直接子组件 ------ 编译报错
}
// ✅ 正确 ------ 用 Column 包裹所有内容
Scroll() {
Column() {
Text('内容1');
Text('内容2');
Text('内容3');
// 任意多个子组件
}
}
3.3.2 高度约束规则
Column 在 Scroll 内部时,高度计算规则与非 Scroll 环境中完全不同:
| 环境 | Column 高度行为 | 说明 |
|---|---|---|
| 非 Scroll 环境 | 被父容器约束 | Column 高度受父容器限制 |
| Scroll 环境 | 不被约束 | Column 自由伸展,由内容撑高 |
这意味着:
typescript
// ✅ Scroll 内部:Column 高度自适应
Scroll() {
Column() {
Text('长内容...') // 100dp
Text('长内容...') // 100dp
// ... 总高度 2000dp
}
// ❌ 不要设置.height('100%') 或固定高度
// 否则 Scroll 无法计算滚动区域
}
// ✅ 正确做法:不给 Column 设置高度
// Scroll 自动测量 Column 的实际内容高度
3.3.3 layoutWeight(1) 的妙用
Scroll 自身必须有高度约束才能形成「滚动容器 vs 内容」的高度差。最常见的做法是在三明治布局中使用 layoutWeight(1):
typescript
Column() {
// 固定顶栏:自动高度
Column() { /* 标题 */ }
.height(48)
// 滚动区域:撑满剩余空间
Scroll() {
Column() { /* 内容 */ }
}
.layoutWeight(1) // ← 关键!占据顶栏和底栏之外的所有空间
.width('100%')
// 固定底栏:自动高度
Column() { /* 按钮 */ }
.height(56)
}
.width('100%')
.height('100%')
layoutWeight(1) 的语义是:在父容器的剩余空间中按权重分配高度。由于顶栏和底栏都是自动高度(由内容撑起),Scroll 拿到了「除了顶栏和底栏之外的所有空间」,从而形成了有界滚动的条件。
3.4 ForEach 在 Scroll 中的应用
在 Scroll 内部的 Column 中,ForEach 是最常用的列表渲染工具。
3.4.1 基本语法
typescript
ForEach(
this.dataArray, // 数据源数组
(item: ItemType, index?: number) => { // 渲染函数
// 返回 UI 组件树
Text(item.name);
},
(item: ItemType) => item.id.toString() // 可选:键值生成器
)
3.4.2 键值生成器的重要性
第三个参数是键值生成器(key generator),它告诉框架如何唯一标识列表中的每一项:
typescript
// 不传键值生成器 ------ 位置绑定(默认使用索引)
ForEach(this.logList, (log: LogEntry) => {
Row() { /* 日志条目 UI */ }
})
// 问题:当列表增删时,所有项都会重新渲染
// ✅ 传键值生成器 ------ 标识绑定
ForEach(this.logList, (log: LogEntry) => {
Row() { /* 日志条目 UI */ }
}, (log: LogEntry) => log.id.toString())
// 优点:只有变化的数据项会重新渲染
3.4.3 ForEach vs LazyForEach 的选择
| 特性 | ForEach | LazyForEach |
|---|---|---|
| 渲染策略 | 全量渲染 | 按需渲染(虚拟列表) |
| 数据量限制 | <200 条 | 上万条 |
| 使用复杂度 | 简单 | 需要实现 DataSource |
| 适用场景 | 博客页、设置页、详情页 | 聊天记录、商品列表、Feed 流 |
经验法则:如果数据条数不会超过 200(比如日志预览、设置项、配置列表),用 ForEach 完全足够。如果可能上千上万(聊天记录、无限滚动列表),必须用 LazyForEach + List。
四、实战代码深度解析
4.1 数据模型定义
typescript
interface LogEntry {
id: number;
level: 'INFO' | 'WARN' | 'ERROR';
message: string;
time: string;
}
数据模型的设计遵循以下原则:
id:唯一标识,作为 ForEach 的键值,确保列表增量更新的性能level:联合类型枚举,用于条件渲染不同颜色的标签message:可变长度的文本内容time:格式化的时间戳
4.2 状态变量设计
typescript
struct ScrollExamplePage {
// 滚动状态
@State isScrolled: boolean = false; // 是否滚动超过阈值
@State scrollPosition: number = 0; // 当前滚动位置
@State scrollDirection: string = '---'; // 滚动方向指示
// 数据状态
@State isRefreshing: boolean = false; // 刷新动画
@State refreshCount: number = 0; // 刷新次数
@State logList: LogEntry[] = [...]; // 日志数据
// 控制器
scroller: Scroller = new Scroller();
}
状态变量分为三类:
- UI 状态 (
isScrolled,scrollPosition,scrollDirection):驱动界面元素的显隐和样式变化 - 业务状态 (
isRefreshing,refreshCount,logList):表示数据逻辑的状态 - 控制器 (
scroller):不驱动 UI 渲染,只提供方法调用
@State 装饰器使得 UI 与数据自动联动------当任一 @State 变量变化时,框架自动重新渲染依赖该变量的组件。
4.3 三明治骨架构建
typescript
build() {
// 第1层:全屏 Column 骨架
Column() {
// ── 第2层:固定顶栏 ──
Column() {
Row() {
Text('‹')
.fontSize(24).fontColor('#007AFF')
Text('Column + Scroll 联动')
.fontSize(18).fontWeight(FontWeight.Bold)
.layoutWeight(1).textAlign(TextAlign.Center)
}
.width('100%').height(48)
}
.width('100%').backgroundColor('#FFFFFF')
Divider().height(1).color('#EEEEEE')
// ── 第3层:Scroll 核心区域 ──
Scroll(this.scroller) {
Column() {
// ... 所有内容块
}
.width('100%')
}
.layoutWeight(1) // ← 撑满顶栏和底栏之间的空间
.width('100%')
.scrollable(ScrollDirection.Vertical)
.edgeEffect(EdgeEffect.Spring)
.scrollBar(BarState.Auto)
Divider().height(1).color('#EEEEEE')
// ── 第4层:固定底栏 ──
Column() {
Row() {
Button('⬆ 回到顶部')
Button('⬇ 滚动到底部')
}
}
.width('100%').backgroundColor('#FFFFFF')
}
.width('100%').height('100%')
}
4.4 滚动事件驱动 UI
这是让页面「活」起来的关键------通过监听滚动事件实时更新 UI:
typescript
Scroll() { ... }
.onDidScroll((xOffset: number, yOffset: number) => {
// 1. 实时记录滚动位置
this.scrollPosition = yOffset;
// 2. 根据滚动位置控制按钮状态
if (yOffset > 800) {
this.isScrolled = true; // 激活「回到顶部」按钮
} else {
this.isScrolled = false; // 禁用「回到顶部」按钮
}
// 3. 显示滚动方向
if (yOffset > 0) {
this.scrollDirection = '↓';
} else if (yOffset < 0) {
this.scrollDirection = '↑';
}
})
这里的闭包捕获了 this 上下文,每次回调修改 @State 变量:
scrollPosition变化 → 顶部的「滚动位置:XXXpx」文字实时刷新isScrolled变化 → 底部按钮的背景色在灰色和蓝色之间切换scrollDirection变化 → 顶部的方向指示器和颜色变化
4.5 编程式滚动控制
typescript
// 回到顶部(带动画)
Button('⬆ 回到顶部')
.backgroundColor(this.isScrolled ? '#007AFF' : '#E0E0E0')
.onClick(() => {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: {
duration: 400,
curve: Curve.EaseInOut
}
});
this.isScrolled = false;
})
// 滚动到底部(直接跳转)
Button('⬇ 滚动到底部')
.backgroundColor('#4ECDC4')
.onClick(() => {
this.scroller.scrollEdge(Edge.Bottom);
})
两个按钮采用不同的滚动策略:
- 回到顶部用
scrollTo()配合动画,视觉上平滑过渡 - 滚动到底部用
scrollEdge(Edge.Bottom),因为底部位置随内容变化而不确定
4.6 动态数据追加
typescript
// 追加一条随机日志
appendLog(): void {
// 1. 随机选择日志级别
const level = levels[Math.floor(Math.random() * 3)];
// 2. 根据级别选择消息文本
const msgs = level === 'INFO' ? infoMsgs :
level === 'WARN' ? warnMsgs : errMsgs;
const msg = msgs[Math.floor(Math.random() * msgs.length)];
// 3. 生成新 ID
const lastId = this.logList[this.logList.length - 1].id;
// 4. 格式化时间
const time = `${hh}:${mm}:${ss}`;
// 5. 追加到状态数组(驱动 UI 自动更新)
this.logList = [...this.logList, {
id: lastId + 1,
level,
message: msg,
time
}];
}
注意这里使用展开运算符 [...this.logList, newItem] 创建新数组,而不是 push()。这是因为 ArkTS 的状态管理依赖引用变化来触发重渲染------只有数组引用发生变化,ForEach 才会重新执行。
4.7 模拟刷新加载
typescript
appendLogs(count: number): void {
this.isRefreshing = true; // 显示加载状态
setTimeout(() => { // 模拟 800ms 网络延迟
for (let i = 0; i < count; i++) {
this.appendLog();
}
this.refreshCount++;
this.isRefreshing = false; // 隐藏加载状态
}, 800);
}
这段代码模拟了真实项目中的「刷新加载」流程:
- 用户点击「追加 3 条」按钮
isRefreshing = true→ UI 从「✅ 已刷新 X 次」切换为「⏳ 刷新中...」- 800ms 后数据追加完成
isRefreshing = false→ UI 恢复为「✅ 已刷新 X+1 次」
这种「状态驱动视觉变化」的编程模式是 ArkTS 声明式 UI 的核心思想------你不需要手动操作 DOM,只需要改变数据状态。
五、关键避坑指南
5.1 Scroll 只能有一个直接子组件
这是新手最容易犯的错误。记住:Scroll 下的所有内容必须被一个容器组件包裹。
typescript
// ❌ 编译错误:不能有多个直接子组件
Scroll() {
Column() { /* 区域 A */ }
Column() { /* 区域 B */ } // ← 多了一个直接子组件
}
// ✅ 正确:用一个 Column 包裹
Scroll() {
Column() {
Column() { /* 区域 A */ }
Column() { /* 区域 B */ }
}
}
5.2 Scroll 内部不要使用固定高度
typescript
// ❌ 错误:内部 Column 设了高度
Scroll() {
Column()
.height('100%') // 这样 Scroll 认为内容已经 100% 填满,不会滚动
}
// ✅ 正确:让 Column 自由扩展
Scroll() {
Column() // 不设高度,由内容撑起
}
5.3 Scroll 自身必须有高度约束
typescript
// ❌ 错误:Scroll 没有高度约束
Column() {
Text('标题')
Scroll() {
// 内容 2000dp
}
// Scroll 在这里高度也是自动扩展,内容不会溢出,不会滚动
}
// ✅ 正确:用 layoutWeight 撑满
Column().height('100%') {
Text('标题').height(48)
Scroll() {
// 内容 2000dp
}
.layoutWeight(1) // 占据剩余所有空间
}
5.4 避免 Scroll 嵌套 Scroll
两个滚动手势系统会冲突,导致滚动体验极差:
typescript
// ❌ 错误:Scroll 嵌套 Scroll
Scroll() {
Column() {
Scroll() { // ← 内部还有 Scroll!手势冲突
Column() { /* ... */ }
}
}
}
// ✅ 正确:展平嵌套,一个 Scroll 管所有
Scroll() {
Column() {
// 所有内容直接放这里
}
}
5.5 ForEach 键值绑定
不传键值生成器时,ForEach 使用位置绑定(即 index 作为 key)。这会导致数据变化时不必要的全量重渲染:
typescript
// ❌ 低效:每次追加日志所有条目重渲染
ForEach(this.logList, (log: LogEntry) => {
ItemComponent({ item: log })
})
// ✅ 高效:通过 id 绑定,仅新条目渲染
ForEach(this.logList, (log: LogEntry) => {
ItemComponent({ item: log })
}, (log: LogEntry) => log.id.toString())
5.6 大量数据用 LazyForEach
当列表数据可能超过 200 条时,ForEach 的全量渲染策略会导致帧率下降:
typescript
// 数据量 < 200 条:ForEach 可用
ForEach(this.logList, ...)
// 数据量 > 200 条:请使用 List + LazyForEach
// List 自带视图回收机制
List() {
LazyForEach(this.dataSource, ...)
}
六、进阶技巧
6.1 滚动到指定元素
有时需要滚动到列表中的某个特定元素位置。可以通过 getBoundingClientRect + scrollTo 实现:
typescript
@State targetId: number = 0;
scroller: Scroller = new Scroller();
// 在 ForEach 中(使用 id 做键值)
ForEach(this.logList, (log: LogEntry, index?: number) => {
Row()
.id(`log_${log.id}`)
.onClick(() => {
// 点击跳转到下一个错误日志
const nextError = this.logList.find(
(item) => item.level === 'ERROR' && item.id > log.id
);
if (nextError) {
// 获取目标元素位置(需要组件引用更精确的计算)
this.scroller.scrollTo({
xOffset: 0,
yOffset: index * 50, // 近似位置
animation: { duration: 300 }
});
}
})
})
6.2 下拉刷新 + Scroll
原生 Scroll 不提供 pull-to-refresh 能力。在 API 24 中,你可以使用 SwipeRefresher 组件包裹 Scroll:
typescript
SwipeRefresher() {
Scroll() {
Column() {
// 内容
}
}
}
.onRefresh(() => {
// 执行刷新逻辑
})
6.3 滚动动画自定义
scrollTo() 的 animation 参数支持自定义:
typescript
scroller.scrollTo({
xOffset: 0,
yOffset: 500,
animation: {
duration: 600, // 时长(毫秒)
curve: Curve.FastOutSlowIn, // 缓动曲线
delay: 0 // 延迟(毫秒)
}
});
常用的 Curve 曲线:
| 曲线 | 特性 | 适用场景 |
|---|---|---|
Curve.Linear |
匀速 | 进度条 |
Curve.Ease |
慢→快→慢 | 通用 |
Curve.EaseIn |
慢→快 | 进入 |
Curve.EaseOut |
快→慢 | 退出 |
Curve.EaseInOut |
慢→快→慢 | 往返 |
Curve.FastOutSlowIn |
较快→慢 | 回弹效果 |
6.4 滚动边界检测
typescript
Scroll() { ... }
.onScrollEdge((side: Edge) => {
if (side === Edge.Top) {
console.log('到达顶部');
} else if (side === Edge.Bottom) {
console.log('到达底部,触发加载更多');
loadMoreData();
}
})
结合 onScrollEdge 可以实现「触底加载」效果:
typescript
@State hasMore: boolean = true;
loadMoreData(): void {
if (!this.hasMore) return;
// 加载下一页数据
fetchNextPage().then((newData) => {
this.logList = [...this.logList, ...newData];
if (newData.length < pageSize) {
this.hasMore = false; // 没有更多数据
}
});
}
七、总结
Scroll + Column 是 HarmonyOS NEXT 中最灵活、最通用的可滚动布局方案。它让你在保持「固定头 + 滚动体 + 固定尾」的三明治结构的同时,在滚动区域内自由组合各类异形组件。
七个你必须记住的核心要点
| # | 要点 | 速记口诀 |
|---|---|---|
| 1 | Scroll 只能有一个直接子组件,用 Column 包裹所有内容 | 「一层 Scroll,一层 Column」 |
| 2 | Scroll 内部 Column 不要设高度 | 「内容自由生长」 |
| 3 | Scroll 自身必须有高度约束(layoutWeight(1)) | 「Scroll 要有边界」 |
| 4 | 使用 Scroller 对象实现编程式滚动 | 「Scroller 是遥控器」 |
| 5 | onDidScroll 监听位置更新 UI 状态 | 「滚动即反馈」 |
| 6 | ForEach 传递键值生成器优化渲染 | 「键值绑定,精准渲染」 |
| 7 | 超过 200 条数据用 LazyForEach | 「小数据 ForEach,大数据 Lazy」 |
技术决策树
是否需要可滚动的列表页面?
├─ 内容类型单一、上千条数据 → List + LazyForEach
├─ 内容类型混合、一两百条数据 → Scroll + Column + ForEach
├─ 需要顶部固定、底部固定 → 三明治结构:Column 包裹三个区域
└─ 需要编程式控制滚动 → 声明 Scroller 并传入 Scroll
完整示例项目
本教程对应的完整可运行源码位于项目根目录 entry/src/main/ets/pages/ScrollExamplePage.ets,包含:
- 三明治布局(固定头 + 滚动体 + 固定尾)
- 6 个内容区域(原理说明、配置演示、日志列表、刷新演示、注意事项)
- 实时滚动状态反馈(方向、位置、高度)
- 编程式滚动控制(回到顶部、滚动到底部)
- 动态数据追加和模拟刷新
- 详尽的中文注释说明
将这个代码跑起来,一边滚动一边观察顶部的状态变化,你就能直观理解 Scroll 和 Column 的每一个协作细节。