HarmonyOS NEXT Scroll 滚动条控制实战 — show / hide / auto 三种策略深度解析


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

目录

  1. 引言:为什么滚动条控制如此重要
  2. HarmonyOS NEXT 与 ArkTS 概述
  3. Scroll 组件深度剖析
  4. BarState 枚举:滚动条的三种灵魂状态
  5. scrollBar() API 详解
  6. 完整示例:ScrollBarDemo 逐段解读
  7. API 24 新特性与迁移指南
  8. 实战场景:不同业务下的滚动条策略选型
  9. 性能优化与最佳实践
  10. 常见问题与踩坑记录
  11. 总结与展望

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

参数

  • barStateBarState 枚举值,指定滚动条的显示策略。

返回值

  • 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(始终隐藏)'
  ];
  // ... 更多状态
}

设计要点

  1. @Entry — 标记该组件为页面入口,可以被路由导航到;
  2. @Component — 声明一个自定义组件;
  3. @State currentStrategy — 状态变量,当其值变化时触发 UI 自动重渲染;
  4. readonly + 数组 — 因为 ArkTS 不支持计算属性名作为对象 key,我们用数组下标索引替代 Record 类型;
  5. 常量定义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 })

交互逻辑

  1. 使用 ForEach 遍历 [0, 1, 2](代表三个策略的索引),动态生成三个按钮;
  2. layoutWeight(1) 让三个按钮在 Row 中等宽分布;
  3. 当前选中的策略对应的按钮高亮为特定颜色(Auto→绿, On→橙, Off→灰),其余按钮为灰色;
  4. 点击事件更新 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 })

这里通过三个辅助函数 getStrategyDescgetStrategyLabelgetStrategyColor 将策略值和展示内容解耦——这是 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)

架构亮点

  1. layoutWeight(1) — Scroll 组件占据 Column 中所有剩余空间,确保内容区域最大化;
  2. 序号 Circle + Text — 由于 .overlay() 在 ArkTS 中参数类型限制,我们将圆和文字分别放置,通过约束尺寸对齐;
  3. 卡片式列表项 — 白色背景、圆角(8)、阴影,每个列表项看起来像独立的卡片,视觉层次分明;
  4. 动态颜色 — 圆的填充色随策略切换而变化,给用户额外的视觉反馈;
  5. .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 组件本身性能良好,但以下场景可能导致卡顿:

  1. 子组件过多(> 100 项):Scroll 会一次性构建所有子组件,DOM 节点过多导致渲染压力;
  2. 子组件布局复杂:每个列表项都包含嵌套容器、阴影、模糊效果、异步加载图片等;
  3. 频繁重排@State 变量变化触发整棵子树重渲染;
  4. 错误使用 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 最佳实践清单

  1. ✅ 始终显式调用 .scrollBar(),哪怕要用默认值 Auto——明确优于隐式
  2. ✅ 始终配合 .scrollable(ScrollDirection.Vertical) 明确滚动方向——避免意外行为
  3. ✅ 超过 50 个子项时迁移到 List 组件;
  4. ✅ 超过 1000 个子项时使用 LazyForEach
  5. ✅ 对于需要固定滚动条的场景(On),考虑搭配 .edgeEffect(EdgeEffect.Spring) 提升交互质感;
  6. ✅ 在沉浸式场景中,Off 模式下确保有其他交互线索(如"向上滑动查看更多"的引导文字);
  7. ✅ 条件允许时,让用户可以自定义滚动条策略(提供设置选项);
  8. ❌ 不要在同一页面中混用不同 Scroll 容器的不同 BarState(除非刻意对比展示);
  9. ❌ 不要在 BarState.Off 模式下依赖滚动条作为唯一的位置指示器。

10. 常见问题与踩坑记录

10.1 编译错误:BarState 未导出

问题

Module '"@kit.ArkUI"' has no exported member 'BarState'.

原因:在 API 23 及以下版本中,BarState 不在 @kit.ArkUI 的顶层导出中。

解决方案

  1. 升级到 API 24(推荐);
  2. 或改用 import { BarState } from '@ohos.arkui.component'
  3. 或直接使用数值: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,滚动条也不显示。

原因排查

  1. 内容未超出容器——如果子组件总高度 < Scroll 容器高度,无需滚动,滚动条自然不显示。检查是否设置了固定高度或 layoutWeight
  2. scrollable 设为 None——.scrollable(ScrollDirection.None) 禁止滚动,滚动条自然失效;
  3. Scroll 容器尺寸异常——如果 Scroll 高度为 0 或极小,可能被挤压。检查父容器是否分配了足够空间;
  4. 模拟器/真机差异——某些低版本模拟器可能存在 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 滚动条控制的方方面面:

  1. BarState 三种策略

    • Auto(0)— 默认,按需显示,适用于大部分场景;
    • On(1)— 始终显示,适用于需要持续位置感知;
    • Off(2)— 始终隐藏,适用于沉浸式体验。
  2. 核心 API.scrollBar(BarState) + .scrollable(ScrollDirection) 是标准的配套使用方式。

  3. ArkTS 限制与应对:计算属性名、枚举导入、类型断言——本文提供了完整的 ArkTS 兼容写法。

  4. 实战选型:不同业务场景下的策略建议,以及何时改用 List。

  5. 性能优化:从子组件数量控制到状态更新节流,确保滚动流畅。

  6. 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 协议,转载需注明出处。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐