鸿蒙原生 ArkTS 布局精讲:Scroll 方向控制(垂直 / 水平 / 双向)


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

一、引言

在移动端应用开发中,滚动(Scroll) 是最基础也最频繁使用的交互方式之一。无论是刷不完的朋友圈、横向滑动的商品推荐栏,还是横纵双向自由探索的图片墙,其底层都离不开 Scroll 容器的支持。

HarmonyOS NEXT 自 API 24 起,ArkUI 框架的 Scroll 组件日趋成熟,在滚动性能、手势流畅度、嵌套协调等方面均有显著提升。然而在实际开发中,很多开发者对 Scroll 的方向控制仍然存在认知盲区:

  • 如何正确设置滚动方向?
  • 为什么有时内容溢出了却无法滚动?
  • ScrollDirection.VerticalHorizontalFREE 有什么区别?
  • 双向滚动场景下,子组件的宽高应该如何设置?

本文将以一个完整的示例应用为主线,手把手带你吃透 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')

要点说明

  1. 外层 Scrollheight 设为 300vp,作为「视口」高度。
  2. 内层 Columnheight 设为 2000vp,远超视口高度,产生溢出。
  3. 通过 .scrollable(ScrollDirection.Vertical) 显式声明方向。
  4. 垂直滚动是 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')

要点说明

  1. 使用 Row 作为子容器,所有色块水平排列。
  2. Rowwidth 设为 4000vp,而 Scroll 容器宽度为父容器宽,产生水平溢出。
  3. 注意 Row 不需要显式设置宽度约束,让子元素内容撑开即可。
  4. .scrollable(ScrollDirection.Horizontal) 声明水平方向。

视觉表现:20 个色块排成一行,用户左右拖动,滚动条在底部浮现。

真实场景映射:淘宝/京东顶部的分类导航栏、音乐 App 的推荐歌曲横滑区域、股票 App 的 K 线时间轴。

3.4 双向(自由)滚动(ScrollDirection.FREE)

原理:当子组件的 widthheight 同时超过 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')

要点说明

  1. 使用 Flex + FlexWrap.Wrap 实现自动换行的网格布局,同时产生水平和垂直方向的溢出。
  2. Flexwidth(2000)height(2000) 均远超 Scroll 容器尺寸,这是触发双向滚动的必要条件
  3. 40 个色块以 4 行 × 10 列的网格排列,行满自动换行。
  4. .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 大,但拖不动。

原因:有两种可能——

  1. 方向设置错误:期望水平滚动但忘了写 .scrollable(ScrollDirection.Horizontal),默认垂直方向自然无法水平拖动。
  2. 子组件尺寸没有真正超出:有时子组件设置了 width('100%') 但父容器宽度等于屏幕宽,导致没有溢出。

解决:水平滚动确认内层子组件 width 超过 Scroll 宽度;双向滚动确认 widthheight 超过。

❌ 问题 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 均超出

避坑清单

  1. ✅ 方法名是 scrollable(),不是 scrollDirection()
  2. ✅ 双向滚动用 ScrollDirection.FREE,不是 Both
  3. ✅ Scroller 全局可用,无需 import
  4. ✅ Tabs 的 onChange 用链式,不用构造参数
  5. ✅ 子容器宽/高必须超过 Scroll 容器对应尺寸
  6. ✅ 每个 Scroll 绑定独立的 Scroller 实例

七、进阶方向

掌握了 Scroll 方向控制之后,可以进一步探索:

  • Scroll + LazyForEach:百万级长列表的懒加载渲染
  • Scroll + Grid:构建可横向滚动的网格布局
  • 嵌套滚动:通过 nestedScroll 属性协调父子滚动组件的联动
  • 自定义下拉刷新:基于 onWillScroll / onDidScroll 事件实现
  • 滚动条美化:通过 scrollBarColorscrollBarWidth 调优视觉风格

八、参考资料


本文所有代码已在 HarmonyOS NEXT API 24 环境下编译通过。如果你在实践过程中遇到问题,欢迎留言交流。

Logo

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

更多推荐