一、引言
在移动应用中,"下拉刷新"是最核心的交互模式之一。无论是微博首页的最新动态、微信朋友圈的新鲜内容、还是资讯 App 的实时新闻,用户都习惯性地下滑列表顶部,期待看到最新的数据更新。"下拉→加载→新内容出现"这个流畅的动作序列,已经成为移动端内容消费的肌肉记忆。
然而,实现下拉刷新并非简单地在列表顶部挂一个加载动画。开发者需要精确处理:下拉距离与视觉反馈的对应关系(下拉多远才触发刷新?回弹动画如何衔接?)、刷新状态管理(空闲→拖动→刷新中→完成→重置)、手势冲突(列表自身的滚动与下拉手势如何区分?)、以及多场景适配(列表为空时要不要显示刷新?网络失败时如何提示?)。
而 HarmonyOS 提供了Refresh组件——一个专用于下拉刷新的容器组件。它包裹在列表内容外层,自动处理手势识别、回弹动画和加载状态切换。开发者只需声明refreshing状态(通过$$双向绑定)并在onRefreshing回调中执行数据加载逻辑,即可获得完整的下拉刷新体验。
本文通过一个"动态资讯"Demo 深入讲解 Refresh 组件的核心用法:如何创建下拉刷新列表?refreshing状态如何通过$$双向绑定?onRefreshing回调如何配合数据加载?以及如何实现手动刷新、展开详情等完整交互。
阅读完本文,你将能够:
- 使用 Refresh 组件实现下拉刷新功能
- 使用
$$双向绑定管理 refreshing 状态 - 在
onRefreshing回调中执行异步数据加载 - 将 Refresh 与 List 组件结合构建资讯流
- 实现手动刷新按钮作为下拉刷新的补充交互
二、Refresh 组件 API 总览
2.1 构造函数
Refresh(options:RefreshOptions){// 子内容:通常是一个 List、Grid 或 Scroll}interfaceRefreshOptions{refreshing:boolean;// 当前的刷新状态}| 参数 | 类型 | 说明 |
|---|---|---|
refreshing | boolean | true 时显示刷新动画(loading 旋转图标),false 时隐藏。通常使用$$前缀实现双向绑定 |
$$是 ArkUI 提供的双向绑定语法糖,格式为$$this.stateVar。它同时完成了两件事:将stateVar的值传递给组件,以及当组件内部改变该值时自动同步回stateVar。在 Refresh 中,用户下拉超过阈值时组件会自动将refreshing设为 true,数据加载完成后开发者手动将其设回 false。
2.2 链式方法
// 刷新状态变化回调(用户下拉触发时调用).onRefreshing(callback:()=>void):RefreshAttribute// 刷新状态变化(包含完整的生命周期状态).onStateChange(callback:(state:RefreshStatus)=>void):RefreshAttribute// 下拉触发阈值距离(vp).offset(value:number|string|Resource):RefreshAttribute// 自定义下拉时的显示内容.refreshContent(content:()=>void):RefreshAttribute| 方法 | 说明 |
|---|---|
.onRefreshing(Callback) | 下拉触发的回调。在此回调中执行数据加载(网络请求、数据库查询等),完成后将 refreshing 设回 false |
.onStateChange(Callback<RefreshStatus>) | 刷新状态的完整生命周期回调,包含 Drag(拖动)、Refreshing(刷新中)、Done(完成)三种状态。用于更细粒度的 UI 反馈(如"释放立即刷新"文案切换) |
.offset(number) | 下拉触发阈值,单位 vp。默认值约为 64vp。设置越大需要下拉越深才触发刷新 |
.refreshContent(CustomBuilder) | 完全自定义下拉区域的显示内容,替代默认的 loading 旋转图标。可嵌入自定义图标、文字提示等 |
2.3 RefreshStatus 枚举
enumRefreshStatus{Inactive,// 未激活(空闲状态)Drag,// 正在拖动(下拉中,未达阈值)OverDrag,// 超过阈值(可释放触发刷新)Refresh,// 正在刷新(加载动画显示中)Done// 刷新完成}| 状态 | 说明 | UI 反馈示例 |
|---|---|---|
Inactive | 空闲,列表未下拉 | 无特殊指示 |
Drag | 正在下拉,未超过阈值 | “下拉刷新” 文字 |
OverDrag | 超过阈值,松手即触发 | “释放立即刷新” 文字 + 箭头翻转动画 |
Refresh | 正在加载数据 | loading 旋转图标 + “加载中…” |
Done | 加载完成,即将收起 | loading 消失,内容区域回弹 |
Demo 中使用了onRefreshing(核心回调)管理加载逻辑,考虑到 Demo 的简洁性未使用onStateChange。对于需要"松手刷新"提示文案的产品级应用,建议使用onStateChange监听 Drag 和 OverDrag 状态。
2.4 Refresh 与手动刷新按钮的配合
Refresh 组件的refreshing状态默认为 false。当开发者通过代码设置this.isRefreshing = true(例如点击"手动刷新"按钮),即使没有下拉手势,Refresh 组件也会进入刷新状态(显示 loading 动画),等待加载完成后设回 false。
这意味着一套刷新逻辑(doRefresh()方法)可以同时服务于两个触发源:下拉手势(通过onRefreshing回调调用)和手动按钮(通过onClick直接调用)。这是 Demo 中的关键设计——用户既可以通过下拉刷新获取最新资讯,也可以点击顶部"手动刷新"按钮触发加载。
三、Demo 设计:动态资讯
3.1 功能概述
Demo 是一个"动态资讯"应用,模拟资讯类 App 的新闻信息流:
- 资讯列表:15 条预置新闻内容池,每次加载随机抽取。每条新闻包含分类标签、来源、发布时间、标题和摘要
- 下拉刷新:下拉列表顶部触发刷新,loading 1.5 秒后新增 3 条随机新闻插入列表头部
- 手动刷新:顶部状态栏"手动刷新"按钮触发相同加载逻辑
- 上次刷新时间:每次刷新后更新时间,格式为"HH:MM"
- 展开/收起详情:点击"展开全文"查看新闻摘要,点击"收起"折叠
- 列表滚动:Refresh 包裹的 List 支持完整滚动交互
3.2 交互点
| # | 交互 | 说明 |
|---|---|---|
| 1 | 下拉刷新 | 下滑列表顶部触发 onRefreshing → 加载 3 条新资讯 → 插入列表头部 |
| 2 | 手动刷新 | 点击"手动刷新"按钮,同样触发加载逻辑,更新刷新时间 |
| 3 | 展开详情 | 点击新闻卡片"展开全文"→ 显示完整摘要 → "收起"折叠 |
| 4 | 滚动浏览 | List 中滚动浏览全部资讯,分类标签和文章来源辅助筛选 |
四、完整代码实现
4.1 数据模型与状态
interfaceNewsItem{id:number;title:string;summary:string;source:string;time:string;category:string;catColor:string;}@StateisRefreshing:boolean=false;@StatelastRefreshTime:string='暂无';@StatenewsList:NewsItem[]=[];@StateexpandedId:number=-1;privatenewsPool:NewsItem[]=[];// 15 条预置新闻NewsItem包含新闻的完整信息:标题、摘要(展开后显示)、来源、发布时间、分类(带颜色标签)。newsPool存储全部 15 条预置新闻作为数据池,newsList是当前显示的列表(初始 5 条随机新闻)。expandedId记录当前展开的新闻 ID(-1 表示无展开)。
4.2 Refresh 包裹 List
Refresh({refreshing:$$this.isRefreshing}){List(){ForEach(this.newsList,(news:NewsItem,idx:number)=>{ListItem(){Column(){Row(){Text(news.category).fontSize(10).fontColor('#FFFFFF').fontWeight(FontWeight.Bold).padding({top:2,bottom:2,left:6,right:6}).borderRadius(4).backgroundColor(news.catColor)Text(news.source).fontSize(11).fontColor('#BBBBCC').margin({left:8})Blank()Text(news.time).fontSize(11).fontColor('#BBBBCC')}.width('100%')Text(news.title).fontSize(16).fontColor('#1a1a2e').fontWeight(FontWeight.Medium).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis}).width('100%').margin({top:10,bottom:this.expandedId===news.id?10:6})if(this.expandedId===news.id){Text(news.summary).fontSize(13).fontColor('#666677').lineHeight(20).width('100%').margin({bottom:10})}Row(){Text(this.expandedId===news.id?'收起':'展开全文').fontSize(11).fontColor('#1677FF').fontWeight(FontWeight.Medium).onClick(()=>{this.toggleExpand(news.id);})}}.width('100%').padding({left:Spacing.LG,right:Spacing.LG,top:14,bottom:14})}.border({width:{bottom:0.5},color:'#F2F3F5'})},(news:NewsItem,idx:number)=>news.id.toString())}.width('100%').height('100%').scrollBar(BarState.Off)}.onRefreshing(()=>{this.doRefresh();})逐行解析:
Refresh({ refreshing: $$this.isRefreshing }):$$双向绑定isRefreshing状态。用户下拉超过阈值时,组件自动将isRefreshing设为 true,触发 loading 动画;加载完成后开发者将isRefreshing设回 falseList():Refresh 内部包裹一个 List 组件作为可滚动内容。List 每项是ListItem(),通过ForEach从newsList数据数组生成.onRefreshing(() => { this.doRefresh(); }):下拉触发回调,调用doRefresh()执行数据加载- 展开/收起逻辑:
expandedId记录当前展开项,toggleExpand()切换expandedId。展开时显示news.summary(约 80~100 字摘要),折叠时仅显示标题
4.3 数据加载逻辑
doRefresh():void{this.isRefreshing=true;setTimeout(()=>{constfreshNews=this.randomNews(3);constnewList:NewsItem[]=[];for(leti=0;i<freshNews.length;i++){newList.push(freshNews[i]);}for(leti=0;i<this.newsList.length;i++){newList.push(this.newsList[i]);}this.newsList=newList;constnow=newDate();this.lastRefreshTime=now.getHours().toString().concat(':',now.getMinutes().toString().length===1?'0'.concat(now.getMinutes().toString()):now.getMinutes().toString());this.isRefreshing=false;},1500);}doRefresh()是核心加载方法,既由onRefreshing回调触发(下拉手势),也由"手动刷新"按钮的onClick触发:
- 设置刷新状态:
this.isRefreshing = true激活 loading 动画 - 模拟网络请求:
setTimeout延迟 1.5 秒模拟网络加载延迟 - 随机抽取新内容:
randomNews(3)从 15 条预置新闻中随机抽取 3 条不重复的新闻 - 插入列表头部:将新 3 条新闻放在
newList头部,原有内容拼接在后(newList.push(this.newsList[i])) - 更新刷新时间:获取当前时间的"HH:MM"格式,处理分钟补零(如 9:05)
- 结束刷新:
this.isRefreshing = false关闭 loading 动画,Refresh 组件自动播放回弹动画
需要注意:数组更新遵循 ArkUI 的不可变数据模式——创建新的数组newList而非修改原数组,确保@State能检测到变化并触发 UI 更新。
4.4 手动刷新按钮
Row(){Text('上次刷新:'.concat(this.lastRefreshTime)).fontSize(11).fontColor('#9999AA')Blank()Text('手动刷新').fontSize(11).fontColor('#1677FF').fontWeight(FontWeight.Medium).padding({top:3,bottom:3,left:10,right:10}).borderRadius(10).backgroundColor('#EEF3FF').onClick(()=>{this.doRefresh();})}.width('100%').height(36).padding({left:Spacing.LG,right:Spacing.LG}).backgroundColor('#F2F3F5')状态栏左侧显示"上次刷新:HH:MM"提供时间参考,右侧"手动刷新"按钮直接调用doRefresh()。这展示了 Refresh 组件的一个重要特性:刷新逻辑与触发方式解耦。同一个doRefresh()方法,通过下拉手势触发和按钮点击触发都能正确工作——因为$$双向绑定确保了两种触发路径下isRefreshing状态的一致性。
4.5 新闻展开/折叠
toggleExpand(id:number):void{if(this.expandedId===id){this.expandedId=-1;// 收起:清除展开状态}else{this.expandedId=id;// 展开:记录当前展开项}}toggleExpand()实现"手风琴"式展开行为——同一时间只有一个新闻项处于展开状态。点击新的"展开全文"会自动收起之前展开的项。展开时标题下方的 margin 从 6vp 扩大到 10vp,为摘要文本留出视觉间距。
五、关键技术点详解
5.1 $$ 双向绑定的底层机制
$$this.isRefreshing是 ArkUI 声明式框架的核心特性之一。它等价于同时做两件事:
- 属性传递:
refreshing: this.isRefreshing—— 将当前isRefreshing的值传给 Refresh 组件 - 事件监听:当 Refresh 组件内部改变
refreshing时(如下拉触发),自动调用this.isRefreshing = newValue同步回状态变量
需要注意的是,$$只能用于@State、@Prop、@Link等可观察状态变量,不能用于普通private变量。此外,在$$模式下,开发者不应在onRefreshing回调中再次设置isRefreshing = true(因为组件已经自动设置了),而应该在数据加载完成后设置isRefreshing = false。
不过 Demo 中doRefresh()同时服务于下拉和手动按钮,所以包含了isRefreshing = true。对于下拉触发的路径,这个赋值是冗余的(一次无副作用的相同值写入),对于按钮触发的路径则是必要的。
5.2 onRefreshing 的触发时机
onRefreshing在下拉距离超过 Refresh 组件的触发阈值时自动调用。具体触发流程:
- 用户在列表顶部继续向下拖动
- 下拉距离逐渐增加,Refresh 组件内部计算偏移量
- 当偏移量超过阈值(默认约 64vp),组件判定用户意图为"刷新"
- 组件内部将
refreshing设为 true(通过$$同步到状态变量) - 调用
onRefreshing回调 - 开发者执行数据加载,完成后将
refreshing设回 false
如果在步骤 4 之前用户松手(下拉不够深),Refresh 组件会自动播放回弹动画回到顶部,不会触发刷新。这个"下拉距离阈值"可以通过.offset(value)自定义,单位为 vp。
5.3 列表为空时的刷新行为
当列表内容为空(newsList为空数组)时,Refresh 组件仍然可以触发刷新。此时的交互略有不同:
- 有内容时:用户需要将列表滚动到顶部再继续下拉(两次手势——先上滑、再下拉),或者列表本身就在顶部时直接下拉
- 空列表时:Refresh 占据整个容器空间,用户在任何位置下拉都能触发刷新
这意味着 Refresh 在空列表场景下更加灵敏——非常适合"首次加载"和"数据清空后重新加载"的场景。Demo 中初始状态有 5 条新闻(不为空),但删除所有新闻后列表为空,Refresh 依然能正确触发。
5.4 手动刷新与下拉刷新的 UX 互补
在产品设计中,不应仅依赖下拉刷新。以下是常见的补充触发方式:
| 触发方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 下拉刷新 | 列表在顶部时 | 符合直觉,手势自然 | 需要将列表滚到顶部才能触发 |
| 手动按钮(Demo 中使用) | 任意滚动位置 | 无需滚回顶部,即时触发 | 占用额外屏幕空间 |
| 自动轮询 | 实时性要求高的场景(聊天、交易) | 零用户操作 | 耗电、耗流量 |
| 长按菜单"刷新" | 工具栏/TabBar 中的刷新选项 | 不需额外空间 | 发现性差 |
Demo 中使用"下拉刷新 + 手动按钮"双触发模式:快速浏览时随手势下拉刷新(自然),精确操作时点击按钮(快捷)。
5.5 自定义 refreshContent
默认的刷新动画是一个简单的 loading 旋转图标。对于品牌化需求,可以使用.refreshContent()自定义下拉区域的显示内容:
Refresh({refreshing:$$this.isRefreshing}){List(){...}}.refreshContent(()=>{this.customRefreshBuilder()})customRefreshBuilder可以是包含图标、文字、动画的任意组件组合。例如"下拉刷新" / "释放立即刷新"的状态切换 + 品牌 Logo 动画。Demo 中使用默认样式以保持简洁,但产品级应用建议自定义以增强品牌识别度。
5.6 性能考量:高频刷新与数据更新策略
刷新操作可能被高频触发(用户反复下拉),需要注意以下性能点:
防抖(Debounce):如果
onRefreshing中触发的是真实网络请求,建议加入防抖逻辑——在刷新动画进行中时忽略新的刷新请求。Demo 中isRefreshing本身起到了一定防护作用(在true状态时重复触发doRefresh()不会产生新动画)不可变数据更新:列表数据更新时必须使用新数组(
newList = [...])而非this.newsList.push(item),否则@State无法检测到变化导致 UI 不刷新。Demo 中演示了正确的不可变更新模式列表 key 函数:
ForEach的第三个参数(key 函数)帮助框架识别哪些列表项发生了变化、哪些可以复用。Demo 中使用news.id.toString()作为唯一 key,确保新增项正确渲染、已有项节点复用控制列表长度:实际产品中列表可能包含数百条数据。每次刷新插入新数据后,如果列表无限增长,应考虑分页加载 + 旧数据清理策略(如保留最近 50 条)
六、运行效果
6.1 初始状态
进入"动态资讯"页面,顶部状态栏显示"上次刷新:暂无"和蓝色"手动刷新"按钮。下方白色说明卡片介绍 Refresh 组件。资讯列表展示 5 条随机新闻,每条包含彩色分类标签(蓝/绿/橙/紫/红)、来源名称(如"华为官方"“开发者社区”)、发布时间和标题。底部有 0.5vp 分隔线。
6.2 下拉刷新
将列表滑到顶部,继续向下拖动 → Refresh 组件检测到超过阈值 → loading 旋转图标出现在列表上方 →onRefreshing触发doRefresh()→ 1.5 秒后 3 条新随机新闻插入列表头部 → loading 消失、列表回弹 → 上次刷新时间更新为当前 HH:MM。
6.3 手动刷新
滚动到列表中间位置 → 点击顶部"手动刷新"按钮 → loading 动画立即显示(无需下拉手势)→ 1.5 秒后 3 条新新闻插入 → 刷新时间更新 → loading 消失。手动刷新与下拉刷新使用完全相同的doRefresh()逻辑,效果一致。
6.4 展开详情
点击任意新闻的"展开全文"→ 该条新闻扩展显示完整摘要(约 80~100 字中文描述),“展开全文"变为"收起”。点击"收起"→ 摘要折叠,仅显示标题。点击另一条新闻的"展开全文"→ 之前展开的自动折叠,新点击的展开(同一时间仅一项展开)。
6.5 多次刷新效果
连续点击 3 次"手动刷新"→ 每次插入 3 条,列表顶部累积 9 条新新闻。滚动浏览所有内容 → 分类标签颜色帮助快速区分新闻类型。列表流畅滚动无卡顿。
七、总结
本文通过一个"动态资讯"实战 Demo,深入讲解了 HarmonyOS Refresh 下拉刷新组件的核心用法:
- Refresh 容器:包裹 List 内容,自动处理下拉手势识别和回弹动画
$$双向绑定:$$this.isRefreshing实现刷新状态与 Refresh 组件的自动同步,组件感知状态变化,状态反映组件内部变化onRefreshing回调:下拉触发的数据加载入口,在此执行异步加载并在完成后将 refreshing 设回 false- 手动刷新:按钮直接调用同一
doRefresh()方法,展示"一套逻辑、多种触发"的设计模式 - 不可变数据更新:每次刷新创建新数组插入头部,确保
@State检测到变化触发 UI 更新
Refresh 将"手势识别→阈值判断→加载动画→数据更新→回弹收起"这一整套下拉刷新的交互流程封装进一个容器组件,开发者只需声明状态绑定和加载回调即可获得完整的下拉刷新体验。希望本文能帮助你在实际项目中高效运用 Refresh 组件。
本文基于 HarmonyOS NEXT API 24 编写,代码经 DevEco Studio 6.1.1 编译验证通过。