一、引言1.1 为什么 Column 需要 Scroll在上一篇文章中我们深入学习了 Column 的垂直排列布局。但 Column 有一个重要的限制它本身不可滚动。Column无 Scroll┌──────────────┐│ 第 1 项 ││ 第 2 项 ││ 第 3 项 ││ … ││ 第 30 项 │ ← 超出屏幕看不到└──────────────┘Scroll Column┌──────────────┐ ← 视口可见区域│ 第 1 项 ││ 第 2 项 ││ 第 3 项 │├──────────────┤ ← 手指向上滑动│ 第 4 项 ││ 第 5 项 ││ … │└──────────────┘当列表数据超过屏幕高度时没有 Scroll 包裹的 Column 会把超出的部分直接截断或被父容器的 clip 属性裁切。Scroll Column 的组合正是为了解决这个问题——Column 负责排列Scroll 负责滚动。1.2 本文核心内容知识点 说明Scroll 容器 可滚动容器的构造与配置Scroller 控制器 编程式滚动滚动到顶部/底部/指定位置scrollable 方向 设置滚动方向垂直/水平/自由onScroll 监听 实时获取滚动偏移量数据驱动列表 ForEach Scroll 的动态列表渲染编程式滚动 按钮控制 自动演示滚动二、ArkUI 可滚动容器体系2.1 四种可滚动容器ArkUI 提供了多种可滚动容器每种适用于不同的场景容器 特点 适用场景Scroll 通用可滚动容器内部可放任意布局Column/Row/Flex 自定义布局列表、表单List 高性能列表支持懒加载、复用、粘性标题 长列表、聊天记录、Feed 流Grid 二维网格滚动 相册、商品展示WaterFlow 瀑布流布局 图片墙、卡片流其中 Scroll 是最灵活的基础容器——它不限制内部布局可以包裹 Column、Row、Flex 甚至 Stack。2.2 Scroll 的「三层结构」Scroll 可滚动容器└── Column / Row 布局容器├── 子元素 1├── 子元素 2├── 子元素 3└── …第一层Scroll — 提供滚动能力手势识别、滚动条、边缘效果第二层Column / Row — 提供排列规则主轴方向、交叉轴对齐第三层子元素 — 具体内容这种能力与布局分离的设计让开发者可以自由组合不同的布局和滚动方式Scroll 方向 内部布局 效果ScrollDirection.Vertical Column 垂直滚动列表ScrollDirection.Horizontal Row 水平滚动列表ScrollDirection.Vertical Flex(wrap) 垂直滚动的流式布局ScrollDirection.Free 自定义布局 双向滚动三、Scroll 核心 API 详解3.1 构造器Scroll(scroller?: Scroller): ScrollAttribute参数 类型 说明scroller Scroller可选 控制器对象用于编程式滚动3.2 滚动方向控制.scrollable(value: ScrollDirection): ScrollAttributeScrollDirection 枚举枚举值 含义ScrollDirection.Vertical 仅垂直滚动默认ScrollDirection.Horizontal 仅水平滚动ScrollDirection.Free 垂直和水平双向滚动ScrollDirection.None 禁止滚动注意ScrollDirection.None 不是禁用滚动容器而是禁用滚动交互——内容仍然可能超出。3.3 Scroller 控制器Scroller 是一个独立的控制器类用于编程式控制滚动位置declare class Scroller {constructor();// 滚动到指定位置 scrollTo(options: ScrollOptions): void; // 滚动到边缘顶部/底部/左/右 scrollEdge(value: Edge, options?: ScrollEdgeOptions): void; // 滚动到下一页 scrollPage(options: ScrollPageOptions): void; // 滚动到指定索引配合 List/Grid 使用 scrollToIndex(index: number, options?: ScrollToIndexOptions): void; // 获取当前滚动偏移 currentOffset(): OffsetResult; // 判断是否在指定边缘 isAtEnd(): boolean; isAtTop(): boolean;}ScrollOptionsinterface ScrollOptions {xOffset: number | string; // 水平目标偏移yOffset: number | string; // 垂直目标偏移}注意scrollTo 默认是即时跳转无动画。要实现平滑滚动需要结合 animateTo 或使用 scrollEdge 的重载版本。3.4 滚动事件监听.onScroll((xOffset: number, yOffset: number) void): ScrollAttribute.onWillScroll((xOffset: number, yOffset: number) void): ScrollAttribute.onDidScroll(callback): ScrollAttribute.onScrollEnd(callback): ScrollAttribute回调 触发时机 用途onScroll 滚动中持续触发 更新 UI、懒加载判断onWillScroll 滚动前预判 预加载数据、拦截滚动onDidScroll 滚动结束后 统计上报、状态同步onScrollEnd 滚动动画结束 惯性滚动结束后的处理3.5 其他常用属性// 滚动条状态.scrollBar(BarState): ScrollAttribute// BarState.On — 常显滚动条// BarState.Off — 隐藏滚动条// BarState.Auto — 自动显隐默认// 边缘回弹效果.edgeEffect(EdgeEffect): ScrollAttribute// EdgeEffect.Spring — 弹性回弹默认// EdgeEffect.None — 无回弹// 嵌套滚动配置.nestedScroll(NestedScrollOptions): ScrollAttribute四、Demo 代码逐层剖析4.1 项目结构与路由{“src”: [“pages/ScrollColumnDemo”]}ScrollColumnDemo.ets 共 451 行结构如下ScrollColumnDemo.ets (451行)├── interface DataItem ← 列表数据类型├── Component ScrollColumnDemo│ ├── State 变量6个 ← scrollY / isAtTop / isAtBottom / items / isAutoScrolling / scrollProgress│ ├── 不可变数据 ← iconMap(30个Emoji) / colorMap(20色)│ ├── Scroller 控制器 ← 编程滚动│ ├── aboutToAppear() ← 初始化 30 条数据│ ├── build()│ │ ├── 标题 副标题│ │ ├── 滚动信息面板 (Row×3) ← Y偏移 / 进度 / 状态│ │ ├── Scroll Column ← 核心布局│ │ │ └── ForEach → 列表项卡片 (60vp高度)│ │ ├── 底部控制栏 (2行×3按钮) ← 顶部/底部/自动 添加/删除/重置│ │ └── 底部提示│ └── 私有方法│ ├── generateItems() ← 数据生成│ ├── updateScrollProgress() ← 进度计算│ ├── scrollToTop/Bottom() ← 编程滚动│ ├── toggleAutoScroll() ← 自动演示│ ├── addItem/removeItem/resetList ← 数据操作│ └── showItemToast() ← 点击反馈│ └── Builder ctrlButton ← 按钮模板4.2 六个 State 变量的设计State private scrollY: number 0; // 滚动偏移驱动信息面板State private isAtTop: boolean true; // 是否在顶部State private isAtBottom: boolean false; // 是否在底部State private items: DataItem[] []; // 列表数据源State private isAutoScrolling: boolean false; // 自动滚动状态State private scrollProgress: number 0; // 滚动进度 0~1变量分组分组 变量 更新者滚动状态 scrollY, isAtTop, isAtBottom, scrollProgress onScroll 回调数据 items addItem / removeItem / generateItems自动控制 isAutoScrolling toggleAutoScroll / startAutoScroll4.3 列表项的数据结构interface DataItem {id: number; // 唯一标识用作 ForEach 的 keytitle: string; // 标题desc: string; // 描述color: string; // 图标背景色icon: string; // Emoji 图标}30 个列表项每项用不同的 Emoji 和颜色区分 #1 | #2 | ️ #3 | #4 | #5 #6 | ⌚ #7 | #8 | #9 | #10…4.4 核心布局Scroll Column// 核心一Scroll(scroller) 构造可滚动容器 Scroll(this.scroller) {// 核心二Column 在内部提供垂直排列 Column() {ForEach(this.items, (item: DataItem) {// 每个列表项是一个 Row横向布局Row() {Text(item.icon) // Emoji 图标.fontSize(28).backgroundColor(item.color ‘33’) // 半透明底色Column() { // 文字区域 Text(item.title) Text(item.desc) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } } .height(60) .gesture(TapGesture().onAction(() { this.showItemToast(item); })) }, (item: DataItem) item.id.toString())}.width(‘100%’)}// 核心三设置滚动方向 .scrollable(ScrollDirection.Vertical)// 核心四监听滚动事件 .onScroll((xOffset: number, yOffset: number) {this.scrollY yOffset;this.isAtTop yOffset 0;this.updateScrollProgress();}).layoutWeight(1)关键设计细节内容宽度Column 设置 .width(‘100%’) 撑满 Scroll 宽度列表项高度每项固定 60vp30 项总计约 1800vp文字溢出maxLines(1) textOverflow(Ellipsis) 确保单行截断点击反馈TapGesture 绑定点击后更新该项的 desc 文字4.5 滚动信息面板信息面板位于列表上方实时显示三个维度的数据┌─────────────────────────────────┐│ Y 偏移 滚动进度 状态 ││ 256 vp 42% ↕ 滚动中 │└─────────────────────────────────┘实现三个 Column 在 Row 中均分权重Row() {Column() { Text(‘Y 偏移’); Text(${this.scrollY.toFixed(0)} vp) }.layoutWeight(1)Column() { Text(‘滚动进度’); Text(${(scrollProgress*100).toFixed(0)}%) }.layoutWeight(1)Column() { Text(‘状态’); Text(isAtTop ? ‘顶部’ : isAtBottom ? ‘底部’ : ‘滚动中’) }.layoutWeight(1)}.height(48)状态颜色编码顶部 → Color.Green绿色底部 → Color.Orange橙色滚动中 → ‘#00B4D8’青色4.6 进度计算private updateScrollProgress(): void {// 估算总内容高度每项约 68vp含 marginconst estimatedTotalHeight: number this.items.length * 68;const viewportHeight: number 600; // 估算视口高度const maxScroll: number Math.max(estimatedTotalHeight - viewportHeight, 1);this.scrollProgress Math.min(Math.max(this.scrollY / maxScroll, 0), 1);// 底部判定距离底部 5vp 以内this.isAtBottom this.scrollY maxScroll - 5;}这是一个估算值因为无法从 Scroller 直接获取内容总高度每项的实际高度 60vp(卡片) 4vp×2(margin) 68vp视口高度取近似值 600vp4.7 编程式滚动控制滚动到顶部private scrollToTop(): void {this.scroller.scrollTo({ xOffset: 0, yOffset: 0 });}滚动到底部private scrollToBottom(): void {this.scroller.scrollTo({ xOffset: 0, yOffset: 99999 });// 传入很大的 yOffsetScroller 会自动限制在最大位置}自动滚动演示private startAutoScroll(): void {this.scroller.scrollTo({ xOffset: 0, yOffset: 99999 }); // 先到底部setTimeout(() {this.scroller.scrollTo({ xOffset: 0, yOffset: 0 }); // 2秒后回顶部}, 2000);}注意scrollTo 是即时跳转无动画因为 ScrollOptions 接口只有 xOffset 和 yOffset不包含 duration 参数。如果需要动画滚动可以使用 animateTo 结合 Scroller 控制器或使用支持动画的 scrollEdge。4.8 动态数据操作添加一项在第 3 项之后插入private addItem(): void {const newItem: DataItem { id: newId, title:✨ 新列表项 #${newId}, … };// ArkTS 不允许数组 spread 运算符使用 slice concatconst before: DataItem[] this.items.slice(0, 3);const after: DataItem[] this.items.slice(3);this.items before.concat([newItem], after);}删除第一项private removeItem(): void {if (this.items.length 3) return; // 保留至少 3 项this.items this.items.slice(1);}重置为 30 项private resetList(): void {this.items this.generateItems(30);setTimeout(() { this.scrollToTop(); }, 50); // 延迟确保数据已更新}4.9 列表项点击反馈private showItemToast(item: DataItem): void {const updatedItems: DataItem[] this.items.map((it: DataItem) {if (it.id item.id) {// ArkTS 不允许对象 spread 运算符使用完整对象字面量return {id: it.id,title: it.title,desc:✓ 已选中 #${item.id} (${new Date().getSeconds()}s),color: it.color,icon: it.icon};}return it;});this.items updatedItems;}当用户点击某一列表项时该项的描述文字会更新为已选中和当前秒数。这是通过修改 State items 数组中的对应项来实现的——ArkUI 的响应式系统会自动检测到数组变化只重新渲染被点击的那一项。五、Scroll Column 与 List 的选择在实际开发中很多人会问什么时候用 ScrollColumn什么时候用 List对比维度 Scroll Column List渲染策略 全部渲染无论是否可见 只渲染可见项 回收离屏项适用数据量 几 ~ 几十项 几十 ~ 数万项布局灵活性 极高任意嵌套布局 较高ListItem 内可自定义粘性标题 ❌ 需手动实现 ✅ 原生支持滑动删除 ❌ 需手势组合 ✅ 原生支持拖拽排序 ❌ 需手势组合 ✅ 原生支持懒加载 ❌ 需手动实现 ✅ LazyForEach性能消耗 低少量数据 低大量数据通过复用选择建议数据量 30 项 且 布局复杂 → Scroll Column数据量 100 项 → List LazyForEach需要粘性标题/滑动删除 → List布局极其灵活各卡片高度差异大→ Scroll Column六、常见问题与坑点6.1 Scroll 不生效Column 不滚动// ❌ 正确写法Scroll() {Column() {// 子元素}}// ❌ 错误写法Column 不在 Scroll 内部Column() {Scroll() {// 子元素}}// Scroll 必须在外层Column 在内部根本原因Scroll 需要直接容纳可滚动内容。如果 Column 在外层撑满Scroll 夹在中间内容可能不会正确滚动。6.2 Scroll 高度未限定// ❌ Scroll 高度不明确可能撑满全部内容没有溢出就不会滚动Column() {Scroll() {Column() { /* 30 items */ }}// 没有限定高度}// ✅ Scroll 高度 父容器剩余高度Column() {Text(‘标题’)Scroll() {Column() { /* 30 items */ }}.layoutWeight(1) // ← 撑满剩余空间}Scroll 必须有明确的视口高度内容超出这个高度才会触发滚动。6.3 onScroll 参数的含义.onScroll((xOffset: number, yOffset: number) {// xOffset: 水平方向已滚动的距离单位 vp// yOffset: 垂直方向已滚动的距离单位 vp// 注意这些是已滚动距离不是当前可见区域的位置})对于垂直滚动的 ScrollyOffset 从 0顶部开始手指上滑后逐渐增大。6.4 scrollTo 为什么没有动画Scroller.scrollTo() 接受的是 ScrollOptions该接口只有 xOffset 和 yOffset不包含动画参数。需要平滑滚动时有以下方案方案一结合 animateTo// 用 animateTo 包裹状态更新实现过渡动画this.getUIContext()?.animateTo({ duration: 300 }, () {this.scroller.scrollTo({ xOffset: 0, yOffset: 500 });});方案二使用 scrollEdge有动画重载this.scroller.scrollEdge(Edge.Top, { velocity: 800 });6.5 数据变化后滚动位置重置当 State items 数组变化时Scroll 的滚动位置可能会重置到顶部。这是因为 ArkUI 的 diff 算法在对比新旧 UI 树时可能认为需要重新挂载 Scroll 组件。解决方案在数据变化后主动恢复滚动位置private resetList(): void {this.items this.generateItems(30);setTimeout(() { this.scrollToTop(); }, 50);}6.6 ForEach 的 key 必须唯一// ✅ 正确使用唯一 id 作为 keyForEach(this.items, (item) { /* … */ },(item: DataItem) item.id.toString())// ❌ 错误使用索引作为 key插入删除后索引变化ForEach(this.items, (item) { /* … */ },(item: DataItem, index: number) index.toString())不唯一的 key 会导致列表项动画异常、状态丢失。七、最佳实践清单7.1 Scroll Column 的标准模板build() {Column() {// 顶部固定区域HeaderView()// 可滚动内容区 Scroll() { Column() { ForEach(this.items, (item) { ListItemView({ item: item }) }, (item) item.id.toString()) } .width(100%) } .scrollable(ScrollDirection.Vertical) .scrollBar(BarState.Auto) .edgeEffect(EdgeEffect.Spring) .layoutWeight(1) // ← 关键撑满剩余高度 // 底部固定区域 FooterView()}.height(‘100%’)}7.2 列表项组件的提取// 将列表项提取为独立组件Componentstruct ListItemView {Prop item: DataItem; // Prop 接收父组件数据build() {Row() {Text(item.icon)Column() {Text(item.title).fontSize(15).fontColor(Color.White)Text(item.desc).fontSize(12).fontColor(Color.Gray)}}.height(60).padding({ left: 12, right: 12 })}}使用 Prop 或 ObjectLink 传递数据避免整个列表重新渲染。7.3 滚动到某个特定条目// 根据 item.id 计算目标偏移量private scrollToItem(targetId: number): void {const index this.items.findIndex(item item.id targetId);if (index 0) {const targetY index * 68; // 每项高度 68vpthis.scroller.scrollTo({ xOffset: 0, yOffset: targetY });}}7.4 下拉刷新 上拉加载Scroll() {Column() { /* 列表内容 */ }}.onWillScroll((xOffset, yOffset) {if (yOffset -50) {// 下拉超过 50vp → 触发刷新this.onRefresh();}if (this.isNearBottom(yOffset)) {// 接近底部 → 触发加载更多this.onLoadMore();}})7.5 性能优化列表项使用固定高度方便 Scroller 计算滚动范围图片使用懒加载Image().objectFit(ImageFit.Cover)避免在 onScroll 中做耗时计算使用 ObjectLink 替代 Prop 传递复杂对象八、常见应用场景8.1 聊天消息列表Scroll(this.scroller) {Column() {ForEach(this.messages, (msg) {MessageBubble({ message: msg, isSelf: msg.sender ‘me’ })})}}// 新消息时自动滚动到底部.onDidScroll(() {if (this.shouldAutoScroll) {this.scroller.scrollTo({ xOffset: 0, yOffset: 99999 });}})8.2 表单页面Scroll() {Column() {Text(‘个人信息’).fontSize(18)TextInput({ placeholder: ‘姓名’ })TextInput({ placeholder: ‘手机号’ })DatePicker({ start: new Date(1900, 1, 1), end: new Date() })Text(‘地址信息’).fontSize(18)TextInput({ placeholder: ‘省份’ })TextInput({ placeholder: ‘城市’ })TextInput({ placeholder: ‘详细地址’ })Button(‘提交’).width(‘100%’).margin({ top: 20 })}.padding(16)}.width(‘100%’)8.3 文章详情页Scroll() {Column() {Text(‘文章标题’).fontSize(24).fontWeight(FontWeight.Bold)Text(‘作者 | 2025-07-01’).fontSize(12).fontColor(Color.Gray)Image($r(‘app.media.cover’)).width(‘100%’).height(200)Text(‘正文内容…’).fontSize(16).lineHeight(28)// 更多段落…Text(‘—— 全文完 ——’).fontSize(14).fontColor(Color.Gray)}.padding(16)}.width(‘100%’)九、结语9.1 核心回顾Column Scroll 排列 滚动 可滚动的列表这两者的组合是 ArkUI 中最常用的布局模式之一┌────────────────┐│ Column │ ← 没有 Scroll → 内容被截断│ (30 items) │└────────────────┘┌────────────────┐│ Scroll │ ← 提供滚动能力│ ┌──────────┐ ││ │ Column │ │ ← 提供排列规则│ │ (30项) │ ││ │ … │ ││ └──────────┘ │└────────────────┘9.2 本文的技术栈全景Scroll Column Demo├── 布局层面│ ├── Scroll 容器配置方向、滚动条、边缘效果│ ├── Column 垂直排列│ ├── Row 列表项图标文字│ └── layoutWeight 比例分配├── 交互层面│ ├── 手指滑动Scroll 原生手势│ ├── 列表项点击TapGesture│ └── 按钮控制TapGesture├── 控制层面│ ├── Scroller 控制器│ └── onScroll 事件监听└── 数据层面├── State 响应式数据├── ForEach 列表渲染└── slice/concat 数组操作9.3 下一步探索List 组件高性能虚拟列表适合超大数量列表LazyForEach懒加载数据源按需渲染Refresh 组件下拉刷新容器Grid 布局网格滚动布局WaterFlow瀑布流布局附录 A完整 Demo 代码/*ScrollColumnDemo.ets —— 鸿蒙原生 ArkTS 布局方式之 Column 与 Scroll 联动 核心技术 Scroll Column —— 当内容超出屏幕时用 Scroll 包裹 Column 实现滚动.scrollable() —— 设置滚动方向Vertical / Horizontal / Free / NoneScroller —— 编程式滚动控制器滚动到指定位置 布局要点 Column 内部子元素总高度 Scroll 容器高度 → 触发滚动Scroller 提供 scrollTo / scrollEdge / scrollPage 等编程控制onScroll 回调实时报告滚动偏移量配合 edgeEffect 实现边缘回弹效果默认纵向滚动可通过 .scrollable() 修改方向*/interface DataItem {id: number;title: string;desc: string;color: string;icon: string;}EntryComponentstruct ScrollColumnDemo {State private scrollY: number 0;State private isAtTop: boolean true;State private isAtBottom: boolean false;State private items: DataItem[] [];State private isAutoScrolling: boolean false;State private scrollProgress: number 0;private scroller: Scroller new Scroller();private readonly iconMap: string[] [‘’, ‘’, ‘️’, ‘’, ‘’, ‘’, ‘⌚’, ‘’,‘’, ‘’, ‘️’, ‘’, ‘’, ‘’, ‘️’, ‘’,‘’, ‘’, ‘️’, ‘⚙️’, ‘’, ‘’, ‘’, ‘’,‘’, ‘’, ‘’, ‘’, ‘’, ‘’];private readonly colorMap: string[] [‘#4A90D9’, ‘#FF6B35’, ‘#2ECC71’, ‘#9B59B6’,‘#F1C40F’, ‘#E74C3C’, ‘#1ABC9C’, ‘#E67E22’,‘#3498DB’, ‘#E91E63’, ‘#00BCD4’, ‘#8BC34A’,‘#FF5722’, ‘#607D8B’, ‘#795548’, ‘#9C27B0’,‘#3F51B5’, ‘#009688’, ‘#FF9800’, ‘#CDDC39’];aboutToAppear(): void { this.generateItems(30); }build() {Column() {// 标题Text(‘Scroll Column 可滚动纵向列表’).fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White).textAlign(TextAlign.Center).width(‘100%’).padding({ top: 12, bottom: 2 })Text(‘内容超出屏幕时优雅滚动 | 共 ’ this.items.length ’ 项’).fontSize(12).fontColor(Color.Gray).textAlign(TextAlign.Center).width(‘100%’).padding({ bottom: 4 })// 信息面板 Row() { Column() { Text(Y 偏移).fontSize(10).fontColor(Color.Gray) Text(${this.scrollY.toFixed(0)} vp).fontSize(14).fontColor(Color.White) .fontWeight(FontWeight.Bold) }.layoutWeight(1).alignItems(HorizontalAlign.Center) Column() { Text(滚动进度).fontSize(10).fontColor(Color.Gray) Text(${(this.scrollProgress * 100).toFixed(0)}%).fontSize(14) .fontColor(Color.Orange).fontWeight(FontWeight.Bold) }.layoutWeight(1).alignItems(HorizontalAlign.Center) Column() { Text(状态).fontSize(10).fontColor(Color.Gray) Text(this.isAtTop ? ✓ 顶部 : this.isAtBottom ? ✓ 底部 : ↕ 滚动中) .fontSize(13).fontColor(this.isAtTop ? Color.Green : this.isAtBottom ? Color.Orange : #00B4D8) }.layoutWeight(1).alignItems(HorizontalAlign.Center) }.width(100%).height(48).backgroundColor(#1a1a3e) .padding({ left: 8, right: 8 }).margin({ left: 12, right: 12, bottom: 4 }) .borderRadius(8) // 核心Scroll Column Scroll(this.scroller) { Column() { ForEach(this.items, (item: DataItem) { Row() { Text(item.icon).fontSize(28).width(44).height(44) .textAlign(TextAlign.Center) .backgroundColor(item.color 33).borderRadius(10) .margin({ right: 12 }) Column() { Text(item.title).fontSize(15).fontColor(Color.White) .fontWeight(FontWeight.Medium).width(100%) Text(item.desc).fontSize(12).fontColor(Color.Gray) .width(100%).margin({ top: 2 }).maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) }.layoutWeight(1).alignItems(HorizontalAlign.Start) .height(44).justifyContent(FlexAlign.Center) } .width(100%).height(60).padding({ left: 12, right: 12 }) .backgroundColor(#ffffff08).borderRadius(8) .margin({ top: 4, bottom: 4 }) .gesture(TapGesture().onAction(() this.showItemToast(item))) }, (item: DataItem) item.id.toString()) }.width(100%) } .scrollable(ScrollDirection.Vertical) .onScroll((xOffset: number, yOffset: number) { this.scrollY yOffset; this.isAtTop yOffset 0; this.updateScrollProgress(); }) .layoutWeight(1).width(100%).margin({ left: 12, right: 12 }) // 控制栏 Column() { Row() { this.ctrlButton(⬆ 顶部, #4A90D9, () this.scrollToTop()) this.ctrlButton(⬇ 底部, #FF6B35, () this.scrollToBottom()) this.ctrlButton(this.isAutoScrolling ? ⏹ 停止 : ▶ 自动, #9B59B6, () this.toggleAutoScroll()) }.width(100%) Row() { this.ctrlButton( 添加, #2ECC71, () this.addItem()) this.ctrlButton( 删除, #E74C3C, () this.removeItem()) this.ctrlButton(↻ 重置, #607D8B, () this.resetList()) }.width(100%).margin({ top: 4 }) }.width(100%).padding({ left: 12, right: 12, top: 6, bottom: 6 }) Text(上下滑动列表 | 点击项查看效果 | 按钮控制滚动位置) .fontSize(11).fontColor(Color.Gray).textAlign(TextAlign.Center) .width(100%).padding({ bottom: 8 }) } .width(100%).height(100%).backgroundColor(#0f3460)}// — 私有方法 —private generateItems(count: number, startId: number 1): DataItem[] {const result: DataItem[] [];for (let i 0; i count; i) {const id startId i;result.push({id, title:列表项 #${id},desc:这是第 ${id} 个列表项的详细描述。,color: this.colorMap[i % this.colorMap.length],icon: this.iconMap[i % this.iconMap.length]});}return result;}private updateScrollProgress(): void {const totalH this.items.length * 68;const maxScroll Math.max(totalH - 600, 1);this.scrollProgress Math.min(Math.max(this.scrollY / maxScroll, 0), 1);this.isAtBottom this.scrollY maxScroll - 5;}private scrollToTop(): void {this.isAutoScrolling false;this.scroller.scrollTo({ xOffset: 0, yOffset: 0 });}private scrollToBottom(): void {this.isAutoScrolling false;this.scroller.scrollTo({ xOffset: 0, yOffset: 99999 });}private toggleAutoScroll(): void {this.isAutoScrolling !this.isAutoScrolling;if (this.isAutoScrolling) {this.scroller.scrollTo({ xOffset: 0, yOffset: 99999 });setTimeout(() {if (!this.isAutoScrolling) return;this.scroller.scrollTo({ xOffset: 0, yOffset: 0 });}, 2000);}}private addItem(): void {const newId this.items.length 1;const newItem: DataItem {id: newId, title:✨ 新列表项 #${newId},desc:这是动态添加的第 ${newId} 项。,color: ‘#FFD700’, icon: ‘✨’};const before this.items.slice(0, 3);const after this.items.slice(3);this.items before.concat([newItem], after);}private removeItem(): void {if (this.items.length 3) return;this.items this.items.slice(1);}private resetList(): void {this.isAutoScrolling false;this.items this.generateItems(30);setTimeout(() this.scrollToTop(), 50);}private showItemToast(item: DataItem): void {this.items this.items.map((it: DataItem) {if (it.id ! item.id) return it;return {id: it.id, title: it.title,desc:✓ 已选中 #${item.id} (${new Date().getSeconds()}s),color: it.color, icon: it.icon};});}BuilderctrlButton(label: string, color: string, action: () void) {Button(label).height(34).fontSize(12).backgroundColor(color).fontColor(Color.White).borderRadius(8).layoutWeight(1).margin({ left: 2, right: 2 }).gesture(TapGesture().onAction(() action()))}}附录 B参考资料HarmonyOS NEXT 开发者文档 — Scroll 容器HarmonyOS NEXT 开发者文档 — Scroller 控制器HarmonyOS NEXT 开发者文档 — ScrollDirectionHarmonyOS NEXT 开发者文档 — ForEach 使用说明版权声明本文为 HarmonyOS NEXT 技术分享系列的第五篇遵循 CC BY-NC 4.0 协议。欢迎转载但请注明出处。系列文章第一篇TapGesture 点击手势布局第二篇PanGesture 拖拽手势布局第三篇GestureGroup 组合手势布局第四篇Column 垂直排列入门第五篇Column Scroll 可滚动列表本文