> 技术文档 > 一文吃透 HarmonyOS 的 Scroll 滚动组件,从入门到精通_鸿蒙scroll组件

一文吃透 HarmonyOS 的 Scroll 滚动组件,从入门到精通_鸿蒙scroll组件

大家好啊!今天咱们来好好聊聊 HarmonyOS 里特别常用的一个组件 ——Scroll。如果你做过移动端开发,肯定知道滚动容器有多重要,当内容太多装不下的时候,就得靠它来帮忙了。今天我就用最接地气的方式,把 Scroll 组件的方方面面都给你讲明白,保证看完就能上手用!

一、啥是 Scroll 组件?

简单说,Scroll 就是个可以滚动的容器。当它里面的内容尺寸超过自身大小时,内容就能上下或者左右滚动。这个组件从 API version 7 就开始支持了,后续还在不断更新功能。

有几个关键点你一开始就得记住:

  • 只能包含一个子组件(这点很重要,别多放)
  • 必须满足主轴方向大小小于内容大小才能滚动
  • 默认就会裁剪超出部分(clip 属性默认 true)
  • 嵌套 List 的时候最好指定宽高,不然可能影响性能

咱们先看个最基本的用法,创建一个带控制器的 Scroll:

@Entry@Componentstruct BasicScrollExample { // 创建滚动控制器 scroller: Scroller = new Scroller(); build() { Scroll(this.scroller) { // 绑定控制器 Column() { // 这里放很多内容,确保超过容器高度 ForEach([1,2,3,4,5,6,7,8,9,10], (item) => { Text(`第${item}项内容`) .width(\'90%\') .height(150) .backgroundColor(\'#fff\') .margin(10) .textAlign(TextAlign.Center) }) }.width(\'100%\') } .height(\'100%\') // 容器高度 .backgroundColor(\'#f1f1f1\') }}

这段代码就创建了一个最基础的垂直滚动容器,里面放了 10 个文本框,因为内容总高度超过容器高度,所以可以上下滚动。

二、Scroll 的核心属性,一个都不能少

Scroll 组件有很多实用属性,咱们一个一个来讲,每个都配上代码示例,保证你看得明明白白。

1. 控制滚动方向:scrollable

默认是垂直滚动,你可以改成水平或者禁止滚动。它接受 ScrollDirection 枚举值:

  • Vertical:垂直滚动(默认)
  • Horizontal:水平滚动
  • None:禁止滚动
// 水平滚动示例Scroll(this.scroller) { Row() { // 用Row作为子组件,实现水平排列 ForEach([1,2,3,4,5,6], (item) => { Text(`第${item}项`) .width(200) .height(200) .backgroundColor(\'#fff\') .margin(10) .textAlign(TextAlign.Center) }) }.width(\'auto\') // 关键:宽度自适应内容}.scrollable(ScrollDirection.Horizontal) // 设置为水平滚动.width(\'100%\').height(220).backgroundColor(\'#f1f1f1\')

这里要注意,水平滚动时,子组件的宽度得超过容器宽度才会滚动,所以 Row 的 width 设为 auto,让它根据内容自适应。

2. 滚动条设置:scrollBar、scrollBarColor、scrollBarWidth

这三个属性是控制滚动条外观的黄金组合:

  • scrollBar:控制滚动条显示状态(Auto/On/Off)
  • scrollBarColor:设置滚动条颜色
  • scrollBarWidth:设置滚动条宽度
Scroll(this.scroller) { Column() { // 内容省略... }}.scrollable(ScrollDirection.Vertical).scrollBar(BarState.On) // 一直显示滚动条.scrollBarColor(Color.Red) // 滚动条设为红色.scrollBarWidth(6) // 滚动条宽度6vp

小贴士:scrollBarWidth 设为 0 的话,滚动条就完全不显示了,适合那些需要隐藏滚动条但保持滚动功能的场景。

3. 边缘滑动效果:edgeEffect

控制滚动到边缘时的反馈效果,支持弹簧效果和阴影效果:

// 弹簧效果示例(越界后会回弹)Scroll(this.scroller) { Column() { // 内容省略... }}.edgeEffect(EdgeEffect.Spring) // 弹簧效果// 阴影效果示例Scroll(this.scroller) { Column() { // 内容省略... }}.edgeEffect(EdgeEffect.Shadow) // 阴影效果

从 API 11 开始,还能设置当内容小于容器时是否启用滑动效果:

Scroll(this.scroller) { Column() { Text(\"内容很少\") .width(\'100%\') .height(100) }}.edgeEffect(EdgeEffect.Spring, { alwaysEnabled: false }) // 当内容小于容器时,不启用滑动效果

4. 限位滚动:scrollSnap(API 10+)

这个属性特别实用,能让滚动停止时自动对齐到指定位置,适合做轮播、分页等场景。它需要传入一个 ScrollSnapOptions 对象:

Scroll(this.scroller) { Column() { ForEach([1,2,3,4,5], (item) => { Text(`第${item}页`) .width(\'100%\') .height(300) .backgroundColor(\'#fff\') .margin({ bottom: 10 }) .textAlign(TextAlign.Center) }) }}.scrollSnap({ snapAlign: ScrollSnapAlign.START, // 对齐方式:首部对齐 snapPagination: 310, // 每页大小(300高度+10边距) enableSnapToStart: true, // 不允许在开头和第一页间滑动 enableSnapToEnd: true // 不允许在最后一页和末尾间滑动}).height(\'100%\')

这样设置后,滚动时会自动吸附到每页的起始位置,特别适合做分页浏览。

5. 滑动翻页:enablePaging(API 11+)

开启后支持滑动翻页,但要注意如果同时设置了 scrollSnap,会优先生效:

Scroll(this.scroller) { Column() { // 每页内容... }}.enablePaging(true) // 开启滑动翻页

6. 初始滚动位置:initialOffset(API 12+)

可以设置组件首次加载时的初始滚动偏移量:

Scroll(this.scroller) { Column() { // 内容... }}.initialOffset({ yOffset: \'50%\' }) // 初始位置在50%高度处

这里的百分比是相对于 Scroll 组件自身的尺寸计算的。

7. 摩擦系数:friction(API 10+)

控制滚动的惯性大小,值越小惯性越大,滚动距离越远:

Scroll(this.scroller) { // 内容...}.friction(0.3) // 小摩擦系数,滚动更顺滑

不同设备有不同默认值:

  • 非可穿戴设备:API 10 是 0.6,API 11 是 0.7,API 12 是 0.75
  • 可穿戴设备:0.9

三、常用事件,监听滚动状态

Scroll 提供了很多事件来监听滚动状态,咱们挑几个最常用的来讲。

1. 滚动前后事件:onWillScroll 和 onDidScroll(API 12+)

这两个是 API 12 之后推荐使用的事件,替代了原来的 onScroll:

Scroll(this.scroller) { // 内容...}.onWillScroll((xOffset, yOffset, scrollState, scrollSource) => { console.log(`即将滚动:x=${xOffset}, y=${yOffset}`); // 可以返回自定义偏移量,比如限制最大滚动距离 if (yOffset > 1000) { return { xOffset: 0, yOffset: 1000 }; }}).onDidScroll((xOffset, yOffset, scrollState) => { console.log(`正在滚动:x=${xOffset}, y=${yOffset}`); // 可以在这里更新UI,比如显示当前位置})

onWillScroll 在滚动前触发,还能修改滚动偏移量;onDidScroll 在滚动时触发,适合做实时反馈。

2. 滚动开始和停止:onScrollStart 和 onScrollStop(API 9+)

Scroll(this.scroller) { // 内容...}.onScrollStart(() => { console.log(\"滚动开始了\");}).onScrollStop(() => { console.log(\"滚动停止了\");})

这两个事件在用户开始拖动和停止滚动时触发,适合做一些状态切换,比如显示 / 隐藏导航栏。

3. 滚动到边缘:onScrollEdge

当滚动到边缘时触发,可以用来做加载更多等操作:

Scroll(this.scroller) { // 内容...}.onScrollEdge((side: Edge) => { console.log(`滚动到了${side}边缘`); if (side === Edge.Bottom) { // 加载更多数据 this.loadMore(); }})

四、Scroller 控制器,让滚动尽在掌握

Scroller 是控制 Scroll 组件的核心,可以通过它实现各种程序化滚动操作。先看怎么创建和绑定:

@Componentstruct MyComponent { // 创建控制器实例 private scroller: Scroller = new Scroller(); build() { // 绑定到Scroll组件 Scroll(this.scroller) { // 内容... } }}

有了这个控制器,就能调用各种滚动方法了。

1. 滚动到指定位置:scrollTo

可以指定 x/y 偏移量,还能配置动画:

// 无动画滚动this.scroller.scrollTo({ xOffset: 0, yOffset: 500 });// 带动画滚动this.scroller.scrollTo({ xOffset: 0, yOffset: 1000, animation: { duration: 1500, // 动画时长1.5秒 curve: Curve.EaseOut, // 减速曲线 canOverScroll: true // 允许越界 }});

2. 滚动到边缘:scrollEdge

快速滚动到顶部、底部等边缘位置:

// 滚动到顶部this.scroller.scrollEdge(Edge.Top);// 滚动到底部,带速度设置(API 12+)this.scroller.scrollEdge(Edge.Bottom, { velocity: 500 });

3. 滚动指定距离:scrollBy(API 9+)

相对于当前位置滚动指定距离:

// 向下滚动100vpthis.scroller.scrollBy(0, 100);// 向左滚动50vpthis.scroller.scrollBy(-50, 0);

4. 翻页操作:scrollPage(API 9+)

可以向前或向后翻一页:

// 向下翻一页,带动画this.scroller.scrollPage({ next: true, animation: true });// 向上翻一页this.scroller.scrollPage({ next: false });

5. 惯性滚动:fling(API 12+)

模拟手指快速滑动后的惯性滚动:

// 向下惯性滚动(正值向下)this.scroller.fling(2000);// 向上惯性滚动(负值向上)this.scroller.fling(-3000);

速度单位是 vp/s,值越大滚动越远。

6. 获取当前偏移量:currentOffset

获取当前的滚动位置:

const offset = this.scroller.currentOffset();console.log(`当前位置:x=${offset.xOffset}, y=${offset.yOffset}`);

7. 判断是否滚动到底部:isAtEnd(API 10+)

if (this.scroller.isAtEnd()) { console.log(\"已经滚动到底部了\");}

五、嵌套滚动,复杂界面的必备技能

在实际开发中,经常会遇到嵌套滚动的场景,比如 Scroll 里套 List,这时候需要特殊处理才能让滚动体验更自然。

方式一:用 onScrollFrameBegin 事件实现

这种方式需要手动控制父子组件的滚动分配:

@Componentstruct NestedScrollExample1 { private parentScroller: Scroller = new Scroller(); private childScroller: Scroller = new Scroller(); private listData: number[] = Array.from({ length: 20 }, (_, i) => i); @State listPosition: number = 0; // 0:顶部 1:中间 2:底部 build() { Scroll(this.parentScroller) { Column() { // 顶部区域 Text(\"顶部内容区\") .width(\'100%\') .height(200) .backgroundColor(\'#330000FF\') .textAlign(TextAlign.Center) // 嵌套的List List({ scroller: this.childScroller, space: 10 }) { ForEach(this.listData, (item) => { ListItem() {  Text(`列表项 ${item}`) .width(\'100%\') .height(80) .backgroundColor(\'#fff\') .textAlign(TextAlign.Center) } }) } .width(\'100%\') .height(400) .edgeEffect(EdgeEffect.None) // 关键:子组件关闭边缘效果 .onReachStart(() => this.listPosition = 0) .onReachEnd(() => this.listPosition = 2) .onScrollFrameBegin((offset) => { // 当子组件在顶部且继续向上滚,让父组件滚动 if (this.listPosition === 0 && offset = 0) { this.parentScroller.scrollBy(0, offset); return { offsetRemain: 0 }; // 子组件不滚动 } // 中间状态,子组件自己滚动 this.listPosition = 1; return { offsetRemain: offset }; }) // 底部区域 Text(\"底部内容区\") .width(\'100%\') .height(200) .backgroundColor(\'#330000FF\') .textAlign(TextAlign.Center) } } .width(\'100%\') .height(\'100%\') .backgroundColor(\'#f1f1f1\') }}

这种方式需要监听子组件的滚动位置,当到达边缘时,将滚动事件传递给父组件。

方式二:用 nestedScroll 属性实现(API 10+)

这种方式更简单,通过配置嵌套滚动规则实现:

@Componentstruct NestedScrollExample2 { private listData: number[] = Array.from({ length: 20 }, (_, i) => i); build() { Scroll() { Column() { Text(\"顶部区域\") .width(\'100%\') .height(200) .backgroundColor(\'#0080DC\') .textAlign(TextAlign.Center) // 嵌套的List List({ space: 10 }) { ForEach(this.listData, (item) => { ListItem() {  Text(`列表项 ${item}`) .width(\'100%\') .height(80) .backgroundColor(\'#fff\') .textAlign(TextAlign.Center) } }) } .width(\'100%\') .height(400) .edgeEffect(EdgeEffect.Spring) // 配置嵌套滚动规则 .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, // 向下滚时先让父组件滚 scrollBackward: NestedScrollMode.SELF_FIRST // 向上滚时先让子组件滚 }) Text(\"底部区域\") .width(\'100%\') .height(200) .backgroundColor(\'#0080DC\') .textAlign(TextAlign.Center) } } .edgeEffect(EdgeEffect.Spring) .backgroundColor(\'#f1f1f1\') }}

这种方式更简洁,通过设置 NestedScrollMode 来控制滚动优先级:

  • SELF_ONLY:只自己滚动
  • PARENT_FIRST:优先父组件滚动
  • SELF_FIRST:优先自己滚动

六、实用功能与高级技巧

1. 获取子组件信息

(1)获取子组件位置和大小:getItemRect(API 11+)
// 点击按钮获取第5项的位置信息Button(\"获取第5项位置\") .onClick(() => { const rect = this.scroller.getItemRect(4); // 索引从0开始 console.log(`位置:x=${rect.x}, y=${rect.y}, 宽=${rect.width}, 高=${rect.height}`); })

注意:只能获取当前可见区域内的子组件信息。

(2)通过坐标获取子组件索引:getItemIndex(API 14+)
List({ scroller: this.scroller }) { // 列表项...}.gesture( PanGesture() .onActionUpdate((event) => { if (event.fingerList[0]) { const index = this.scroller.getItemIndex( event.fingerList[0].localX, event.fingerList[0].localY ); console.log(`当前触摸的是第${index}项`); } }))

2. 边缘渐隐效果

可以给滚动组件添加边缘渐隐,让内容滚动到边缘时自然淡出:

import { LengthMetrics } from \'@kit.ArkUI\';Scroll(this.scroller) { Column() { ForEach([1,2,3,4,5,6,7,8,9], (item) => { Text(`项 ${item}`) .width(\'90%\') .height(150) .backgroundColor(\'#fff\') .margin(10) .textAlign(TextAlign.Center) }) }}// 开启边缘渐隐,设置渐隐长度为80vp.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(80) }).height(\'100%\')

3. 性能优化建议

  1. 嵌套 List 时一定要指定宽高,避免 List 默认全部加载导致性能问题
  2. 滚动内容较多时,考虑用 LazyForEach 替代 ForEach,实现懒加载
  3. 避免在滚动事件中做复杂计算,会影响滚动流畅度
  4. 不需要显示滚动条时,设置 scrollBarWidth (0) 而不是 scrollBar (BarState.Off),性能更好

七、完整示例:一个功能丰富的 Scroll 应用

下面咱们整合前面学的知识,做一个包含多种功能的 Scroll 示例:

import { curves, LengthMetrics } from \'@kit.ArkUI\';@Entry@Componentstruct ScrollComprehensiveExample { private scroller: Scroller = new Scroller(); private dataList: number[] = Array.from({ length: 20 }, (_, i) => i + 1); @State currentOffset: string = \"0\"; @State isAtBottom: boolean = false; // 检查是否在底部 checkBottom() { this.isAtBottom = this.scroller.isAtEnd(); } build() { Column() { // 控制栏 Row({ space: 10 }) { Button(\"到顶部\") .onClick(() => this.scroller.scrollEdge(Edge.Top)) Button(\"到底部\") .onClick(() => this.scroller.scrollEdge(Edge.Bottom)) Button(\"下一页\") .onClick(() => this.scroller.scrollPage({ next: true, animation: true })) Button(\"惯性滚动\") .onClick(() => this.scroller.fling(3000)) } .padding(10) .width(\'100%\') .backgroundColor(\'#eee\') // 滚动区域 Scroll(this.scroller) { Column() { // 头部大图 Image(\'https://images.unsplash.com/photo-1600294037792-2f1b165a3857?ixlib=rb-4.0.3\') .width(\'100%\') .height(200) .objectFit(ImageFit.Cover) // 列表内容 ForEach(this.dataList, (item) => { Text(`内容项 ${item}`)  .width(\'90%\')  .height(120)  .backgroundColor(\'#fff\')  .borderRadius(10)  .margin({ top: 10 })  .textAlign(TextAlign.Center)  .fontSize(18) }) // 底部信息 Text(\"已经到底啦~\") .width(\'100%\') .height(100) .textAlign(TextAlign.Center) .margin({ top: 20 }) } } .scrollable(ScrollDirection.Vertical) .scrollBar(BarState.Auto) .scrollBarColor(\'#666\') .scrollBarWidth(4) .edgeEffect(EdgeEffect.Spring) .friction(0.7) .scrollSnap({ snapAlign: ScrollSnapAlign.START, snapPagination: 130, // 120高度+10边距 }) .onDidScroll((x, y) => { this.currentOffset = y.toFixed(0); this.checkBottom(); }) .onScrollEdge((side) => { if (side === Edge.Bottom) { promptAction.showToast({ message: \"已经到底部了\" }); } }) .flexGrow(1) // 状态显示 Row() { Text(`当前偏移:${this.currentOffset}vp`) Text(`是否底部:${this.isAtBottom ? \'是\' : \'否\'}`) .marginLeft(20) } .padding(10) .width(\'100%\') .backgroundColor(\'#eee\') } .width(\'100%\') .height(\'100%\') .backgroundColor(\'#f5f5f5\') }}

这个示例包含了:

  • 多种滚动控制按钮(顶部、底部、下一页、惯性滚动)
  • 滚动状态显示(当前偏移量、是否在底部)
  • 美化的滚动条样式
  • 弹簧边缘效果
  • 限位滚动(自动对齐)
  • 滚动到边缘提示

八、常见问题与解决方案

  1. 问题:Scroll 无法滚动
    原因

    • 子组件尺寸没有超过 Scroll 尺寸
    • 没有正确设置滚动方向
    • 被其他组件遮挡或禁用了交互
      解决:检查内容尺寸和滚动方向,确保满足滚动条件
  2. 问题:嵌套滚动时滑动不流畅
    原因:父子组件滚动冲突
    解决:使用 nestedScroll 属性或 onScrollFrameBegin 事件协调滚动

  3. 问题:调用 Scroller 方法无效
    原因

    • 调用时机太早(组件还未创建)
    • 控制器未正确绑定
      解决:在 onAppear 回调中调用,确保组件已加载
  4. 问题:滚动时性能差、卡顿
    原因

    • 内容太多未做懒加载
    • 滚动事件中做了 heavy 计算
      解决:使用懒加载,优化滚动事件处理逻辑

总结

Scroll 组件作为 HarmonyOS 中最基础也最常用的滚动容器,功能非常强大。从简单的垂直滚动到复杂的嵌套滚动,从基础的属性设置到高级的控制器操作,掌握好这些知识能让你应对各种界面需求。

记住几个核心点:

  • 理解滚动方向和内容尺寸的关系
  • 熟练使用 Scroller 控制器的各种方法
  • 掌握嵌套滚动的处理技巧
  • 注意性能优化,特别是内容较多的情况

希望这篇文章能帮你彻底搞懂 Scroll 组件,在实际开发中灵活运用,打造出流畅的滚动体验!如果有什么问题,欢迎在评论区交流哦~