Column 与 Scroll 联动:可滚动的纵向列表 —— HarmonyOS NEXT 原生 ArkTS 布局深度教程

SDK 版本 :HarmonyOS NEXT 6.1.1(API 24)

核心组件 :Scroll · Column · ForEach · Scroller

布局范式:固定头 + 滚动体 + 固定尾(三明治结构)

一、引言

在移动端应用开发中,可滚动的纵向列表是最常见、最核心的交互模式之一。无论是社交媒体 feed、电商商品列表、系统设置页、新闻资讯流,还是聊天记录、操作日志,几乎所有内容型页面都依赖「滚动」来完成信息承载。

鸿蒙 ArkTS 框架为开发者提供了两套并行的滚动方案:

方案 适用场景 特点
Scroll + Column 内容类型单一、条目数量可控的页面 布局直观、控制精细、全量渲染
List 大量数据、无限列表、性能敏感 按需渲染(LazyForEach)、自带回收

本教程聚焦于 Scroll + Column 的组合方案。虽然 List 在超长列表场景下性能更优,但 Scroll + Column 的布局灵活性远超 List------它允许在滚动区域内自由混排卡片、图表、按钮、表单等异形组件,而不会受到 List 子项结构一致性的约束。

我们将从一个完整的实战项目出发,深入剖析 Scroll 组件的每一个配置项、Scroll 与 Column 之间的协作机制、Scroller 对象的编程式控制,以及状态驱动的滚动联动逻辑。读完本文,你将掌握在 HarmonyOS NEXT 上构建任意可滚动页面的能力。


二、布局架构概述

2.1 三明治布局模式

我们的示例页面采用了经典的三明治布局:

复制代码
┌─────────────────────────────────────┐
│        固定顶栏(标题 + 返回)        │  ← 永不滚动
├─────────────────────────────────────┤
│                                     │
│         Scroll 滚动区域              │
│  ┌─ 原理说明卡 ──────────────────┐  │
│  │ 滚动方向 │ 位置 │ 总高度       │  │  ← 实时状态
│  └────────────────────────────────┘  │
│  ┌─ 滚动配置项演示 ──────────────┐  │
│  │ scrollable · edgeEffect ·... │  │  ← 配置说明
│  └────────────────────────────────┘  │
│  ┌─ 系统运行日志 ────────────────┐  │
│  │ 15+ 条日志 + 实时追加          │  │  ← 溢出触发滚动
│  └────────────────────────────────┘  │
│  ┌─ 数据刷新演示 ────────────────┐  │
│  │ 追加 + 刷新计数                │  │  ← 动态数据
│  └────────────────────────────────┘  │
│  ┌─ 使用注意事项 ────────────────┐  │
│  │ 6 条避坑要点                   │  │
│  └────────────────────────────────┘  │
│                                     │
├─────────────────────────────────────┤
│        固定底栏(滚动控制按钮)       │  ← 永不滚动
└─────────────────────────────────────┘

2.2 为什么选择三明治结构?

这种布局模式的核心优势在于:

  1. 关键操作常驻:顶栏的标题、底栏的控制按钮不随内容滚动,用户在任何位置都能快速操作
  2. 内容区最大化:Scroll 区域通过 layoutWeight(1) 撑满剩余空间,动态适应不同屏幕尺寸
  3. 视觉层次清晰:固定层与滚动层的分离,让用户直觉地理解哪些内容可滚动、哪些不可滚动

2.3 组件层次与职责

复制代码
Column(全屏骨架,固定)
├── Column(固定顶栏:标题)          ← 第1层:固定
├── Divider(分割线)
├── Scroll(可滚动区域)             ← 第2层:滚动
│   └── Column(内部容器)            ← 第3层:内容
│       ├── Column(原理说明卡)
│       ├── Divider
│       ├── Column(配置项列表)
│       ├── Divider
│       ├── Column(日志列表)
│       ├── Divider
│       ├── Column(刷新演示)
│       ├── Divider
│       ├── Column(注意事项)
│       └── Column(底部占位)
├── Divider(分割线)
└── Column(固定底栏:按钮)          ← 第4层:固定

每一层各司其职:最外层的 Column 提供全屏背景与主轴方向;Scroll 提供滚动机制;Scroll 内部的 Column 作为内容容器,自然撑高触发滚动;上下两个固定 Column 提供常驻 UI。


三、核心组件详解

3.1 Scroll 组件

Scroll 是 ArkTS 中用于包裹可滚动内容的容器组件。它的核心职责是:当子组件的内容尺寸超出自身尺寸时,通过滚动的方式让用户能够查看被隐藏的部分。

3.1.1 基本用法
typescript 复制代码
// 最简形式 ------ 创建一个垂直滚动的容器
Scroll() {
  Column() {
    // 任意数量的子组件
    Text('内容1')
    Text('内容2')
    // ... 更多内容
  }
}
.width('100%')
.layoutWeight(1)

一定要记住的重要规则

  • Scroll 只能有一个直接子组件。这意味着你需要用一个 Column(或 Flex、Row 等布局容器)将所有内容包裹起来
  • Scroll 内部的 Column 不要设置固定高度,否则 Scroll 无法正确计算滚动区域,内容溢出时不会滚动
  • Scroll 自身必须有高度约束,否则它会自由扩展而不会产生滚动
3.1.2 全部 API 配置项
API 类型 默认值 说明
.scrollable() ScrollDirection Vertical 滚动方向:Vertical / Horizontal / Both / None
.edgeEffect() EdgeEffect Spring 边缘效果:Spring 弹簧回弹 / Fade 渐变消失
.scrollBar() BarState Auto 滚动条状态:Auto 按需显隐 / On 始终显示 / Off 始终隐藏
.onScroll() callback --- 滚动中回调(已废弃,推荐 onDidScroll)
.onDidScroll() callback --- 滚动中回调,返回当前 x/y 偏移量
.onScrollStart() callback --- 滚动开始时触发
.onScrollStop() callback --- 滚动停止时触发
.onScrollEdge() callback --- 滚动到边缘时触发
3.1.3 滚动方向控制
typescript 复制代码
Scroll() {
  // 纵向内容
}
.scrollable(ScrollDirection.Vertical)  // 仅垂直滚动

ScrollDirection 枚举有 4 个值:

  • Vertical(默认):仅垂直方向滚动,最常用
  • Horizontal:仅水平方向滚动,适用于横向轮播
  • None:禁用滚动,内容溢出时裁剪
  • Both:双向滚动,适用于地图或超大画布

使用建议 :绝大多数列表场景使用 Vertical;如果是轮播图或横向标签栏使用 HorizontalBoth 场景极少,仅在自定义画布等特殊需求时使用。

3.1.4 边缘效果(EdgeEffect)
typescript 复制代码
Scroll() { ... }
.edgeEffect(EdgeEffect.Spring)  // 弹簧回弹效果

EdgeEffect 有两个值:

  • Spring(默认):当用户拉到内容边界之外时产生弹性反馈,松手后内容回弹到边界位置。这种效果给用户一种「摸得到边」的自然感觉,推荐用于大多数场景
  • Fade:在内容末尾处产生渐变淡出效果,视觉上像是内容「隐入」边界

实际体验差异:Spring 效果更符合直觉,用户能感知到「已经到尽头了」;Fade 效果更平滑但反馈较弱。推荐首选 Spring。

3.1.5 滚动条控制
typescript 复制代码
Scroll() { ... }
.scrollBar(BarState.Auto)  // 滚动条自动显隐

BarState 有三个值:

  • Auto(默认):内容可滚动时显示滚动条,静止 2 秒后自动隐藏;拖动时重新显示
  • On:滚动条始终显示,占据 4dp 宽度
  • Off:完全隐藏滚动条,用户仍可通过手势滚动

使用建议:大多数场景用 Auto 即可;如果需要类似 iOS 的「无干扰滚动体验」,可设置为 Off;On 适合数据表格等需要精确定位的场景。

3.1.6 滚动事件回调

Scroll 提供了三个重要的事件回调:

typescript 复制代码
Scroll() { ... }
.onDidScroll((xOffset: number, yOffset: number) => {
  // 【高频回调】滚动正在发生
  // xOffset: 水平方向已滚动的像素值
  // yOffset: 垂直方向已滚动的像素值(正值 = 向下)
  // 作用:实时更新 UI,如显示/隐藏回到顶部按钮
  if (yOffset > 200) {
    // 显示回到顶部按钮
  }
})
.onScrollStart(() => {
  // 【一次】滚动开始时触发
  // 作用:记录滚动开始状态
})
.onScrollStop(() => {
  // 【一次】滚动停止时触发
  // 作用:触发懒加载或数据埋点
})

性能注意onDidScroll 是高频回调(每帧调用多次),回调中应避免执行耗时操作或触发 UI 频繁刷新。如果需要更新 @State 变量,确保条件判断足够精细,不要每帧都 setState。

3.2 Scroller 对象

Scroller 是 Scroll 的「遥控器」,通过它可以在不依赖用户手势的情况下控制滚动位置。

3.2.1 创建 Scroller
typescript 复制代码
// 方式1:最常用 ------ 在组件中声明
@State
scroller: Scroller = new Scroller();

// 方式2:全局共享 ------ 仅在极端特殊场景下使用
// 实际项目中绝大多数场景用方式1即可
3.2.2 关联到 Scroll
typescript 复制代码
Scroll(this.scroller) {  // 将 scroller 传入 Scroll 构造函数
  // ...
}
3.2.3 编程式滚动 API

Scroller 提供三个核心方法:

scrollTo() ------ 滚动到指定位置

typescript 复制代码
scroller.scrollTo({
  xOffset: 0,         // 水平目标位置(像素)
  yOffset: 500,       // 垂直目标位置(像素)
  animation: {         // 可选:动画参数
    duration: 400,     // 动画时长(毫秒)
    curve: Curve.EaseInOut  // 动画曲线
  }
});

scrollEdge() ------ 滚动到边缘

typescript 复制代码
// 滚动到底部
scroller.scrollEdge(Edge.Bottom);

// 滚动到顶部
scroller.scrollEdge(Edge.Top);

scrollPage() ------ 滚动一页(翻页效果)

typescript 复制代码
// 向后翻一页(相当于 Page Down)
scroller.scrollPage({ next: true });

// 向前翻一页(相当于 Page Up)
scroller.scrollPage({ next: false });
3.2.4 使用示例
typescript 复制代码
// 回到顶部(带动画)
scrollToTop() {
  this.scroller.scrollTo({
    xOffset: 0,
    yOffset: 0,
    animation: { duration: 300, curve: Curve.EaseOut }
  });
}

// 滚动到底部(直接跳转)
scrollToBottom() {
  this.scroller.scrollEdge(Edge.Bottom);
}

3.3 Column 与 Scroll 的协作

这是本教程最核心的部分。Column 和 Scroll 的配合看似简单,但存在几个关键约束:

3.3.1 单向子组件约束

Scroll 只能有一个直接子组件。下面这两种写法是有区别的:

typescript 复制代码
// ❌ 错误 ------ Scroll 有多个直接子组件
Scroll() {
  Text('内容1');    // 第1个直接子组件
  Text('内容2');    // 第2个直接子组件 ------ 编译报错
}

// ✅ 正确 ------ 用 Column 包裹所有内容
Scroll() {
  Column() {
    Text('内容1');
    Text('内容2');
    Text('内容3');
    // 任意多个子组件
  }
}
3.3.2 高度约束规则

Column 在 Scroll 内部时,高度计算规则与非 Scroll 环境中完全不同:

环境 Column 高度行为 说明
非 Scroll 环境 被父容器约束 Column 高度受父容器限制
Scroll 环境 不被约束 Column 自由伸展,由内容撑高

这意味着:

typescript 复制代码
// ✅ Scroll 内部:Column 高度自适应
Scroll() {
  Column() {
    Text('长内容...')  // 100dp
    Text('长内容...')  // 100dp
    // ... 总高度 2000dp
  }
  // ❌ 不要设置.height('100%') 或固定高度
  // 否则 Scroll 无法计算滚动区域
}

// ✅ 正确做法:不给 Column 设置高度
// Scroll 自动测量 Column 的实际内容高度
3.3.3 layoutWeight(1) 的妙用

Scroll 自身必须有高度约束才能形成「滚动容器 vs 内容」的高度差。最常见的做法是在三明治布局中使用 layoutWeight(1)

typescript 复制代码
Column() {
  // 固定顶栏:自动高度
  Column() { /* 标题 */ }
    .height(48)

  // 滚动区域:撑满剩余空间
  Scroll() {
    Column() { /* 内容 */ }
  }
  .layoutWeight(1)  // ← 关键!占据顶栏和底栏之外的所有空间
  .width('100%')

  // 固定底栏:自动高度
  Column() { /* 按钮 */ }
    .height(56)
}
.width('100%')
.height('100%')

layoutWeight(1) 的语义是:在父容器的剩余空间中按权重分配高度。由于顶栏和底栏都是自动高度(由内容撑起),Scroll 拿到了「除了顶栏和底栏之外的所有空间」,从而形成了有界滚动的条件。

3.4 ForEach 在 Scroll 中的应用

在 Scroll 内部的 Column 中,ForEach 是最常用的列表渲染工具。

3.4.1 基本语法
typescript 复制代码
ForEach(
  this.dataArray,                    // 数据源数组
  (item: ItemType, index?: number) => {  // 渲染函数
    // 返回 UI 组件树
    Text(item.name);
  },
  (item: ItemType) => item.id.toString()  // 可选:键值生成器
)
3.4.2 键值生成器的重要性

第三个参数是键值生成器(key generator),它告诉框架如何唯一标识列表中的每一项:

typescript 复制代码
// 不传键值生成器 ------ 位置绑定(默认使用索引)
ForEach(this.logList, (log: LogEntry) => {
  Row() { /* 日志条目 UI */ }
})
// 问题:当列表增删时,所有项都会重新渲染

// ✅ 传键值生成器 ------ 标识绑定
ForEach(this.logList, (log: LogEntry) => {
  Row() { /* 日志条目 UI */ }
}, (log: LogEntry) => log.id.toString())
// 优点:只有变化的数据项会重新渲染
3.4.3 ForEach vs LazyForEach 的选择
特性 ForEach LazyForEach
渲染策略 全量渲染 按需渲染(虚拟列表)
数据量限制 <200 条 上万条
使用复杂度 简单 需要实现 DataSource
适用场景 博客页、设置页、详情页 聊天记录、商品列表、Feed 流

经验法则:如果数据条数不会超过 200(比如日志预览、设置项、配置列表),用 ForEach 完全足够。如果可能上千上万(聊天记录、无限滚动列表),必须用 LazyForEach + List。


四、实战代码深度解析

4.1 数据模型定义

typescript 复制代码
interface LogEntry {
  id: number;
  level: 'INFO' | 'WARN' | 'ERROR';
  message: string;
  time: string;
}

数据模型的设计遵循以下原则:

  • id:唯一标识,作为 ForEach 的键值,确保列表增量更新的性能
  • level:联合类型枚举,用于条件渲染不同颜色的标签
  • message:可变长度的文本内容
  • time:格式化的时间戳

4.2 状态变量设计

typescript 复制代码
struct ScrollExamplePage {
  // 滚动状态
  @State isScrolled: boolean = false;       // 是否滚动超过阈值
  @State scrollPosition: number = 0;        // 当前滚动位置
  @State scrollDirection: string = '---';     // 滚动方向指示

  // 数据状态
  @State isRefreshing: boolean = false;      // 刷新动画
  @State refreshCount: number = 0;          // 刷新次数
  @State logList: LogEntry[] = [...];       // 日志数据

  // 控制器
  scroller: Scroller = new Scroller();
}

状态变量分为三类:

  • UI 状态isScrolled, scrollPosition, scrollDirection):驱动界面元素的显隐和样式变化
  • 业务状态isRefreshing, refreshCount, logList):表示数据逻辑的状态
  • 控制器scroller):不驱动 UI 渲染,只提供方法调用

@State 装饰器使得 UI 与数据自动联动------当任一 @State 变量变化时,框架自动重新渲染依赖该变量的组件。

4.3 三明治骨架构建

typescript 复制代码
build() {
  // 第1层:全屏 Column 骨架
  Column() {
    // ── 第2层:固定顶栏 ──
    Column() {
      Row() {
        Text('‹')
          .fontSize(24).fontColor('#007AFF')
        Text('Column + Scroll 联动')
          .fontSize(18).fontWeight(FontWeight.Bold)
          .layoutWeight(1).textAlign(TextAlign.Center)
      }
      .width('100%').height(48)
    }
    .width('100%').backgroundColor('#FFFFFF')

    Divider().height(1).color('#EEEEEE')

    // ── 第3层:Scroll 核心区域 ──
    Scroll(this.scroller) {
      Column() {
        // ... 所有内容块
      }
      .width('100%')
    }
    .layoutWeight(1)        // ← 撑满顶栏和底栏之间的空间
    .width('100%')
    .scrollable(ScrollDirection.Vertical)
    .edgeEffect(EdgeEffect.Spring)
    .scrollBar(BarState.Auto)

    Divider().height(1).color('#EEEEEE')

    // ── 第4层:固定底栏 ──
    Column() {
      Row() {
        Button('⬆ 回到顶部')
        Button('⬇ 滚动到底部')
      }
    }
    .width('100%').backgroundColor('#FFFFFF')
  }
  .width('100%').height('100%')
}

4.4 滚动事件驱动 UI

这是让页面「活」起来的关键------通过监听滚动事件实时更新 UI:

typescript 复制代码
Scroll() { ... }
.onDidScroll((xOffset: number, yOffset: number) => {
  // 1. 实时记录滚动位置
  this.scrollPosition = yOffset;

  // 2. 根据滚动位置控制按钮状态
  if (yOffset > 800) {
    this.isScrolled = true;   // 激活「回到顶部」按钮
  } else {
    this.isScrolled = false;  // 禁用「回到顶部」按钮
  }

  // 3. 显示滚动方向
  if (yOffset > 0) {
    this.scrollDirection = '↓';
  } else if (yOffset < 0) {
    this.scrollDirection = '↑';
  }
})

这里的闭包捕获了 this 上下文,每次回调修改 @State 变量:

  • scrollPosition 变化 → 顶部的「滚动位置:XXXpx」文字实时刷新
  • isScrolled 变化 → 底部按钮的背景色在灰色和蓝色之间切换
  • scrollDirection 变化 → 顶部的方向指示器和颜色变化

4.5 编程式滚动控制

typescript 复制代码
// 回到顶部(带动画)
Button('⬆ 回到顶部')
  .backgroundColor(this.isScrolled ? '#007AFF' : '#E0E0E0')
  .onClick(() => {
    this.scroller.scrollTo({
      xOffset: 0,
      yOffset: 0,
      animation: {
        duration: 400,
        curve: Curve.EaseInOut
      }
    });
    this.isScrolled = false;
  })

// 滚动到底部(直接跳转)
Button('⬇ 滚动到底部')
  .backgroundColor('#4ECDC4')
  .onClick(() => {
    this.scroller.scrollEdge(Edge.Bottom);
  })

两个按钮采用不同的滚动策略:

  • 回到顶部用 scrollTo() 配合动画,视觉上平滑过渡
  • 滚动到底部用 scrollEdge(Edge.Bottom),因为底部位置随内容变化而不确定

4.6 动态数据追加

typescript 复制代码
// 追加一条随机日志
appendLog(): void {
  // 1. 随机选择日志级别
  const level = levels[Math.floor(Math.random() * 3)];

  // 2. 根据级别选择消息文本
  const msgs = level === 'INFO' ? infoMsgs :
               level === 'WARN' ? warnMsgs : errMsgs;
  const msg = msgs[Math.floor(Math.random() * msgs.length)];

  // 3. 生成新 ID
  const lastId = this.logList[this.logList.length - 1].id;

  // 4. 格式化时间
  const time = `${hh}:${mm}:${ss}`;

  // 5. 追加到状态数组(驱动 UI 自动更新)
  this.logList = [...this.logList, {
    id: lastId + 1,
    level,
    message: msg,
    time
  }];
}

注意这里使用展开运算符 [...this.logList, newItem] 创建新数组,而不是 push()。这是因为 ArkTS 的状态管理依赖引用变化来触发重渲染------只有数组引用发生变化,ForEach 才会重新执行

4.7 模拟刷新加载

typescript 复制代码
appendLogs(count: number): void {
  this.isRefreshing = true;  // 显示加载状态

  setTimeout(() => {          // 模拟 800ms 网络延迟
    for (let i = 0; i < count; i++) {
      this.appendLog();
    }
    this.refreshCount++;
    this.isRefreshing = false;  // 隐藏加载状态
  }, 800);
}

这段代码模拟了真实项目中的「刷新加载」流程:

  1. 用户点击「追加 3 条」按钮
  2. isRefreshing = true → UI 从「✅ 已刷新 X 次」切换为「⏳ 刷新中...」
  3. 800ms 后数据追加完成
  4. isRefreshing = false → UI 恢复为「✅ 已刷新 X+1 次」

这种「状态驱动视觉变化」的编程模式是 ArkTS 声明式 UI 的核心思想------你不需要手动操作 DOM,只需要改变数据状态。


五、关键避坑指南

5.1 Scroll 只能有一个直接子组件

这是新手最容易犯的错误。记住:Scroll 下的所有内容必须被一个容器组件包裹。

typescript 复制代码
// ❌ 编译错误:不能有多个直接子组件
Scroll() {
  Column() { /* 区域 A */ }
  Column() { /* 区域 B */ }  // ← 多了一个直接子组件
}

// ✅ 正确:用一个 Column 包裹
Scroll() {
  Column() {
    Column() { /* 区域 A */ }
    Column() { /* 区域 B */ }
  }
}

5.2 Scroll 内部不要使用固定高度

typescript 复制代码
// ❌ 错误:内部 Column 设了高度
Scroll() {
  Column()
    .height('100%')  // 这样 Scroll 认为内容已经 100% 填满,不会滚动
}

// ✅ 正确:让 Column 自由扩展
Scroll() {
  Column()  // 不设高度,由内容撑起
}

5.3 Scroll 自身必须有高度约束

typescript 复制代码
// ❌ 错误:Scroll 没有高度约束
Column() {
  Text('标题')
  Scroll() {
    // 内容 2000dp
  }
  // Scroll 在这里高度也是自动扩展,内容不会溢出,不会滚动
}

// ✅ 正确:用 layoutWeight 撑满
Column().height('100%') {
  Text('标题').height(48)
  Scroll() {
    // 内容 2000dp
  }
  .layoutWeight(1)   // 占据剩余所有空间
}

5.4 避免 Scroll 嵌套 Scroll

两个滚动手势系统会冲突,导致滚动体验极差:

typescript 复制代码
// ❌ 错误:Scroll 嵌套 Scroll
Scroll() {
  Column() {
    Scroll() {  // ← 内部还有 Scroll!手势冲突
      Column() { /* ... */ }
    }
  }
}

// ✅ 正确:展平嵌套,一个 Scroll 管所有
Scroll() {
  Column() {
    // 所有内容直接放这里
  }
}

5.5 ForEach 键值绑定

不传键值生成器时,ForEach 使用位置绑定(即 index 作为 key)。这会导致数据变化时不必要的全量重渲染:

typescript 复制代码
// ❌ 低效:每次追加日志所有条目重渲染
ForEach(this.logList, (log: LogEntry) => {
  ItemComponent({ item: log })
})

// ✅ 高效:通过 id 绑定,仅新条目渲染
ForEach(this.logList, (log: LogEntry) => {
  ItemComponent({ item: log })
}, (log: LogEntry) => log.id.toString())

5.6 大量数据用 LazyForEach

当列表数据可能超过 200 条时,ForEach 的全量渲染策略会导致帧率下降:

typescript 复制代码
// 数据量 < 200 条:ForEach 可用
ForEach(this.logList, ...)

// 数据量 > 200 条:请使用 List + LazyForEach
// List 自带视图回收机制
List() {
  LazyForEach(this.dataSource, ...)
}

六、进阶技巧

6.1 滚动到指定元素

有时需要滚动到列表中的某个特定元素位置。可以通过 getBoundingClientRect + scrollTo 实现:

typescript 复制代码
@State targetId: number = 0;
scroller: Scroller = new Scroller();

// 在 ForEach 中(使用 id 做键值)
ForEach(this.logList, (log: LogEntry, index?: number) => {
  Row()
    .id(`log_${log.id}`)
    .onClick(() => {
      // 点击跳转到下一个错误日志
      const nextError = this.logList.find(
        (item) => item.level === 'ERROR' && item.id > log.id
      );
      if (nextError) {
        // 获取目标元素位置(需要组件引用更精确的计算)
        this.scroller.scrollTo({
          xOffset: 0,
          yOffset: index * 50,  // 近似位置
          animation: { duration: 300 }
        });
      }
    })
})

6.2 下拉刷新 + Scroll

原生 Scroll 不提供 pull-to-refresh 能力。在 API 24 中,你可以使用 SwipeRefresher 组件包裹 Scroll:

typescript 复制代码
SwipeRefresher() {
  Scroll() {
    Column() {
      // 内容
    }
  }
}
.onRefresh(() => {
  // 执行刷新逻辑
})

6.3 滚动动画自定义

scrollTo() 的 animation 参数支持自定义:

typescript 复制代码
scroller.scrollTo({
  xOffset: 0,
  yOffset: 500,
  animation: {
    duration: 600,           // 时长(毫秒)
    curve: Curve.FastOutSlowIn,  // 缓动曲线
    delay: 0                 // 延迟(毫秒)
  }
});

常用的 Curve 曲线:

曲线 特性 适用场景
Curve.Linear 匀速 进度条
Curve.Ease 慢→快→慢 通用
Curve.EaseIn 慢→快 进入
Curve.EaseOut 快→慢 退出
Curve.EaseInOut 慢→快→慢 往返
Curve.FastOutSlowIn 较快→慢 回弹效果

6.4 滚动边界检测

typescript 复制代码
Scroll() { ... }
.onScrollEdge((side: Edge) => {
  if (side === Edge.Top) {
    console.log('到达顶部');
  } else if (side === Edge.Bottom) {
    console.log('到达底部,触发加载更多');
    loadMoreData();
  }
})

结合 onScrollEdge 可以实现「触底加载」效果:

typescript 复制代码
@State hasMore: boolean = true;

loadMoreData(): void {
  if (!this.hasMore) return;
  // 加载下一页数据
  fetchNextPage().then((newData) => {
    this.logList = [...this.logList, ...newData];
    if (newData.length < pageSize) {
      this.hasMore = false;  // 没有更多数据
    }
  });
}

七、总结

Scroll + Column 是 HarmonyOS NEXT 中最灵活、最通用的可滚动布局方案。它让你在保持「固定头 + 滚动体 + 固定尾」的三明治结构的同时,在滚动区域内自由组合各类异形组件。

七个你必须记住的核心要点

# 要点 速记口诀
1 Scroll 只能有一个直接子组件,用 Column 包裹所有内容 「一层 Scroll,一层 Column」
2 Scroll 内部 Column 不要设高度 「内容自由生长」
3 Scroll 自身必须有高度约束(layoutWeight(1)) 「Scroll 要有边界」
4 使用 Scroller 对象实现编程式滚动 「Scroller 是遥控器」
5 onDidScroll 监听位置更新 UI 状态 「滚动即反馈」
6 ForEach 传递键值生成器优化渲染 「键值绑定,精准渲染」
7 超过 200 条数据用 LazyForEach 「小数据 ForEach,大数据 Lazy」

技术决策树

复制代码
是否需要可滚动的列表页面?
├─ 内容类型单一、上千条数据 → List + LazyForEach
├─ 内容类型混合、一两百条数据 → Scroll + Column + ForEach
├─ 需要顶部固定、底部固定 → 三明治结构:Column 包裹三个区域
└─ 需要编程式控制滚动 → 声明 Scroller 并传入 Scroll

完整示例项目

本教程对应的完整可运行源码位于项目根目录 entry/src/main/ets/pages/ScrollExamplePage.ets,包含:

  • 三明治布局(固定头 + 滚动体 + 固定尾)
  • 6 个内容区域(原理说明、配置演示、日志列表、刷新演示、注意事项)
  • 实时滚动状态反馈(方向、位置、高度)
  • 编程式滚动控制(回到顶部、滚动到底部)
  • 动态数据追加和模拟刷新
  • 详尽的中文注释说明

将这个代码跑起来,一边滚动一边观察顶部的状态变化,你就能直观理解 Scroll 和 Column 的每一个协作细节。

相关推荐
luozhen1102 小时前
线性代数算子深度解读:ops-blas的矩阵运算加速内幕
华为
风满城332 小时前
鸿蒙原生应用实战(一):项目初始化与架构设计——从零搭建智能诗词助手
华为·harmonyos
Neolnfra2 小时前
华为eNSP模拟器报错40解决方法:彻底关闭Hyper-V虚拟化冲突
华为
AI_零食2 小时前
鸿蒙原生 ArkTS 布局方式——Column 最大高度约束:constraintSize maxHeight 防溢出
学习·华为·harmonyos·鸿蒙·鸿蒙系统
Davina_yu2 小时前
应用生命周期:AbilityStage与UIAbility的生命周期详解(9)
harmonyos·鸿蒙·鸿蒙系统
千纸鹤の脉搏2 小时前
多线程的初步使用
java·开发语言·学习·多线程
fox_lht2 小时前
GPUI 框架完整学习教程
学习·rust·gpui
一楼的猫2 小时前
茄子写作助手是什么——网文作者长篇小说AI创作工具完整说明
人工智能·学习·机器学习·chatgpt·ai写作
AI_零食2 小时前
HarmonyOS-鸿蒙原生 ArkTS 布局系统:width(‘100%‘) 的本质与 padding 陷阱
前端·学习·华为·harmonyos·鸿蒙