列表没有下拉刷新就像网页没有F5——用户刷新不了数据,总觉得看到的是旧内容。HarmonyOS的Refresh组件能做基础刷新,但真要做出"下拉→弹性动画→加载中→收起"的完整体验,默认配置远远不够。上拉加载更是没有内置支持,得自己从头撸。
Refresh组件基础
最简单的下拉刷新:
@StateisRefreshing:boolean=falseRefresh({refreshing:$$this.isRefreshing}){List(){ForEach(this.items,(item:DataItem)=>{ListItem(){Text(item.title)}},(item:DataItem)=>item.id)}}.onRefreshing(()=>{this.loadData();})$$this.isRefreshing是双向绑定——Refresh组件会自动设为true触发刷新,数据加载完后设为false收起。onRefreshing是刷新触发回调。
问题:Refresh的默认动画太简陋——就一个圈在转,没有弹性下拉效果,没有文字提示,没有成功/失败反馈。
自定义刷新布局
用Refresh的builder参数自定义刷新区域:
@StateisRefreshing:boolean=false@StaterefreshState:string='idle'@BuilderrefreshBuilder(){Row(){if(this.refreshState==='idle'){Text('下拉刷新').fontSize(14).fontColor('#999999')}elseif(this.refreshState==='pulling'){Text('释放立即刷新').fontSize(14).fontColor('#666666')}elseif(this.refreshState==='refreshing'){LoadingProgress().width(20).height(20).margin({right:8})Text('正在刷新...').fontSize(14).fontColor('#1a73e8')}elseif(this.refreshState==='success'){Text('刷新成功').fontSize(14).fontColor('#43a047')}elseif(this.refreshState==='failed'){Text('刷新失败,请重试').fontSize(14).fontColor('#e53935')}}.width('100%').height(60).justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)}Refresh({refreshing:$$this.isRefreshing,builder:this.refreshBuilder()}){List(){// 列表内容}}.onRefreshing(async()=>{this.refreshState='refreshing';try{awaitthis.loadData();this.refreshState='success';}catch(e){this.refreshState='failed';}setTimeout(()=>{this.isRefreshing=false;this.refreshState='idle';},500);})五个状态:idle→pulling→refreshing→success/failed→idle。成功和失败显示500ms后收起。
下拉距离监听
Refresh组件的onPullingDown回调提供下拉距离:
Refresh({refreshing:$$this.isRefreshing}){List(){// 内容}}.onPullingDown((progress:number)=>{if(progress<0.6&&this.refreshState!=='refreshing'){this.refreshState='idle';}elseif(progress>=0.6&&this.refreshState!=='refreshing'){this.refreshState='pulling';}})progress是0~1的进度值,0表示未下拉,1表示到达触发阈值。0.6是个经验值——下拉超过60%时提示"释放刷新"。
自定义旋转箭头
下拉时箭头随进度旋转:
@StatepullProgress:number=0@BuilderrefreshBuilder(){Row(){Image($r('sys.media.ohos_ic_public_arrow_down')).width(20).height(20).rotate({angle:this.pullProgress*180}).animation({duration:100}).margin({right:8})if(this.refreshState==='refreshing'){LoadingProgress().width(20).height(20).margin({right:8})Text('加载中...')}elseif(this.pullProgress>=0.6){Text('释放刷新')}else{Text('下拉刷新')}}.fontSize(14).fontColor('#666666').width('100%').height(60).justifyContent(FlexAlign.Center)}.onPullingDown((progress:number)=>{this.pullProgress=progress;// 状态判断})pullProgress从0到1,箭头旋转0°到180°。超过阈值时箭头朝上,提示"释放刷新"。
上拉加载更多
HarmonyOS没有内置上拉加载,需要自己实现。最简单的方式是在List底部加一个加载触发器:
@StateisLoadingMore:boolean=false@StatehasMore:boolean=trueprivatecurrentPage:number=1List(){ForEach(this.items,(item:DataItem)=>{ListItem(){// 列表项内容}},(item:DataItem)=>item.id)// 底部加载指示器if(this.hasMore){ListItem(){Row(){LoadingProgress().width(20).height(20).margin({right:8})Text('加载更多...').fontSize(14).fontColor('#999999')}.width('100%').height(50).justifyContent(FlexAlign.Center)}}else{ListItem(){Text('没有更多了').fontSize(14).fontColor('#cccccc').width('100%').textAlign(TextAlign.Center).padding(16)}}}.onReachEnd(()=>{if(!this.isLoadingMore&&this.hasMore){this.loadMore();}})onReachEnd在列表滚动到底部时触发。isLoadingMore防止重复触发,hasMore判断是否还有更多数据。
loadMore的实现:
privateasyncloadMore():Promise<void>{this.isLoadingMore=true;this.currentPage++;try{letnewItems=awaitthis.fetchData(this.currentPage);if(newItems.length===0){this.hasMore=false;}else{this.items=this.items.concat(newItems);}}catch(e){this.currentPage--;}this.isLoadingMore=false;}concat合并新旧数据。新数据为空说明到底了,设hasMore=false。
手动触发加载更多
onReachEnd在快速滚动时可能不触发。可以用onScrollIndex作为补充:
.onScrollIndex((start:number,end:number)=>{if(end>=this.items.length-3&&!this.isLoadingMore&&this.hasMore){this.loadMore();}})距离底部3条数据时提前加载,实现"预加载"效果。用户看到最后几条时新数据已经准备好了。
空状态处理
首次加载和刷新后都可能有空结果:
if(this.items.length===0){Column(){if(this.isFirstLoad){LoadingProgress().width(40).height(40).margin({bottom:16})Text('加载中...').fontSize(14).fontColor('#999999')}else{Image($r('app.media.empty_state')).width(120).height(120).margin({bottom:16})Text('暂无数据').fontSize(16).fontColor('#999999').margin({bottom:8})Text('点击重试').fontSize(14).fontColor('#1a73e8').onClick(()=>{this.isRefreshing=true;this.loadData();})}}.width('100%').height('100%').justifyContent(FlexAlign.Center)}else{Refresh({refreshing:$$this.isRefreshing}){List(){// 列表内容}}.onRefreshing(()=>{this.loadData();})}首次加载显示loading,加载完成无数据显示空状态+重试按钮。有数据时才显示列表。
刷新与加载的协调
下拉刷新应该重置分页状态:
privateasyncloadData():Promise<void>{this.currentPage=1;this.hasMore=true;try{this.items=awaitthis.fetchData(1);this.refreshState='success';}catch(e){this.refreshState='failed';}setTimeout(()=>{this.isRefreshing=false;this.refreshState='idle';},500);}刷新时currentPage重置为1,hasMore恢复true,items用新数据替换(不是concat)。加载更多时才concat追加。
关键:刷新和加载更多不能同时进行。在isRefreshing为true时禁止触发loadMore,在isLoadingMore为true时不响应onRefreshing。
列表滚动位置记忆
刷新后列表会跳回顶部。要保持位置:
privatescroller:Scroller=newScroller();privatescrollOffset:number=0;List(){// 内容}.scroller(this.scroller).onScroll(()=>{this.scrollOffset=this.scroller.currentOffset().yOffset;})// 刷新完成后恢复位置asyncloadData():Promise<void>{letsavedOffset=this.scrollOffset;this.currentPage=1;this.items=awaitthis.fetchData(1);this.isRefreshing=false;// 恢复滚动位置setTimeout(()=>{this.scroller.scrollTo({xOffset:0,yOffset:savedOffset});},100);}刷新前记录currentOffset,刷新后scrollTo恢复。setTimeout 100ms等UI更新完成再滚动。
但大多数应用刷新后跳回顶部是预期行为——用户下拉刷新就是想看最新内容。只有"静默刷新"(后台刷新不跳位置)才需要记忆位置。
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| 刷新动画收不起来 | isRefreshing没设回false | loadData完成后设false |
| 下拉没有弹性 | 没用Refresh组件 | 用Refresh包裹List |
| 加载更多重复触发 | 没用isLoadingMore防重 | 加布尔锁 |
| 没有更多数据还触发 | hasMore没判断 | 加hasMore标记 |
| 刷新时触发加载更多 | 两个状态没互斥 | isRefreshing时禁止loadMore |
| onReachEnd不触发 | LazyForEach缓存不够 | 用onScrollIndex提前触发 |
| 空状态没显示 | 只判断items空没判断加载状态 | 区分首次加载和加载完成 |
| 刷新后列表跳顶 | 这是默认行为 | 如需保持用scroller记忆位置 |
| 自定义builder不显示 | builder参数写法错 | builder: this.xxxBuilder() |
| 下拉进度不准确 | onPullingDown的progress是0~1 | progress>=0.6为可触发 |
下拉刷新用Refresh+自定义builder,上拉加载用onReachEnd+分页控制。两者共享数据源但状态独立——刷新重置分页,加载追加数据。记住这个原则,任何列表的刷新加载都能搞定。