引言

滚动条(ScrollBar)是移动应用中最不起眼但也最不可或缺的 UI 元素之一。它默默地位于屏幕边缘,告诉用户"你在这里"和"还有更多内容"。大多数开发者使用系统默认的滚动条从未想过定制,但当你构建一个阅读器、一个长列表页面、或者一个追求品牌一致性的产品时,对滚动条的精细控制就变得至关重要。

HarmonyOS NEXT 的 ArkUI 为 Scroll 组件提供了完整的滚动条定制能力——从显隐控制到颜色宽度,从滚动事件追踪到编程式跳转。本文将通过构建一个功能完整的"文档阅读器"Demo,全面覆盖这些能力在实际项目中的应用。

读完本文你将能够:

  • 掌握 BarState 三态显隐控制和适用场景
  • 使用 scrollBarColor()scrollBarWidth() 定制视觉样式
  • 通过 Scroller 实现编程式滚动跳转
  • 利用 .onScroll() 事件构建阅读进度追踪系统
  • 实现章节自动定位和双向联动导航

BarState:滚动条的三种存在方式

BarState 是控制滚动条显隐行为的枚举,定义在 ArkUI 的 Scroll 组件中。它有三种取值:

BarState.On — 始终可见

Scroll() { ... }
  .scrollBar(BarState.On)

滚动条始终显示,不论用户是否正在滚动。这种模式适合:

  • 内容较长的文档/文章页面,让用户随时感知位置
  • 数据表格等需要精确导航的场景
  • 桌面/平板等大屏设备,滚动条不显得突兀

本文的"文档阅读器"Demo 默认使用此模式。

BarState.Off — 始终隐藏

Scroll() { ... }
  .scrollBar(BarState.Off)

滚动条完全不可见。适合:

  • 沉浸式阅读体验(电子书、长文阅读)
  • 全屏图片/视频浏览
  • 自定义滚动指示器(用顶部的迷你进度条替代侧边滚动条)

BarState.Auto — 自动显隐(默认)

Scroll() { ... }
  .scrollBar(BarState.Auto)

这是系统的默认行为:滚动时短暂显示滚动条,停止滚动后自动淡出隐藏。适合大多数常规场景。

动态切换

本文 Demo 通过底部 Toggle 开关让用户在 BarState.OnBarState.Off 之间自由切换:

@State showSystemBar: boolean = true;

getBarState(): BarState {
  return this.showSystemBar ? BarState.On : BarState.Off;
}

// UI 中
Toggle({ type: ToggleType.Switch, isOn: this.showSystemBar })
  .onChange((val: boolean) => { this.showSystemBar = val; })

这个交互让开发者可以直观对比两种模式的差异:On 模式下,右侧蓝色细线随时可见;Off 模式下,界面更干净,但需要依赖顶部的阅读进度条来感知位置。

自定义滚动条样式

Beyond visibility control, ArkUI provides two style customization attributes:

scrollBarColor — 自定义颜色

Scroll() { ... }
  .scrollBarColor('#1677FF')

默认的系统滚动条通常是半透明灰色。通过 scrollBarColor(),你可以将滚动条颜色与应用的品牌色统一。本文 Demo 使用了蓝色(#1677FF),与顶部进度条和章节高亮按钮形成色彩一致。

颜色选择建议:

  • 浅色背景:使用品牌的中间色调或 #CCCCDD 系列中性灰
  • 深色背景:使用 #FFFFFF44 半透明白色
  • 彩色背景:使用对比色或高明度的同色系

scrollBarWidth — 自定义宽度

Scroll() { ... }
  .scrollBarWidth(4)

默认滚动条宽度约 2-3vp。增加宽度到 4-6vp 可以让滚动条更容易被注意,尤其在内容密集的页面中。本文 Demo 使用 4vp 宽度,在"显眼"和"不突兀"之间找到平衡。

宽度设计考量:

  • 2-3vp:极简风格,适合现代设计语言
  • 4-5vp:标准宽度,可读性和美观兼顾
  • 6-8vp:强调型,适合老年人模式或触屏精度要求高的场景

Scroller:编程式滚动控制

Scroller 是 HarmonyOS 中实现代码控制滚动的核心对象。通过创建实例并绑定到 Scroll 组件,开发者可以精确控制滚动行为。

创建和绑定

private scroller: Scroller = new Scroller();

Scroll(this.scroller) { ... }

scrollTo — 跳转到指定位置

this.scroller.scrollTo({ xOffset: 0, yOffset: targetY });

ScrollOptions 要求同时提供 xOffsetyOffset——这是 ArkTS 类型系统的严格性体现。在本文 Demo 中,章节跳转按钮通过此方法将页面滚动到对应章节的锚点坐标:

scrollToChapter(idx: number): void {
  this.scroller.scrollTo({
    xOffset: 0,
    yOffset: this.chapters[idx].anchorY
  });
}

每个章节预设一个锚点 Y 坐标(概述=0, 入门=450, 进阶=900, 实践=1350, 总结=1800),点击按钮即刻跳转。

其他 Scroller 方法

  • scrollEdge(Edge.Top) — 滚动到顶部
  • scrollEdge(Edge.Bottom) — 滚动到底部
  • scrollPage({ next: true }) — 向下翻一页
  • scrollPage({ next: false }) — 向上翻一页

onScroll:实时滚动追踪

.onScroll() 是 Scroll 组件的事件回调,每次滚动位置变化时触发:

Scroll(this.scroller) { ... }
  .onScroll((xOffset: number, yOffset: number) => {
    this.scrollY = yOffset;
    // 计算阅读进度
    let maxScroll = this.totalHeight - viewportHeight;
    this.progress = Math.min((yOffset / maxScroll) * 100, 100);
    // 更新当前章节
    this.updateActiveChapter(yOffset);
  })

在本文 Demo 中,.onScroll() 承担了三个任务:

1. 更新阅读进度条

进度条是一个位于标题栏下方的细线,宽度百分比随滚动位置变化:

Row()
  .width(this.progress.toString().concat('%'))
  .height(3)
  .backgroundColor('#1677FF')

进度计算核心公式:

progress = scrollY / (totalHeight - viewportHeight) × 100

其中 totalHeight - viewportHeight 是真正可滚动的范围。当 scrollY = 0 时进度为 0%,当 scrollY 达到最大值时进度为 100%。

2. 更新底部状态信息

底部状态栏实时显示当前滚动像素位置和阅读百分比:

Text(this.scrollY.toString().concat(' px'))
Text(this.getProgressPercent().toString().concat('%'))

在这里插入图片描述
在这里插入图片描述

3. 自动定位当前章节

通过比较当前 scrollY 与各章节锚点,确定用户正在阅读的章节:

// onScroll 回调中
let newCh = 0;
for (let i = this.chapters.length - 1; i >= 0; i--) {
  if (yOffset >= this.chapters[i].anchorY) {
    newCh = i;
    break;
  }
}
if (newCh !== this.activeChapter) {
  this.activeChapter = newCh;
}

算法从最后一个章节开始倒序查找(效率更高,因为用户通常从前往后读,越后面的章节越早匹配到),找到第一个锚点 ≤ 当前 scrollY 的章节即为当前章节。检测到章节变化时才更新 activeChapter,避免不必要的 UI 重绘。

章节数据结构与锚点系统

class Chapter {
  title: string;
  anchorY: number;

  constructor(title: string, anchorY: number) {
    this.title = title;
    this.anchorY = anchorY;
  }
}

private chapters: Chapter[] = [
  new Chapter('概述', 0),
  new Chapter('入门', 450),
  new Chapter('进阶', 900),
  new Chapter('实践', 1350),
  new Chapter('总结', 1800),
];

锚点坐标是手动估算的——每个章节约 450vp 的内容高度。在生产项目中,可以通过 .onAreaChange() 事件动态获取每个章节标题元素的实际 Y 坐标,实现更精准的锚点定位。

双向联动导航

本文 Demo 实现了一种优雅的双向导航模式:

正向(手动滚动 → 自动高亮):用户用手指滚动正文 → onScroll 检测 scrollY 变化 → 更新 activeChapter → 顶部章节按钮自动高亮切换。

反向(点击按钮 → 编程跳转):用户点击章节按钮 → scrollToChapter(idx) 调用 scroller.scrollTo() → 页面滚动到对应锚点 → onScroll 再次触发 → 章节高亮更新(确认一致性)。

这种双向联动创造了一种"无论怎么操作,导航状态始终正确"的可靠体验。

完整页面结构

Column
├── Header(标题 + 阅读百分比徽章)
├── Progress Bar(3vp 高度蓝色线条)
├── Chapter Nav Bar(水平排列的 5 个章节按钮)
├── Scroll(正文内容区,绑定 scroller + onScroll)
│   ├── 概述章节(标题 + 两段正文)
│   ├── 入门章节(两个小节)
│   ├── 进阶章节(两个小节)
│   ├── 实践章节(两个小节)
│   └── 总结章节(一段正文)
└── Bottom Bar(滚动位置 + 进度 + 滚动条 Toggle)

常见问题与解决方案

问题一:scrollTo 参数不完整

报错Property 'xOffset' is missing in type '{ yOffset: number; }'

原因ScrollOptions 接口要求同时提供 xOffsetyOffset

解决:同时传递两个参数,水平滚动不需要时传 0。

// 错误
this.scroller.scrollTo({ yOffset: 500 });

// 正确
this.scroller.scrollTo({ xOffset: 0, yOffset: 500 });

问题二:进度计算不准确

原因:直接用 scrollY / totalHeight 计算进度,没考虑可视区域。

解决:使用 scrollY / (totalHeight - viewportHeight) 并加 Math.min(..., 100) 防溢出。

问题三:onScroll 触发过于频繁

现象:滚动时 onScroll 以 60fps+ 的频率触发,如果回调中有复杂计算会影响性能。

解决方案:onScroll 回调中只做轻量级的状态赋值和简单数学运算,避免在回调中触发额外的 animateTo 或复杂遍历。

总结

本文通过构建一个"文档阅读器"Demo,完整覆盖了 HarmonyOS ScrollBar 定制的五个核心维度:

  1. BarState 显隐控制:On / Off / Auto 三种模式及适用场景
  2. 视觉样式定制scrollBarColor()scrollBarWidth() 实现品牌一致性
  3. Scroller 编程跳转scrollTo() 实现章节导航
  4. onScroll 事件追踪:实时计算阅读进度和章节定位
  5. 双向联动导航:手动滚动与编程跳转的协调配合

这些技术组合起来,可以应用于电子书阅读器、新闻客户端、技术文档站、帮助中心等多种实际产品场景。在掌握了本文的基础定制能力后,你还可以进一步探索:自定义滚动条形状(通过完全隐藏系统滚动条 + 自绘指示器)、基于物理参数的惯性滚动调节、以及结合动画系统的平滑滚动过渡效果。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐