一文吃透 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. 性能优化建议
- 嵌套 List 时一定要指定宽高,避免 List 默认全部加载导致性能问题
- 滚动内容较多时,考虑用 LazyForEach 替代 ForEach,实现懒加载
- 避免在滚动事件中做复杂计算,会影响滚动流畅度
- 不需要显示滚动条时,设置 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\') }}
这个示例包含了:
- 多种滚动控制按钮(顶部、底部、下一页、惯性滚动)
- 滚动状态显示(当前偏移量、是否在底部)
- 美化的滚动条样式
- 弹簧边缘效果
- 限位滚动(自动对齐)
- 滚动到边缘提示
八、常见问题与解决方案
-
问题:Scroll 无法滚动
原因:- 子组件尺寸没有超过 Scroll 尺寸
- 没有正确设置滚动方向
- 被其他组件遮挡或禁用了交互
解决:检查内容尺寸和滚动方向,确保满足滚动条件
-
问题:嵌套滚动时滑动不流畅
原因:父子组件滚动冲突
解决:使用 nestedScroll 属性或 onScrollFrameBegin 事件协调滚动 -
问题:调用 Scroller 方法无效
原因:- 调用时机太早(组件还未创建)
- 控制器未正确绑定
解决:在 onAppear 回调中调用,确保组件已加载
-
问题:滚动时性能差、卡顿
原因:- 内容太多未做懒加载
- 滚动事件中做了 heavy 计算
解决:使用懒加载,优化滚动事件处理逻辑
总结
Scroll 组件作为 HarmonyOS 中最基础也最常用的滚动容器,功能非常强大。从简单的垂直滚动到复杂的嵌套滚动,从基础的属性设置到高级的控制器操作,掌握好这些知识能让你应对各种界面需求。
记住几个核心点:
- 理解滚动方向和内容尺寸的关系
- 熟练使用 Scroller 控制器的各种方法
- 掌握嵌套滚动的处理技巧
- 注意性能优化,特别是内容较多的情况
希望这篇文章能帮你彻底搞懂 Scroll 组件,在实际开发中灵活运用,打造出流畅的滚动体验!如果有什么问题,欢迎在评论区交流哦~