滚动是移动端最高频的交互操作,没有之一。HarmonyOS NEXT对滚动体验的控制相当细——边缘效果有三种模式,滚动条可以定制颜色和宽度,onScrollFrameBegin可以实现吸顶和折叠。这些API看着简单,但搭配起来有不少细节。
EdgeEffect:三种边缘效果
List、Scroll、WaterFlow等滚动组件都支持edgeEffect属性:
Spring — 弹簧回弹
List().edgeEffect(EdgeEffect.Spring)拉到边界后可以继续拉,松手弹回来。iOS风格的经典效果,HarmonyOS默认就是这个。适合大部分内容列表。
回弹距离跟拉动力度成正比,松手后有过冲动画(弹过一点再回正)。这种物理感很自然,用户拉到边界时明确知道"到底了"。
Fade — 渐隐
List().edgeEffect(EdgeEffect.Fade)拉到边界后边缘出现渐隐效果,不是回弹。视觉上比Spring内敛,适合工具类应用、设置页面这类不需要"活泼感"的场景。
Fade效果在垂直和水平方向都生效,水波纹一样向外扩散然后消散。
None — 无效果
List().edgeEffect(EdgeEffect.None)拉到边界直接停住,没有任何视觉反馈。很少用,除非业务要求严格的"到底即止"行为——比如分页数据每页固定条数,拉到底就该翻页不该回弹。
BarState:滚动条显示控制
List().scrollBar(BarState.Auto)// 自动显示/隐藏(默认).scrollBar(BarState.On)// 始终显示.scrollBar(BarState.Off)// 始终隐藏- Auto:滚动时显示,停止后渐隐。最常用。
- On:始终显示。适合长文档、代码查看等需要随时看滚动位置的场景。
- Off:隐藏。搭配自定义滚动指示器使用。
滚动条样式定制
List().scrollBarColor('#1a73e8')// 滚动条颜色.scrollBarWidth(4)// 滚动条宽度(vp)默认滚动条是灰色圆角条,3-4vp宽。改成品牌色+4vp是常见的定制手法。宽度不建议超过6vp,太粗影响内容区域。
Scroll组件同样支持这些属性:
Scroll().scrollBar(BarState.Auto).scrollBarColor('#43a047').scrollBarWidth(4).edgeEffect(EdgeEffect.Fade)List滚动进阶
initialIndex — 初始位置
List({initialIndex:5}){ForEach(...)}列表初始显示第5项。搭配聊天气泡场景——从最新消息开始显示,不用先滚到底。
scrollToIndex — 编程式跳转
@StatescrollIndex:number=0;List({initialIndex:this.scrollIndex}){// ...}Button('跳到底部').onClick(()=>{this.scrollIndex=this.itemCount-1;})注意:List的initialIndex只在初始化时生效,后续修改不会自动跳转。要实现动态跳转需要用Scroller:
privatelistScroller:Scroller=newScroller();List({scroller:this.listScroller}){// ...}Button('跳到顶部').onClick(()=>{this.listScroller.scrollToIndex(0);})Button('跳到底部').onClick(()=>{this.listScroller.scrollToIndex(this.itemCount-1);})Scroller还支持scrollEdge(Edge.Top/Edge.Bottom)直接滚到头尾。
divider — 分隔线
List().divider({strokeWidth:1,color:'#eeeeee',startMargin:12,endMargin:12})比在ListItem里手动画Divider方便,而且自动跳过最后一项的分隔线。startMargin/endMargin控制左右留白。
onScrollFrameBegin — 滚动拦截
这是实现吸顶、折叠头部的核心API:
List().onScrollFrameBegin((offset:number)=>{// offset是本次滚动偏移量(正=向上滚,负=向下滚)// 返回{offsetRemain: 实际应用的偏移}this.addLog('scroll offset='+offset.toString());return{offsetRemain:offset};})返回的offsetRemain决定了实际滚动距离。如果返回{offsetRemain: 0},滚动被完全拦截——这就是嵌套Scroll实现吸顶的关键。
注意回调频率很高(每帧一次),不要在里面做重计算或setState。
水平滚动
Scroll组件默认垂直滚动,改成水平:
Scroll(){Row(){ForEach(items,(item:string)=>{Column(){Text(item)}.width(80).height(80).margin({right:8})},(item:string,index:number)=>index.toString())}}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Auto).edgeEffect(EdgeEffect.Spring)水平滚动必须用Scroll+Row,List不支持水平模式(List的横向布局用List+ListItem+Row模拟)。
嵌套滚动
垂直Scroll嵌套水平Scroll是常见模式:
Scroll(){Column(){Text('水平区域')Scroll(){Row(){/* 横向内容 */}}.scrollable(ScrollDirection.Horizontal)Text('垂直内容')Column(){/* 纵向内容 */}}}嵌套滚动默认会冲突——手指竖滑时水平Scroll不响应,横滑时垂直Scroll不响应。这是预期的,GestureGroup的Exclusive模式自动处理了方向判断。
ForEach在List中的注意事项
ForEach生成ListItem时,数组必须是显式类型的:
// 错误:数组字面量类型不可推断ForEach([{name:'a'},{name:'b'}],...)// 正确:先声明接口,再创建数组interfaceStringItem{name:string;}constitems:StringItem[]=[{name:'a'},{name:'b'}];ForEach(items,(item:StringItem)=>{...})ArkTS的arkts-no-untyped-obj-literals规则要求对象字面量必须有对应的接口声明。这个规则在List场景下特别容易触发,因为列表数据通常是对象数组。
另一个常见写法是用Array.from:
constitems:number[]=Array.from({length:30},(_v:Object,i:number)=>i);ForEach(items,(item:number)=>{...},(item:number,index:number)=>index.toString())边缘效果选型指南
| 场景 | EdgeEffect | BarState | 理由 |
|---|---|---|---|
| 新闻列表 | Spring | Auto | 活泼感,拉到头弹回 |
| 设置页 | Fade | Auto | 内敛,功能导向 |
| 聊天记录 | Spring | Off | 自定义滚动指示 |
| 代码查看 | None | On | 精确定位,无回弹干扰 |
| 图片横滑 | Spring | Auto | 丝滑横滑体验 |
| 分页数据 | None | Auto | 到底即止,引导翻页 |
性能提示
- edgeEffect(EdgeEffect.Spring)比None略耗性能(回弹动画),但差异可忽略
- scrollBar(BarState.Off)比On少渲染一个滚动条组件
- onScrollFrameBegin每帧回调,保持轻量
- divider比手动Divider组件性能好(批量渲染优化)
- cachedCount减少列表项重建,但不影响滚动体验本身
滚动体验是"用的时候无感,差的时候要命"的典型维度。Spring回弹给身体感,Fade给视觉感,None给控制感——选对边缘效果比优化滚动速度重要得多。