【共创季稿事节】HarmonyOS NEXT Scroll 滚动条控制实战 — show / hide / auto 三种策略深度解析
HarmonyOS NEXT Scroll 滚动条控制实战 — show / hide / auto 三种策略深度解析



目录
- 引言:为什么滚动条控制如此重要
- HarmonyOS NEXT 与 ArkTS 概述
- Scroll 组件深度剖析
- BarState 枚举:滚动条的三种灵魂状态
- scrollBar() API 详解
- 完整示例:ScrollBarDemo 逐段解读
- API 24 新特性与迁移指南
- 实战场景:不同业务下的滚动条策略选型
- 性能优化与最佳实践
- 常见问题与踩坑记录
- 总结与展望
1. 引言:为什么滚动条控制如此重要
在移动端和桌面端应用开发中,滚动(Scroll)是最基础、最频繁的交互行为之一。用户通过上下滑动浏览长列表、长文章、聊天记录、商品橱窗……可以说,没有滚动,就没有现代移动应用。
而滚动条(Scrollbar),作为滚动状态的视觉指示器,其存在感往往被忽视——直到它出了问题。一个设计得当的滚动条,能:
- 让用户直观感知当前内容在整体中的位置(“已经看到一半了”);
- 提供操作反馈(“我确实在滚动,速度有多快”);
- 维持页面视觉整洁(“不该显示的时候不要乱入”);
- 在无障碍场景中告知屏幕阅读器可滚动区域的范围。
正因如此,HarmonyOS NEXT 在 ArkUI 框架中提供了 .scrollBar() API,让开发者可以精确控制滚动条的三种显示行为:Auto(按需显示)、On(始终显示)、Off(始终隐藏)。
本文将从一个完整的 ArkTS 示例应用出发,逐行剖析 Scroll 滚动条控制的实现原理、API 用法、架构设计和最佳实践。无论你是刚接触 HarmonyOS 开发的新手,还是从 Android/iOS 跨平台迁移而来的工程师,这篇文章都能帮你彻底掌握 Scroll 滚动条控制这一核心布局技能。
2. HarmonyOS NEXT 与 ArkTS 概述
2.1 什么是 HarmonyOS NEXT
HarmonyOS NEXT 是华为从 2024 年起全力推进的纯血鸿蒙操作系统——它彻底剥离了 AOSP(Android 开源项目)代码,完全基于鸿蒙微内核,使用鸿蒙原生应用框架进行开发。这意味着:
- 不再兼容 APK,所有应用必须使用鸿蒙原生 SDK 开发;
- 全面拥抱 ArkTS/ArkUI,作为第一公民的声明式 UI 框架;
- 性能与安全大幅提升,微内核架构天然支持分布式、多设备协同。
2.2 ArkTS:强类型的声明式 UI 语言
ArkTS(Ark TypeScript)是鸿蒙原生应用开发的语言,它是 TypeScript 的超集,但做了严格的限制和增强:
| 特性 | ArkTS | 标准 TypeScript |
|---|---|---|
| 类型系统 | 强制显式类型,禁止 any/unknown |
可选类型,可隐式推断 |
| 装饰器 | @Component、@Entry、@State、@Prop 等 |
无 |
| 对象字面量 | 必须对应显式声明的类或接口 | 自由使用 |
| 计算属性名 | 不支持 [expr] 语法 |
支持 |
| UI 构建 | build() 方法内声明式布局 |
无 |
小贴士:将 ArkTS 理解为一个"带 UI 框架的严格模式 TypeScript"——它对类型安全的要求比传统 TS 更苛刻,但换来的是编译期就能捕获大量错误,运行时更加稳定可靠。
2.3 API 版本演进:从 API 23 到 API 24
本文示例最初基于 API 23(SDK 6.1.0)开发,但在编写博客时已升级到 API 24(SDK 7.0.0)。API 24 带来了若干重要变化:
- BarState 导入路径统一:
import { BarState } from '@kit.ArkUI'在 API 24 中正式可用; - 滚动条 API 行为优化:Auto 模式的淡入淡出动画更加平滑;
- 编译规则收紧:对
@State装饰的类型检查更加严格; - 新增 ScrollEdgeEffect API:支持边缘回弹效果配置。
我们将在第 7 节详细讨论 API 升级的影响和迁移步骤。
3. Scroll 组件深度剖析
3.1 基本概念
Scroll 是 ArkUI 中最核心的可滚动容器组件,它允许其子组件的内容超出容器尺寸时,通过滑动操作来浏览完整内容。它的地位相当于 Android 中的 ScrollView、iOS 中的 UIScrollView、Web 中的 overflow: auto 容器。
Scroll() {
// 子组件:可以是 Column、Row、Flex 或任意容器
Column() {
// 超长内容...
}
}
// 链式配置
.scrollBar(BarState.Auto)
.scrollable(ScrollDirection.Vertical)
3.2 Scroll 的核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
.scrollBar() |
BarState |
BarState.Auto |
滚动条显示策略 |
.scrollable() |
ScrollDirection |
ScrollDirection.Vertical |
滚动方向 |
.edgeEffect() |
EdgeEffect |
EdgeEffect.None |
边缘回弹效果 |
.enableScrollInteraction() |
boolean |
true |
是否允许滚动交互 |
.scrollSpeed() |
number |
— | 滚动速度控制 |
.scrollBy() |
(dx, dy) |
— | 编程式滚动 |
.scrollTo() |
(x, y, smooth) |
— | 定位到指定位置 |
.scrollToIndex() |
index |
— | 定位到指定子项(配合 List) |
3.3 Scroll 与 List 的区别
初学者经常混淆 Scroll 和 List。它们的核心区别在于:
- Scroll:通用可滚动容器,子组件全部一次性布局渲染。适用于内容数量固定或较少的场景(一篇文章、一个表单)。
- List:高性能虚拟列表,只渲染可见区域的子项。适用于数据量很大(几百上千条)的场景(联系人列表、信息流)。
选择建议:如果你的内容项超过 50 个,或者每个子项渲染成本较高,应优先考虑
List而非Scroll。Scroll 适合内容量可控的通用场景。
3.4 Scroll 的内部机制
在底层,Scroll 组件维护了一个可滑动区域(scrollable content area)和一个视口(viewport)。视口是用户当前看到的矩形区域,可滑动区域是子组件实际占用的完整矩形区域。
┌─────────────────────────────────┐ ← 可滑动区域顶部(Scroll 内容上边界)
│ 内容项 1 │
├─────────────────────────────────┤
│ 内容项 2 │
├─────────────────────────────────┤
│ 内容项 3 │
├─────────────────────────────────┤
│ 内容项 4 │
├─────────────────────────────────┤
│ 内容项 5 │
├─────────────────────────────────┤
│ ... │
├─────────────────────────────────┤
│ 内容项 30 │
└─────────────────────────────────┘ ← 可滑动区域底部
┌─────────────────────────┐ ← 视口(用户可见区域,高度由父容器决定)
│ 内容项 1 │
│ 内容项 2 │
│ 内容项 3 │
│ 内容项 4 │
└─────────────────────────┘
↑ 滚动偏移量 (scrollOffset)
当用户滑动时,Scroll 计算新的 scrollOffset,并重绘视口内的内容。滚动条的位置和大小就是基于 scrollOffset 和视口/内容比例计算得出的。
3.5 ScrollDirection 详解
.scrollable() 方法接受一个 ScrollDirection 枚举值:
| 枚举值 | 说明 |
|---|---|
ScrollDirection.Vertical |
仅垂直方向可滚动 |
ScrollDirection.Horizontal |
仅水平方向可滚动 |
ScrollDirection.Both |
两个方向均可滚动(不常见) |
ScrollDirection.None |
禁止滚动 |
注意:当设定为
None时,内容溢出会被裁剪且无法滑动,应谨慎使用。
4. BarState 枚举:滚动条的三种灵魂状态
4.1 枚举定义
BarState 是 ArkUI 中专门用于控制滚动条显示状态的枚举,定义如下:
enum BarState {
Auto = 0, // 滚动时显示,停止后自动隐藏
On = 1, // 始终显示
Off = 2 // 始终隐藏
}
在 API 24 中,可以通过以下方式导入:
// 推荐方式(API 24+)
import { BarState } from '@kit.ArkUI';
// 或(API 23 及以下)
import { BarState } from '@ohos.arkui.component';
4.2 BarState.Auto(按需显示)—— 默认策略
行为描述:
用户手指/鼠标触发的滚动操作开始 → 滚动条淡入显示
用户停止滚动操作 → 等待约 1.5s 的静默期 → 滚动条淡出隐藏
再次滚动 → 重新淡入,循环往复
视觉特征:
· 滚动条在不操作时完全不可见,界面干净
· 滚动过程中半透明显示,不遮挡主要内容
· 淡入淡出有约 200ms 的过渡动画
适用场景:
- 绝大多数移动端应用(聊天、信息流、文章阅读)
- 需要最大化内容可视区域
- 用户通过滑动行为本身就能感知"还有更多内容"
缺点:
- 不能直观告知用户当前浏览位置(除非正在滑动)
- 对鼠标用户(桌面端)不如 On 策略友好
4.3 BarState.On(始终显示)—— 精确指示
行为描述:
滚动条始终在界面右侧/底部占据固定轨道
内容滚动时,滚动条滑块移动指示当前位置
不随时间淡出
视觉特征:
· 滚动条轨道和滑块始终可见
· 滑块大小反映内容比例(视口高度 / 总内容高度)
· 颜色可能比 Auto 模式更深一些,以维持持续可见性
适用场景:
- 桌面端应用或平板大屏场景
- 数据报表、文档编辑器、代码编辑器
- 需要用户精确知道"浏览进度"的场景
- 无障碍需求中需持续指示可滚动区域
缺点:
- 占用了部分屏幕空间(尽管很小)
- 在内容很短(无需滚动)时,不应显示滚动条(但 On 模式仍会显示轨道)
细节注意:在 BarState.On 模式下,即使内容不需要滚动,滚动条轨道也可能显示。某些场景下可能需要配合判断是否可滚动来隐藏。
4.4 BarState.Off(始终隐藏)—— 沉浸体验
行为描述:
无论用户是否滚动,滚动条始终不可见
滚动交互仍然正常进行(除非 scrollable 设为 None)
视觉特征:
· 完全不显示滚动条轨道和滑块
· 界面最干净
· 用户通过滑动行为感知可滚动
适用场景:
- 全屏相册、画廊、轮播图
- 沉浸式阅读器、电子书
- 游戏界面、视频播放器控制层
- 自定义滚动指示器(如进度条、页码指示器)
- 极简主义 UI 设计
缺点:
- 用户无法直接从视觉上判断内容是否可滚动
- 需要其他交互线索(如部分内容显露、手势提示、渐进加载)来暗示可滚动
4.5 三种策略对比汇总
| 特性 | Auto | On | Off |
|---|---|---|---|
| 滚动中显示 | ✅ | ✅ | ❌ |
| 静止时显示 | ❌(1.5s 后消失) | ✅ | ❌ |
| 占用屏幕空间 | 否 | 是(小轨道) | 否 |
| 位置感知 | 仅滚动时 | 始终可用 | 无 |
| 适用于移动端 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 适用于桌面端 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 沉浸感 | 高 | 中 | 最高 |
| 无障碍友好 | 中 | 高 | 低(需配合其他提示) |
5. scrollBar() API 详解
5.1 方法签名
scrollBar(barState: BarState): ScrollAttribute
参数:
barState—BarState枚举值,指定滚动条的显示策略。
返回值:
ScrollAttribute— 返回 Scroll 组件自身,支持链式调用。
5.2 使用方式
// 方式一:传入枚举值
Scroll() {
// ...
}
.scrollBar(BarState.On)
.scrollable(ScrollDirection.Vertical)
// 方式二:直接传入数值(兼容旧版本 SDK)
Scroll() {
// ...
}
.scrollBar(1) // 等同于 BarState.On
5.3 与其他滚动属性的配合
.scrollBar() 不是孤立的 API——它的效果和以下属性密切相关:
Scroll() {
// 子组件...
}
.scrollBar(BarState.Auto)
.scrollable(ScrollDirection.Vertical) // 滚动方向影响滚动条出现位置
.edgeEffect(EdgeEffect.Spring) // 边缘回弹效果(API 24+)
.enableScrollInteraction(true) // 关闭滚动时滚动条自然也不工作
5.4 动态切换策略
利用 @State 装饰器和 ArkTS 的声明式特性,可以运行时动态切换滚动条策略。这正是我们示例应用的核心机制:
@Component
struct ScrollBarDemo {
@State currentStrategy: BarState = BarState.Auto;
build() {
Column() {
// 按钮切换 this.currentStrategy...
Button('切换为始终显示')
.onClick(() => {
this.currentStrategy = BarState.On; // 状态变量变化触发 UI 重渲染
})
Scroll() {
// 长内容...
}
.scrollBar(this.currentStrategy) // ← 动态绑定策略值
}
}
}
每当 currentStrategy 变化,ArkUI 框架会重新执行 build() 方法,将新的策略值传给 .scrollBar() 方法,Scroll 组件随即更新滚动条显示行为——无需手动操作 DOM 或调用额外的更新方法。
5.5 scrollBar 的默认值
Scroll 组件的 .scrollBar() 默认值为 BarState.Auto。这意味着如果你在使用 Scroll 时没有显式调用 .scrollBar(),系统会使用 Auto 策略。
这是一个明智的默认值——大多数场景下 Auto 就是最佳选择,同时它也保持了和 Web/移动平台一致的预期行为。
6. 完整示例:ScrollBarDemo 逐段解读
现在我们逐段分析完整示例代码。为了更好地说明,这里使用 API 24 的导入方式(与项目中的 API 23 兼容版本略有不同)。
6.1 文件结构与导入
/**
* ScrollBarDemo — 鸿蒙原生 ArkTS 布局:Scroll 滚动条控制(show / hide / auto)
*
* 场景说明:
* Scroll 组件默认在滚动时会出现滚动条。通过 .scrollBar() 方法
* 传入 BarState 枚举值,可精确控制滚动条的显示行为策略。
*
* 核心技术:
* · Scroll 组件
* · .scrollBar(BarState) — 控制滚动条显示策略
* · .scrollable(ScrollDirection) — 滚动方向
*/
// API 24 推荐导入方式
import { BarState } from '@kit.ArkUI';
注释要点:
- 使用 JSDoc 风格的块注释描述文件用途,方便后续维护者快速理解;
- 明确列出"核心技术点",相当于文件的目录摘要;
- API 24 中
BarState可以直接从@kit.ArkUI导入,更加统一。
6.2 组件结构与状态定义
@Entry
@Component
struct ScrollBarDemo {
// BarState 枚举的数值常量定义(作为后备方案)
private readonly BAR_STATE_AUTO: number = 0;
private readonly BAR_STATE_ON: number = 1;
private readonly BAR_STATE_OFF: number = 2;
// 当前选中的滚动条策略,默认 Auto
@State currentStrategy: BarState = BarState.Auto;
// 策略选项对应的中文标签
private readonly strategyLabels: string[] = [
'Auto(自动显示)',
'On(始终显示)',
'Off(始终隐藏)'
];
// ... 更多状态
}
设计要点:
@Entry— 标记该组件为页面入口,可以被路由导航到;@Component— 声明一个自定义组件;@State currentStrategy— 状态变量,当其值变化时触发 UI 自动重渲染;readonly+ 数组 — 因为 ArkTS 不支持计算属性名作为对象 key,我们用数组下标索引替代 Record 类型;- 常量定义 —
BAR_STATE_AUTO/ON/OFF作为数值兜底,确保在不便导入枚举时也能正常工作。
6.3 数据模拟
/** 模拟的列表数据项(用来撑开内容高度,使滚动生效) */
private readonly items: string[] = this.buildItems(30);
buildItems(count: number): string[] {
const result: string[] = [];
for (let i: number = 0; i < count; i++) {
result.push(`第 ${i + 1} 项内容`);
}
return result;
}
设计考量:
- 使用
for循环替代Array.map(),避免匿名函数的类型推断问题; - 30 项数据足以让内容超过视口高度,触发滚动;
- 显式标注
i: number类型,满足 ArkTS 的严格类型要求。
6.4 页面布局构建
build() {
Column({ space: 12 }) {
// ── 1) 页面标题 ──
Text('Scroll 滚动条控制:show / hide / auto')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 16, bottom: 4 })
Text('点击下方按钮切换 .scrollBar() 策略,观察滚动条行为变化')
.fontSize(14)
.fontColor(Color.Gray)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ bottom: 8 })
// ... 后续区域
}
.width('100%')
.height('100%')
.backgroundColor(0xFFF0F0F0)
}
布局策略:
- 最外层是
Column容器,垂直排列所有区域; { space: 12 }设置子组件的间距,保持呼吸感;- 标题使用超大字号(20)加粗,副标题使用灰色小字(14);
- 整个页面背景为浅灰色
#F0F0F0,营造干净的技术演示氛围。
6.5 策略切换按钮组
Row({ space: 8 }) {
ForEach(
[0, 1, 2] as number[],
(index: number) => {
Button(this.strategyLabels[index])
.height(40)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.backgroundColor(
this.currentStrategy === this.strategyValues[index]
? this.strategyColors[index]
: Color.Gray
)
.onClick(() => {
this.currentStrategy = this.strategyValues[index];
})
}
)
}
.width('100%')
.padding({ left: 12, right: 12 })
交互逻辑:
- 使用
ForEach遍历[0, 1, 2](代表三个策略的索引),动态生成三个按钮; layoutWeight(1)让三个按钮在 Row 中等宽分布;- 当前选中的策略对应的按钮高亮为特定颜色(Auto→绿, On→橙, Off→灰),其余按钮为灰色;
- 点击事件更新
currentStrategy,触发 UI 重渲染。
颜色编码的意义:
- 绿色(Auto)——代表"自然、自动、智能",Auto 是最推荐、最通用的策略;
- 橙色(On)——代表"醒目、始终存在",On 模式始终显示,适合需要明确指示的场景;
- 灰色(Off)——代表"隐藏、低调",Off 模式完全隐藏滚动条。
6.6 策略说明与状态标签
// 策略说明文字
Text(this.getStrategyDesc(this.currentStrategy))
.fontSize(13)
.fontColor(Color.Gray)
.lineHeight(20)
.width('100%')
.padding({ left: 12, right: 12 })
// 当前策略标签(醒目展示当前值)
Text(this.getStrategyLabel(this.currentStrategy))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.getStrategyColor(this.currentStrategy))
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 4, bottom: 4 })
这里通过三个辅助函数 getStrategyDesc、getStrategyLabel、getStrategyColor 将策略值和展示内容解耦——这是 ArkTS 中替代对象映射表的推荐做法:
getStrategyLabel(strategy: number): string {
if (strategy === this.BAR_STATE_AUTO) {
return '当前策略:Auto(自动显示)';
} else if (strategy === this.BAR_STATE_ON) {
return '当前策略:On(始终显示)';
} else {
return '当前策略:Off(始终隐藏)';
}
}
6.7 核心滚动区域
// ★ 核心 API ★
Scroll() {
Column({ space: 8 }) {
ForEach(
this.items,
(item: string, index?: number) => {
Row() {
// 序号圆形图标
Circle()
.width(36)
.height(36)
.fill(this.getStrategyColor(this.currentStrategy))
.opacity(0.7)
// 序号文字
Text(`${(index ?? 0) + 1}`)
.fontColor(Color.White)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.width(36)
.height(36)
.textAlign(TextAlign.Center)
// 内容文本
Text(item)
.fontSize(16)
.margin({ left: 12 })
}
.width('100%')
.height(56)
.padding({ left: 12, right: 12 })
.borderRadius(8)
.backgroundColor(Color.White)
.shadow({
radius: 4,
color: 0x22000000,
offsetX: 0,
offsetY: 2
})
}
)
}
.width('100%')
.padding(12)
}
.layoutWeight(1)
.width('100%')
.borderRadius(12)
.backgroundColor(0xFFF5F5F5)
.padding(4)
// ★ 关键 API 调用:动态绑定滚动条策略 ★
.scrollBar(this.currentStrategy as BarState)
// 明确设定为垂直滚动
.scrollable(ScrollDirection.Vertical)
架构亮点:
layoutWeight(1)— Scroll 组件占据 Column 中所有剩余空间,确保内容区域最大化;- 序号 Circle + Text — 由于
.overlay()在 ArkTS 中参数类型限制,我们将圆和文字分别放置,通过约束尺寸对齐; - 卡片式列表项 — 白色背景、圆角(8)、阴影,每个列表项看起来像独立的卡片,视觉层次分明;
- 动态颜色 — 圆的填充色随策略切换而变化,给用户额外的视觉反馈;
.scrollBar(this.currentStrategy as BarState)— 核心逻辑:策略值的变化通过 ArkTS 声明式绑定传递给 Scroll 组件。
6.8 底部代码提示
Text(this.getApiDisplayText(this.currentStrategy))
.fontSize(14)
.fontColor(0xFF666666)
.fontFamily('Courier New')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ bottom: 16 })
使用等宽字体 Courier New 显示当前生效的 API 调用文本(如 .scrollBar(BarState.On)),让读者直观地将 UI 行为和代码对应起来。
getApiDisplayText 方法的实现:
getApiDisplayText(strategy: number): string {
if (strategy === this.BAR_STATE_AUTO) {
return '.scrollBar(BarState.Auto)';
} else if (strategy === this.BAR_STATE_ON) {
return '.scrollBar(BarState.On)';
} else {
return '.scrollBar(BarState.Off)';
}
}
6.9 EntryAbility 的入口配置
为了让应用启动后直接加载 ScrollBarDemo 页面,我们修改了 EntryAbility.ets:
windowStage.loadContent('pages/ScrollBarDemo', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag',
'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag',
'Succeeded in loading the content.');
});
同时需要在 main_pages.json 中注册路由:
{
"src": [
"pages/Index",
"pages/ScrollBarDemo"
]
}
7. API 24 新特性与迁移指南
7.1 API 24(SDK 7.0.0)的重要更新
HarmonyOS NEXT API 24(对应 SDK 7.0.0)于 2025 年下半年发布,带来了大量改进。以下是和 Scroll 组件直接相关的变化:
7.1.1 更统一的导入路径
API 23(旧):
import { BarState } from '@ohos.arkui.component';
// 或
// BarState 不在 @kit.ArkUI 的顶层导出中
API 24(新):
import { BarState } from '@kit.ArkUI';
// 统一从 Kit 层面导入,不再需要关注底层模块路径
这是一个"微观但重要"的变化。API 24 显著简化了 ArkUI 组件的导入路径,开发者只需记忆 Kit 名称即可,无需纠结 ArkUI 内部模块结构。
7.1.2 类型安全增强
API 24 的编译检查比 API 23 更严格:
| 检查项 | API 23 | API 24 |
|---|---|---|
| 对象字面量必须对应接口/类 | ✅ | ✅(更严格) |
| 无计算属性名 | ✅ | ✅ |
无 any/unknown |
✅ | ✅ |
| @State 类型推断 | 宽松 | 严格 |
| 枚举类型传递 | 允许 number → enum 隐式转换 | 推荐显式 as 转换 |
在我们的示例中,.scrollBar(this.currentStrategy as BarState) 这种显式类型断言在 API 24 中既是推荐写法,也是避免编译警告的保障。
7.1.3 边缘回弹效果
API 24 新增了 edgeEffect API,可以与 scrollBar 配合使用:
Scroll() {
// ...
}
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring) // 到达边缘时产生弹性回弹效果
可选的 EdgeEffect 值:
| 值 | 效果 |
|---|---|
EdgeEffect.None |
无特殊效果,滚动到边缘即停止 |
EdgeEffect.Spring |
弹性回弹效果(类似 iOS 的 rubber-band) |
结合 Auto 模式 + 弹性效果,可以提供非常接近原生 iOS 的滚动体验。
7.2 从 API 23 迁移到 API 24 的步骤
如果你的项目当前使用 API 23(如我们的示例),迁移到 API 24 只需几步:
步骤 1:更新 build-profile.json5
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "7.0.0(24)", // 由 6.1.0(23) 改为 7.0.0(24)
"compatibleSdkVersion": "7.0.0(24)",
// ...
}
],
// ...
}
}
步骤 2:迁移导入语句
// 旧
// import { BarState } from '@ohos.arkui.component';
// 新
import { BarState } from '@kit.ArkUI';
// 同时也可以移除数值常量的兜底方案
// private readonly BAR_STATE_AUTO: number = 0; // 不再需要
步骤 3:补充 edgeEffect(可选)
Scroll() {
// ...
}
.scrollBar(BarState.Auto)
.scrollable(ScrollDirection.Vertical)
.edgeEffect(EdgeEffect.Spring) // API 24+
步骤 4:重新编译验证
hvigorw clean
hvigorw assembleApp
7.3 版本兼容性策略
如果你的应用需要同时兼容 API 23 和 API 24(例如分发到不同系统版本的设备),可以考虑条件编译或运行时检测:
// 运行时判断 API 版本
const isApi24OrAbove: boolean = canIUse('SystemCapability.ArkUI.ArkUI24');
// 根据版本选择不同的导入或写法
if (isApi24OrAbove) {
// 使用 API 24 特性(如 edgeEffect)
}
不过在实际开发中,建议直接以最新稳定 API 版本为目标,避免维护两套代码。
8. 实战场景:不同业务下的滚动条策略选型
8.1 社交聊天应用
典型页面:聊天界面、消息列表
推荐策略:BarState.Auto
理由:
- 聊天记录通常较长,用户频繁上下滑动浏览;
- 滚动条只在滑动时出现,不影响消息气泡的显示面积;
- 如果设为 Off,用户无法感知"上面还有未读消息";
- 如果设为 On,滚动条轨道持续存在可能干扰阅读沉浸感。
代码示例:
Scroll() {
Column() {
ForEach(this.messages, (msg: Message) => {
MessageBubble({ message: msg })
})
}
}
.scrollBar(BarState.Auto)
.scrollable(ScrollDirection.Vertical)
.edgeEffect(EdgeEffect.Spring)
8.2 文档阅读 / PDF 查看器
典型页面:长文章阅读页、电子书阅读器
推荐策略:BarState.On(阅读时)或 BarState.Auto(通用)
理由:
- 用户阅读长文档时,需要随时知道"我已经读到 67% 的位置了";
- On 模式的滚动条可以充当阅读进度条;
- 但有些阅读器更偏好 Auto + 顶部进度条的方案。
变体——结合自定义进度条:
Scroll() {
Column() {
Text(this.longArticleContent)
.fontSize(16)
.lineHeight(28)
}
}
.scrollBar(BarState.Off) // 隐藏默认滚动条
.onScroll((x: number, y: number) => {
// 更新自定义顶部进度条
this.scrollProgress = y / this.maxScrollHeight;
})
8.3 相册 / 图片浏览器
典型页面:全屏图片浏览、照片网格
推荐策略:BarState.Off
理由:
- 全屏沉浸式场景,任何 UI 元素都是干扰;
- 用户通过左右/上下滑动自然会切换图片;
- 可用页码指示器或缩略图预览替代滚动条。
代码示例:
Scroll() {
Row() {
ForEach(this.images, (img: string) => {
Image(img)
.width('100vw')
.height('100%')
.objectFit(ImageFit.Contain)
})
}
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Horizontal)
8.4 数据报表 / 仪表盘
典型页面:数据大屏、交易记录列表
推荐策略:BarState.On
理由:
- 数据密集型页面,用户需要精确定位;
- On 模式的滚动条提供了持续的位置感知;
- 搭配 List 组件使用效果更佳。
8.5 设置页面 / 表单
典型页面:系统设置页、注册表单
推荐策略:BarState.Auto(或 Off 如果页面很短)
理由:
- 设置项通常在 10-20 个左右,滚动量不大;
- Auto 足以提供必要的滚动反馈;
- 如果页面很短(5-7 项以内),可考虑 Off。
8.6 决策速查表
| 应用类型 | 推荐策略 | 原因 |
|---|---|---|
| 聊天/IM | Auto | 频繁滑动,不干扰阅读 |
| 社交媒体 Feed | Auto | 标准和大多数用户预期一致 |
| 长文章阅读 | On 或 Auto + 顶部进度条 | 需要位置感知 |
| 相册/画廊 | Off | 全屏沉浸体验 |
| 电子书阅读器 | On | 阅读进度指示 |
| 商品列表 | Auto | 标准电商体验 |
| 数据报表 | On | 精确位置感知 |
| 系统设置 | Auto | 页面长度适中 |
| 代码编辑器 | On | 开发者需要精确位置 |
| 视频播放器控制层 | Off | 沉浸播放体验 |
| 表单填写 | Auto | 滚动量不定,按需显示即可 |
9. 性能优化与最佳实践
9.1 Scroll 性能瓶颈
虽然 Scroll 组件本身性能良好,但以下场景可能导致卡顿:
- 子组件过多(> 100 项):Scroll 会一次性构建所有子组件,DOM 节点过多导致渲染压力;
- 子组件布局复杂:每个列表项都包含嵌套容器、阴影、模糊效果、异步加载图片等;
- 频繁重排:
@State变量变化触发整棵子树重渲染; - 错误使用 Index 属性:ForEach 的 key 设置不当导致全部重建。
9.2 优化方案
9.2.1 大量数据改用 List
当内容项超过 50 时,优先使用 List 代替 Scroll:
List({ space: 8 }) {
ForEach(this.items, (item: string) => {
ListItem() {
// 列表项内容...
}
})
}
.listDirection(Axis.Vertical)
.scrollBar(BarState.Auto)
List 组件使用懒加载机制,只渲染视口内的可见项,内存占用大大降低。
9.2.2 使用 LazyForEach 替代 ForEach
对于非常大的数据集(> 1000 项),使用 LazyForEach:
class ItemModel {
constructor(public id: string, public name: string) {}
}
class MyDataSource extends BasicDataSource {
// 实现数据源接口...
}
Scroll() {
Column() {
LazyForEach(new MyDataSource(), (item: ItemModel) => {
Text(item.name)
.height(56)
})
}
}
.scrollBar(BarState.Auto)
9.2.3 控制阴影和透明度的作用范围
在 Scroll 的子组件中使用阴影和透明度时,要注意它们会触发离屏渲染:
// 优化前:每个列表项都有阴影
Row() {
// ...
}
.shadow({ radius: 4, color: 0x22000000 })
// 优化方案 1:减少阴影项的数量(只对首项使用)
// 优化方案 2:使用边框替代阴影(性能更好)
Row() {
// ...
}
.border({ width: 1, color: 0xFFE0E0E0 })
.borderRadius(8)
9.2.4 避免不必要的状态更新
// ❌ 不推荐:每次滚动都更新状态,触发频繁重渲染
Scroll() {
// ...
}
.onScroll((x: number, y: number) => {
this.scrollY = y; // 更新 @State 变量
})
// ✅ 推荐:对更新做节流处理
private lastUpdateTime: number = 0;
onScroll(y: number): void {
const now = Date.now();
if (now - this.lastUpdateTime > 100) { // 100ms 节流
this.lastUpdateTime = now;
this.scrollY = y;
}
}
9.2.5 使用常量代替方法调用
在 build() 中,如果某些值在组件生命周期内不会变化,使用常量或 readonly 变量,而不是每次都调用方法:
// ❌ 不推荐
Text(this.computeSomeExpensiveValue())
// ✅ 推荐
private readonly computedValue: string = this.computeSomeExpensiveValue();
build() {
Text(this.computedValue)
}
9.3 滚动条自身性能
.scrollBar() 本身对性能的影响可以忽略不计——它只是一个渲染标记,不涉及复杂计算。但请注意:
- BarState.On 模式比 Auto 多了一个"持续绘制轨道"的操作,但对现代 GPU 而言这点开销微不足道;
- BarState.Off 模式理论上渲染最轻量(少绘制两个矩形);
- 真正的性能瓶颈总是在"内容"而非"滚动条本身"。
9.4 最佳实践清单
- ✅ 始终显式调用
.scrollBar(),哪怕要用默认值 Auto——明确优于隐式; - ✅ 始终配合
.scrollable(ScrollDirection.Vertical)明确滚动方向——避免意外行为; - ✅ 超过 50 个子项时迁移到
List组件; - ✅ 超过 1000 个子项时使用
LazyForEach; - ✅ 对于需要固定滚动条的场景(On),考虑搭配
.edgeEffect(EdgeEffect.Spring)提升交互质感; - ✅ 在沉浸式场景中,Off 模式下确保有其他交互线索(如"向上滑动查看更多"的引导文字);
- ✅ 条件允许时,让用户可以自定义滚动条策略(提供设置选项);
- ❌ 不要在同一页面中混用不同 Scroll 容器的不同 BarState(除非刻意对比展示);
- ❌ 不要在 BarState.Off 模式下依赖滚动条作为唯一的位置指示器。
10. 常见问题与踩坑记录
10.1 编译错误:BarState 未导出
问题:
Module '"@kit.ArkUI"' has no exported member 'BarState'.
原因:在 API 23 及以下版本中,BarState 不在 @kit.ArkUI 的顶层导出中。
解决方案:
- 升级到 API 24(推荐);
- 或改用
import { BarState } from '@ohos.arkui.component'; - 或直接使用数值:
0(Auto)、1(On)、2(Off)。
// 兼容方案
import { BarState } from '@kit.ArkUI';
// 如果是 API 23,用数值代替
scrollBar(0) // Auto
scrollBar(1) // On
scrollBar(2) // Off
10.2 编译错误:计算属性名不支持
问题:
Objects with property names that are not identifiers are not supported.
原因:ArkTS 不支持类似 [BarState.Auto]: 'description' 的计算属性名语法。
解决方案:改用数组 + 索引,或 if/else 辅助函数:
// ❌ 不支持的写法
const labels: Record<BarState, string> = {
[BarState.Auto]: 'Auto...', // 编译错误!
[BarState.On]: 'On...',
[BarState.Off]: 'Off...',
};
// ✅ 支持的写法
const labels: string[] = [
'Auto...', // index 0
'On...', // index 1
'Off...' // index 2
];
10.3 运行时:滚动条不出现
问题:即使设为 Auto 或 On,滚动条也不显示。
原因排查:
- 内容未超出容器——如果子组件总高度 < Scroll 容器高度,无需滚动,滚动条自然不显示。检查是否设置了固定高度或
layoutWeight; - scrollable 设为 None——
.scrollable(ScrollDirection.None)禁止滚动,滚动条自然失效; - Scroll 容器尺寸异常——如果 Scroll 高度为 0 或极小,可能被挤压。检查父容器是否分配了足够空间;
- 模拟器/真机差异——某些低版本模拟器可能存在 bug。
调试方法:
Scroll() {
Column() {
// 添加一个超高的占位组件验证是否可滚动
Column()
.width('100%')
.height(2000) // 强制超高
.backgroundColor(Color.Blue)
}
}
.scrollBar(BarState.On) // 强制显示
// 如果此时能看到蓝色方块并可滚动,说明是内容量的问题
10.4 运行时:滚动条闪烁
问题:在切换 BarState 时,滚动条出现闪烁或位置跳动。
原因:@State 变量触发重建时,Scroll 组件内部状态被重置。
解决方案:
- 使用
@State+@Link或@Consume来保持状态一致性; - 尽量避免频繁切换 BarState;
- 如果必须切换,尝试在动画帧中切换(使用
animateTo):
onClick() {
animateTo({ duration: 200 }, () => {
this.currentStrategy = BarState.On;
});
}
10.5 与页面路由的交互问题
问题:从其他页面返回时,Scroll 组件的滚动位置被重置。
原因:默认情况下,离开页面时组件会被销毁,状态丢失。
解决方案:
- 使用页面级状态管理(如
@StorageLink)持久化滚动位置; - 或在
onPageHide中保存滚动偏移,在onPageShow中恢复。
@StorageLink('scrollOffset') savedScrollOffset: number = 0;
build() {
Scroll() {
// ...
}
.scrollBar(BarState.Auto)
.onScroll((x: number, y: number) => {
this.savedScrollOffset = y; // 实时保存
})
}
10.6 BarState.On 模式下内容不足仍显示滚动条
问题:内容很少(无需滚动)时,On 模式的滚动条轨道仍然显示。
原因:这是 BarState.On 的设计行为——轨道始终存在。
解决方案:运行时判断是否需要显示滚动条:
// 需要 Scroll 控制器 API(API 24+)
@State scrollable: boolean = true;
build() {
Scroll() {
// ...
}
.scrollBar(this.scrollable ? BarState.On : BarState.Off)
.onDidScroll(() => {
// 判断是否可滚动
this.scrollable = /* 根据内容高度和容器高度 */;
})
}
11. 总结与展望
11.1 本文核心要点回顾
在这篇总计近万字的博客中,我们从理论到实践,全面覆盖了 HarmonyOS NEXT 中 Scroll 滚动条控制的方方面面:
-
BarState 三种策略:
Auto(0)— 默认,按需显示,适用于大部分场景;On(1)— 始终显示,适用于需要持续位置感知;Off(2)— 始终隐藏,适用于沉浸式体验。
-
核心 API:
.scrollBar(BarState)+.scrollable(ScrollDirection)是标准的配套使用方式。 -
ArkTS 限制与应对:计算属性名、枚举导入、类型断言——本文提供了完整的 ArkTS 兼容写法。
-
实战选型:不同业务场景下的策略建议,以及何时改用 List。
-
性能优化:从子组件数量控制到状态更新节流,确保滚动流畅。
-
API 24 升级:统一的导入路径、增强的类型检查、新增的
edgeEffect支持。
11.2 从 Scroll 到更广阔的 ArkUI 世界
掌握了 Scroll 滚动条控制后,你可以进一步探索:
- List + scrollBar:高性能虚拟列表的滚动条控制;
- Grid + scrollBar:网格布局的滚动条控制;
- Swiper:轮播图组件,自带指示器;
- Scroll 嵌套场景:NestedScroll 机制处理多层滚动冲突;
- 自定义滚动指示器:在 Off 模式下用 Canvas 绘制自己的进度条。
每一个组件都遵循相似的 API 设计哲学——声明式、链式调用、状态驱动。这正是 HarmonyOS NEXT 的优雅之处:学会一个,触类旁通。
11.3 写给读者的寄语
作为开发者,我们常常沉迷于"实现功能"而忽略了细节体验。滚动条虽然微小,但它直接影响用户对应用的"质感"认知——就像一个房门的手柄,很少有人会专门注意它,但如果它松了、歪了、或者没有,人们立刻会感到不对劲。
在 HarmonyOS NEXT 时代,华为将 UI 控制权完全交给了开发者,从 BarState 这样的枚举设计中就能看出:系统提供能力,开发者决定体验。
希望通过这篇博客,你不仅学会了 .scrollBar() 的用法,更领会了 ArkTS 声明式 UI 的核心理念。现在,回到你的项目,去看看那些滚动条——它们的状态是对的吗?
附录 A:完整代码清单
A.1 ScrollBarDemo.ets(API 24 版本)
/**
* ScrollBarDemo — 鸿蒙原生 ArkTS 布局:Scroll 滚动条控制(show / hide / auto)
* 版本: HarmonyOS NEXT API 24
*/
import { BarState } from '@kit.ArkUI';
@Entry
@Component
struct ScrollBarDemo {
@State currentStrategy: BarState = BarState.Auto;
private readonly strategyLabels: string[] = [
'Auto(自动显示)',
'On(始终显示)',
'Off(始终隐藏)'
];
private readonly strategyDescs: string[] = [
'滚动时显示滚动条,手指/鼠标停止操作后自动淡出隐藏。'
+ '这是系统默认行为,兼顾视觉简洁与操作指引。',
'滚动条始终占据空间并保持可见,'
+ '用户可随时感知可滚动区域的位置与范围。'
+ '适用于需要明确指示可滚动内容的场景。',
'完全隐藏滚动条,界面更干净整洁。'
+ '常用于自定义滚动指示器或内容本身已暗示可滚动的场景。'
];
private readonly strategyColors: ResourceColor[] = [
Color.Green,
Color.Orange,
Color.Gray
];
private readonly strategyValues: BarState[] = [
BarState.Auto,
BarState.On,
BarState.Off
];
private readonly items: string[] = this.buildItems(30);
buildItems(count: number): string[] {
const result: string[] = [];
for (let i: number = 0; i < count; i++) {
result.push(`第 ${i + 1} 项内容`);
}
return result;
}
build() {
Column({ space: 12 }) {
// 标题
Text('Scroll 滚动条控制:show / hide / auto')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 16, bottom: 4 })
Text('点击下方按钮切换 .scrollBar() 策略')
.fontSize(14)
.fontColor(Color.Gray)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ bottom: 8 })
// 策略切换按钮组
Row({ space: 8 }) {
ForEach(
[0, 1, 2] as number[],
(index: number) => {
Button(this.strategyLabels[index])
.height(40)
.fontSize(13)
.layoutWeight(1)
.backgroundColor(
this.currentStrategy === this.strategyValues[index]
? this.strategyColors[index]
: Color.Gray
)
.onClick(() => {
this.currentStrategy = this.strategyValues[index];
})
}
)
}
.width('100%')
.padding({ left: 12, right: 12 })
// 策略说明
Text(this.getStrategyDesc())
.fontSize(13)
.fontColor(Color.Gray)
.width('100%')
.padding({ left: 12, right: 12 })
// 当前策略标签
Text(this.getStrategyLabel())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.getStrategyColor())
.width('100%')
.textAlign(TextAlign.Center)
// 核心滚动区域
Scroll() {
Column({ space: 8 }) {
ForEach(this.items, (item: string, index?: number) => {
Row() {
Circle()
.width(36).height(36)
.fill(this.getStrategyColor())
.opacity(0.7)
Text(`${(index ?? 0) + 1}`)
.fontColor(Color.White).fontSize(14)
.width(36).height(36)
.textAlign(TextAlign.Center)
Text(item).fontSize(16).margin({ left: 12 })
}
.width('100%').height(56)
.padding({ left: 12, right: 12 })
.borderRadius(8)
.backgroundColor(Color.White)
.shadow({ radius: 4, color: 0x22000000, offsetY: 2 })
})
}
.width('100%').padding(12)
}
.layoutWeight(1).width('100%')
.borderRadius(12).backgroundColor(0xFFF5F5F5)
.padding(4)
.scrollBar(this.currentStrategy)
.scrollable(ScrollDirection.Vertical)
.edgeEffect(EdgeEffect.Spring) // API 24 新特性
// 底部 API 提示
Text(this.getApiDisplayText())
.fontSize(14).fontColor(0xFF666666)
.fontFamily('Courier New')
.width('100%').textAlign(TextAlign.Center)
.padding({ bottom: 16 })
}
.width('100%').height('100%')
.backgroundColor(0xFFF0F0F0)
}
// 辅助方法
getStrategyLabel(): string {
if (this.currentStrategy === BarState.Auto) {
return '当前策略:Auto(自动显示)';
} else if (this.currentStrategy === BarState.On) {
return '当前策略:On(始终显示)';
}
return '当前策略:Off(始终隐藏)';
}
getStrategyDesc(): string {
if (this.currentStrategy === BarState.Auto) return this.strategyDescs[0];
if (this.currentStrategy === BarState.On) return this.strategyDescs[1];
return this.strategyDescs[2];
}
getStrategyColor(): ResourceColor {
if (this.currentStrategy === BarState.Auto) return Color.Green;
if (this.currentStrategy === BarState.On) return Color.Orange;
return Color.Gray;
}
getApiDisplayText(): string {
if (this.currentStrategy === BarState.Auto) return '.scrollBar(BarState.Auto)';
if (this.currentStrategy === BarState.On) return '.scrollBar(BarState.On)';
return '.scrollBar(BarState.Off)';
}
}
A.2 项目结构
ap13/
├── AppScope/ # 应用级配置
├── entry/ # HAP 模块
│ └── src/main/ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # Ability 入口(加载 ScrollBarDemo)
│ └── pages/
│ ├── Index.ets # 原始首页
│ └── ScrollBarDemo.ets# 滚动条控制演示页面
├── build-profile.json5 # 编译配置(targetSdkVersion: 24)
└── hvigor/ # 构建工具配置
附录 B:BarState 相关 API 速查表
| API | 类型 | 说明 | 版本 |
|---|---|---|---|
.scrollBar(BarState) |
方法 | 设置滚动条显示策略 | API 10+ |
BarState.Auto |
枚举值 (0) | 按需显示 | API 10+ |
BarState.On |
枚举值 (1) | 始终显示 | API 10+ |
BarState.Off |
枚举值 (2) | 始终隐藏 | API 10+ |
.scrollable(ScrollDirection) |
方法 | 设置滚动方向 | API 10+ |
.edgeEffect(EdgeEffect) |
方法 | 设置边缘回弹效果 | API 24+ |
.onScroll(callback) |
事件 | 滚动事件回调 | API 10+ |
.onScrollStart(callback) |
事件 | 滚动开始回调 | API 10+ |
.onScrollStop(callback) |
事件 | 滚动停止回调 | API 10+ |
.onDidScroll(callback) |
事件 | 滚动位置已更新回调 | API 12+ |
本文档配套项目: https://github.com/example/scrollbar-demo
免责声明: 示例代码基于 HarmonyOS NEXT API 24 编写,不排除后续 SDK 更新导致 API 行为变化。请以官方文档为准。许可: 本文采用 CC BY-NC 4.0 协议,转载需注明出处。
更多推荐


所有评论(0)