【共创季稿事节】鸿蒙原生 ArkTS 布局精讲:Scroll 方向控制
鸿蒙原生 ArkTS 布局精讲:Scroll 方向控制(垂直 / 水平 / 双向)



一、引言
在移动端应用开发中,滚动(Scroll) 是最基础也最频繁使用的交互方式之一。无论是刷不完的朋友圈、横向滑动的商品推荐栏,还是横纵双向自由探索的图片墙,其底层都离不开 Scroll 容器的支持。
HarmonyOS NEXT 自 API 24 起,ArkUI 框架的 Scroll 组件日趋成熟,在滚动性能、手势流畅度、嵌套协调等方面均有显著提升。然而在实际开发中,很多开发者对 Scroll 的方向控制仍然存在认知盲区:
- 如何正确设置滚动方向?
- 为什么有时内容溢出了却无法滚动?
ScrollDirection.Vertical、Horizontal和FREE有什么区别?- 双向滚动场景下,子组件的宽高应该如何设置?
本文将以一个完整的示例应用为主线,手把手带你吃透 Scroll 方向控制。无论你是刚入门鸿蒙开发的新手,还是有一定经验想查漏补缺的进阶者,这篇文章都能让你有所收获。
二、Scroll 组件核心概念
2.1 什么是 Scroll?
Scroll 是 ArkUI 提供的一个可滚动容器组件,它允许在有限的视口(Viewport)内容纳超出自身尺寸的内容,用户通过手指拖拽即可查看被遮挡的部分。
┌─────────────────────┐ ← Scroll 容器(视口,固定宽高)
│ 可见区域 │
│ ┌───────────────┐ │
│ │ Item 1 │ │
│ │ Item 2 │ │
│ │ Item 3 │ │ ← 实际内容超出视口,需滚动查看
│ │ ... │ │
│ └───────────────┘ │
│ ↓ 拖动 │
└─────────────────────┘
关键约束:Scroll 只能包含一个直接子组件,通常使用 Column(垂直)、Row(水平)或 Flex(双向)作为内容容器,再在该容器中放置多个子元素。
2.2 核心 API
Scroll(scroller?: Scroller)
.scrollable(value: ScrollDirection)
⚠️ 注意:在 API 24 中,滚动方向使用
.scrollable()方法,不是.scrollDirection()。这是新手最容易踩的坑之一。
2.3 ScrollDirection 枚举(API 24)
| 枚举值 | 说明 | 典型场景 |
|---|---|---|
ScrollDirection.Vertical |
仅垂直方向滚动 | 新闻列表、聊天记录、评论流 |
ScrollDirection.Horizontal |
仅水平方向滚动 | 横向标签栏、商品卡片轮播 |
ScrollDirection.FREE |
自由双向滚动(API 20+) | 图片网格、地图、画布 |
ScrollDirection.None |
禁止滚动 | 固定内容区域 |
2.4 Scroll 的其他常用属性
| 属性 | 用途 | 示例值 |
|---|---|---|
scrollBar |
滚动条显示策略 | BarState.Auto / On / Off |
scrollBarColor |
滚动条颜色 | Color.Gray |
scrollBarWidth |
滚动条宽度 | 6 |
edgeEffect |
边缘回弹效果 | EdgeEffect.Spring / Fade / None |
enableScrollInteraction |
是否允许滚动交互 | true / false |
friction |
滚动摩擦系数 | 0.6(值越小越滑) |
三、实战:构建 Scroll 方向演示应用
下面通过一个完整的示例来直观理解三种方向配置。页面整体结构如下:
┌──────────────────────────────────┐
│ Scroll 方向控制演示 │
│ 当前方向描述文字 │
├────────┬────────┬───────────────┤
│ 垂直方向│ 水平方向│ 双向滚动 │ ← Tabs 切换
├────────┴────────┴───────────────┤
│ ┌──────────────────────────┐ │
│ │ Scroll 内容区域 │ │
│ │ (拖拽查看效果) │ │
│ └──────────────────────────┘ │
│ 📐 布局要点总结 │
│ 📌 核心 API │ ← 信息卡片
└──────────────────────────────────┘
3.1 数据模型与色块组件
我们首先定义一个 ColorItem 接口来描述每个色块的数据结构:
interface ColorItem {
label: string; // 色块编号,如 "V-01"
bgColor: string; // 背景色
width: number; // 色块宽度(vp)
height: number; // 色块高度(vp)
}
编写一个工具方法批量生成色块数据,每个色块被赋予不同的背景色和编号前缀(V / H / B 分别代表三种方向):
buildColorItems(prefix: string, count: number): ColorItem[] {
const items: ColorItem[] = [];
const colors = [
'#FF6B6B', '#FFA94D', '#FFD43B', '#69DB7C', '#38D9A9',
'#4DABF7', '#748FFC', '#9775FA', '#F06595', '#FF8787',
'#FFC078', '#FCC419', '#8CE99A', '#66D9E8', '#74C0FC',
'#91A7FF', '#B197FC', '#F783AC', '#FF8A8A', '#FFD399'
];
for (let i = 0; i < count; i++) {
items.push({
label: `${prefix}-${String(i + 1).padStart(2, '0')}`,
bgColor: colors[i % colors.length],
width: 160,
height: 80
});
}
return items;
}
色块渲染则抽离为一个 @Builder 方法,方便在三个 Tab 中复用:
@Builder
ColorBlock(item: ColorItem) {
Column() {
Text(item.label).fontSize(16).fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Text('拖动可滚动').fontSize(10)
.fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })
}
.width(item.width).height(item.height)
.backgroundColor(item.bgColor).borderRadius(8)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.15)', offsetY: 2 })
}
3.2 垂直滚动(ScrollDirection.Vertical)
原理:当子组件的 height 超过 Scroll 容器的 height 时,且方向设置为 Vertical,用户即可在纵向上拖拽滚动。
Scroll(this.verticalScroller) {
Column({ space: 12 }) {
ForEach(this.buildColorItems('V', 20), (item: ColorItem) => {
this.ColorBlock(item)
})
}
.width('100%')
.height(2000) // ★ 远超 Scroll 容器的视口高度(300vp)
}
.scrollable(ScrollDirection.Vertical) // 设置为垂直滚动
.width('100%')
.height(300)
.border({ width: 1, color: '#007DFF', style: BorderStyle.Solid })
.borderRadius(12).padding(8)
.backgroundColor('#F5F9FF')
要点说明:
- 外层
Scroll的height设为300vp,作为「视口」高度。 - 内层
Column的height设为2000vp,远超视口高度,产生溢出。 - 通过
.scrollable(ScrollDirection.Vertical)显式声明方向。 - 垂直滚动是 Scroll 的默认方向,不写
.scrollable()也默认为垂直。
视觉表现:20 个彩色方块纵向排列,用户上下拖动即可依次查看 V-01 到 V-20。
真实场景映射:微信朋友圈的时间线、微博信息流、商品评论列表——这些都是典型的垂直滚动场景。
3.3 水平滚动(ScrollDirection.Horizontal)
原理:当子组件的 width 超过 Scroll 容器的 width 时,且方向设置为 Horizontal,用户即可在横向上拖拽滚动。
Scroll(this.horizontalScroller) {
Row({ space: 12 }) {
ForEach(this.buildColorItems('H', 20), (item: ColorItem) => {
this.ColorBlock(item)
})
}
.width(4000) // ★ 远超 Scroll 容器的宽度
.height('100%')
}
.scrollable(ScrollDirection.Horizontal) // 设置为水平滚动
.width('100%')
.height(300)
.border({ width: 1, color: '#00A86B', style: BorderStyle.Solid })
.borderRadius(12).padding(8)
.backgroundColor('#F0FFF5')
要点说明:
- 使用
Row作为子容器,所有色块水平排列。 Row的width设为4000vp,而 Scroll 容器宽度为父容器宽,产生水平溢出。- 注意
Row不需要显式设置宽度约束,让子元素内容撑开即可。 .scrollable(ScrollDirection.Horizontal)声明水平方向。
视觉表现:20 个色块排成一行,用户左右拖动,滚动条在底部浮现。
真实场景映射:淘宝/京东顶部的分类导航栏、音乐 App 的推荐歌曲横滑区域、股票 App 的 K 线时间轴。
3.4 双向(自由)滚动(ScrollDirection.FREE)
原理:当子组件的 width 和 height 同时超过 Scroll 容器的对应尺寸时,且方向设置为 FREE,用户即可在横纵两个方向上自由滚动。
⚠️ 重要演进:在 API 9 之前,双向滚动使用
ScrollDirection.Free(已废弃);自 API 20 起,推荐使用ScrollDirection.FREE。注意大小写差异。
Scroll(this.bothScroller) {
Flex({
direction: FlexDirection.Row,
wrap: FlexWrap.Wrap, // ★ 自动换行,产生垂直方向溢出
justifyContent: FlexAlign.Start,
alignContent: FlexAlign.Start,
}) {
ForEach(this.buildColorItems('B', 40), (item: ColorItem) => {
this.ColorBlock(item)
})
}
.width(2000) // ★ 宽高均远超 Scroll 容器
.height(2000)
}
.scrollable(ScrollDirection.FREE) // 自由双向滚动
.width('100%')
.height(300)
.border({ width: 1, color: '#FF6B35', style: BorderStyle.Solid })
.borderRadius(12).padding(8)
.backgroundColor('#FFF8F0')
要点说明:
- 使用
Flex+FlexWrap.Wrap实现自动换行的网格布局,同时产生水平和垂直方向的溢出。 Flex的width(2000)和height(2000)均远超 Scroll 容器尺寸,这是触发双向滚动的必要条件。- 40 个色块以 4 行 × 10 列的网格排列,行满自动换行。
.scrollable(ScrollDirection.FREE)声明为自由方向。
常见误区:
| 错误写法 | 问题 | 后果 |
|---|---|---|
只设 width(2000),height 用默认值 |
仅在水平方向溢出 | 只触发水平滚动 |
只设 height(2000),width 用默认值 |
仅在垂直方向溢出 | 只触发垂直滚动 |
| 两者都不超 Scroll 尺寸 | 无溢出 | 根本无法滚动 |
使用 ScrollDirection.Free(小写 f) |
API 9 已废弃 | 编译器警告 |
真实场景映射:手机相册的网格视图、地图应用的缩放平移、Excel/表格数据查看器。
3.5 用 Tabs 整合三种方向
为了让三种方向在同一页面中直观对比,我们使用 Tabs 组件进行切换:
Tabs({ index: this.currentTabIndex }) {
TabContent() { /* 垂直滚动 */ }.tabBar('垂直方向')
TabContent() { /* 水平滚动 */ }.tabBar('水平方向')
TabContent() { /* 双向滚动 */ }.tabBar('双向滚动')
}
// ★ onChange 作为链式方法,不是构造参数!
.onChange((index: number) => {
this.currentTabIndex = index;
})
⚠️ API 24 注意:
onChange事件需要通过链式调用绑定,而不是放在Tabs的构造参数对象中。这是很多从低版本迁移上来的开发者容易出错的地方。
每个 Tab 切换时,顶部的描述文字也会同步变化:
getDirectionDescription(): string {
return [
'垂直滚动 — 内容在纵向上超出容器高度时触发滚动',
'水平滚动 — 内容在横向上超出容器宽度时触发滚动',
'双向滚动 — 内容在横向和纵向均超出容器尺寸时触发滚动'
][this.currentTabIndex];
}
四、滚动控制器(Scroller)进阶
每个 Scroll 绑定一个 Scroller 实例,可以实现编程式滚动控制。注意在 API 23+ 中 Scroller 已内置到全局作用域,无需 import。
private scroller: Scroller = new Scroller();
// 滚动到顶部
this.scroller.scrollEdge(Edge.Top);
// 滚动到底部
this.scroller.scrollEdge(Edge.Bottom);
// 滚动到指定偏移量(带动画)
this.scroller.scrollTo({ xOffset: 0, yOffset: 500 });
// 获取当前偏移量
const offset = this.scroller.currentOffset();
console.info(`x: ${offset.xOffset}, y: ${offset.yOffset}`);
// 惯性滑动
this.scroller.fling(-3000);
五、常见踩坑记录
❌ 问题 1:内容溢出了但无法滚动
现象:子组件明明比 Scroll 大,但拖不动。
原因:有两种可能——
- 方向设置错误:期望水平滚动但忘了写
.scrollable(ScrollDirection.Horizontal),默认垂直方向自然无法水平拖动。 - 子组件尺寸没有真正超出:有时子组件设置了
width('100%')但父容器宽度等于屏幕宽,导致没有溢出。
解决:水平滚动确认内层子组件 width 超过 Scroll 宽度;双向滚动确认 width 和 height 均超过。
❌ 问题 2:ScrollDirection.Both 编译报错
现象:编译器报错 Property 'Both' does not exist on type 'typeof ScrollDirection'。
原因:Both 不是合法值。API 20+ 中双向滚动使用 ScrollDirection.FREE(全大写)。
// ✅ 正确
.scrollable(ScrollDirection.FREE)
// ❌ 错误写法
.scrollable(ScrollDirection.Both) // 不存在
.scrollable(ScrollDirection.Free) // API 9 已废弃
❌ 问题 3:.scrollDirection() 方法不存在
现象:编译器报错 Property 'scrollDirection' does not exist on type 'ScrollAttribute'。
原因:正确的方法名是 .scrollable(),不是 .scrollDirection()。
❌ 问题 4:Tabs 的 onChange 不生效
现象:在 Tabs 构造参数中传 onChange 函数,编译器报错。
原因:API 24 中,TabsOptions 不再将 onChange 作为构造属性。
// ✅ 正确
Tabs({ index: 0 }) { /* ... */ }
.onChange((index) => { /* ... */ })
// ❌ 错误
Tabs({ index: 0, onChange: (i) => { } }) { }
❌ 问题 5:Scroller 导入报错
现象:import { Scroller } from '@kit.ArkUI' 报 has no exported member。
原因:API 23+ 中 Scroller 类型已内置到全局作用域,无需显式导入。
// ✅ 无需 import,直接使用
private scroller: Scroller = new Scroller();
六、核心知识速查表
| 方向 | 方法 | 子容器 | 溢出条件 |
|---|---|---|---|
| 垂直 | .scrollable(Vertical) |
Column |
height > 容器高度 |
| 水平 | .scrollable(Horizontal) |
Row |
width > 容器宽度 |
| 双向 | .scrollable(FREE) |
Flex 等 |
width 且 height 均超出 |
避坑清单
- ✅ 方法名是
scrollable(),不是scrollDirection() - ✅ 双向滚动用
ScrollDirection.FREE,不是Both - ✅ Scroller 全局可用,无需 import
- ✅ Tabs 的
onChange用链式,不用构造参数 - ✅ 子容器宽/高必须超过 Scroll 容器对应尺寸
- ✅ 每个 Scroll 绑定独立的 Scroller 实例
七、进阶方向
掌握了 Scroll 方向控制之后,可以进一步探索:
- Scroll + LazyForEach:百万级长列表的懒加载渲染
- Scroll + Grid:构建可横向滚动的网格布局
- 嵌套滚动:通过
nestedScroll属性协调父子滚动组件的联动 - 自定义下拉刷新:基于
onWillScroll/onDidScroll事件实现 - 滚动条美化:通过
scrollBarColor和scrollBarWidth调优视觉风格
八、参考资料
- HarmonyOS 开发者文档 — Scroll 组件
- HarmonyOS 开发者文档 — Tabs 组件
- HarmonyOS 开发者文档 — ScrollDirection 枚举
- OpenHarmony GitHub — Scroll 源码
本文所有代码已在 HarmonyOS NEXT API 24 环境下编译通过。如果你在实践过程中遇到问题,欢迎留言交流。
更多推荐


所有评论(0)