鸿蒙原生ArkTS布局方式之Refresh+List下拉刷新布局深度解析
项目演示



一、引言
在移动应用开发中,下拉刷新是一种极为常见的交互模式,它允许用户通过简单的手势操作获取最新数据。HarmonyOS NEXT 作为华为新一代操作系统,提供了强大的 ArkTS 语言和 ArkUI 框架,使得实现下拉刷新功能变得简洁而高效。
本文将深入探讨鸿蒙原生 ArkTS 布局方式中的 Refresh + List 下拉刷新布局,详细讲解其核心原理、API 用法、实现步骤、高级特性以及最佳实践。本文基于 HarmonyOS 6.1.1 (API 24) 版本,确保内容的时效性和准确性。
二、核心组件概述
2.1 Refresh 组件
Refresh 组件是鸿蒙系统提供的一个容器组件,专门用于实现下拉刷新功能。它可以包裹任意子组件,为其提供下拉刷新能力。
基本特性:
- 单子组件限制:Refresh 组件只能包含一个直接子组件。如果需要包含多个组件,应先将它们包裹在 Column、Row 等容器中。
- 双向绑定支持:通过
$$运算符实现刷新状态的双向绑定,确保组件状态与数据模型保持同步。 - 状态回调:提供丰富的状态变化回调,允许开发者监听刷新的各个阶段。
- 自定义支持:支持自定义刷新区域的显示内容和样式。
核心属性(API 24):
| 属性名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| refreshing | boolean | 是 | 当前是否处于刷新状态,支持双向绑定 |
| offset | number | string | 否 | 刷新组件静止时距离父组件顶部的距离,默认值为 16vp |
| friction | number | string | 否 | 下拉摩擦系数,取值范围为 0-100,默认值为 62 |
| promptText | ResourceStr | 否(API 12+) | 设置刷新区域底部显示的自定义文本 |
| refreshingContent | CustomBuilder | 否(API 12+) | 自定义刷新区域的内容 |
核心事件(API 24):
| 事件名 | 参数 | 说明 |
|---|---|---|
| onRefreshing | () => void | 进入刷新状态时触发回调 |
| onStateChange | (state: RefreshStatus) => void | 当前刷新状态变更时触发回调 |
| onOffsetChange | (offset: number) => void | 下拉偏移量变化时触发回调(API 12+) |
RefreshStatus 枚举值:
| 枚举值 | 说明 |
|---|---|
| Inactive | 默认未下拉状态 |
| Drag | 下拉中,下拉距离小于刷新距离 |
| OverDrag | 下拉中,下拉距离超过刷新距离 |
| Refresh | 下拉结束,回弹至刷新距离,进入刷新状态 |
| Done | 刷新结束,返回初始状态(顶部) |
2.2 List 组件
List 组件是鸿蒙系统中用于展示列表数据的核心组件,支持垂直和水平滚动,具备高性能的列表项复用机制。
基本特性:
- 高性能渲染:采用懒加载机制,只渲染可视区域内的列表项,显著提升性能。
- 列表项复用:支持列表项的复用,减少内存占用和渲染开销。
- 滚动控制:提供丰富的滚动控制能力,包括滚动到指定位置、滚动监听等。
- 分割线支持:内置分割线样式配置,无需手动绘制。
核心属性(API 24):
| 属性名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| scroller | Scroller | 否 | 滚动控制器,用于控制列表滚动行为 |
| space | number | 否 | 列表项之间的间距 |
| initialIndex | number | 否 | 初始滚动位置的索引 |
| listDirection | Axis | 否 | 滚动方向,默认值为 Axis.Vertical |
核心事件(API 24):
| 事件名 | 参数 | 说明 |
|---|---|---|
| onScroll | (scrollOffset: number) => void | 滚动时触发回调 |
| onScrollIndex | (firstIndex: number, lastIndex: number) => void | 滚动时触发,返回当前可视区域的第一个和最后一个列表项索引 |
| onReachStart | () => void | 滚动到列表顶部时触发 |
| onReachEnd | () => void | 滚动到列表底部时触发 |
2.3 ForEach 组件
ForEach 是鸿蒙声明式 UI 中的迭代渲染组件,用于根据数据源动态生成 UI 组件。
基本特性:
- 高效渲染:支持数据变化时的增量更新,只重新渲染变化的部分。
- 唯一标识:必须提供唯一标识生成函数,用于列表项的复用和识别。
- 条件渲染:支持根据条件选择性渲染列表项。
核心参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| arr | Array | 是 | 要遍历的数据源数组 |
| itemGenerator | (item: any, index: number) => void | 是 | 列表项生成函数 |
| keyGenerator | (item: any, index: number) => string | 是 | 唯一标识生成函数 |
三、状态管理机制
3.1 @State 装饰器
@State 是 ArkTS 中最基础的状态管理装饰器,用于管理组件内部的状态数据。当 @State 装饰的变量发生变化时,系统会自动触发组件的重新渲染。
使用方式:
@State refreshing: boolean = false;
@State listData: ListItemData[] = [];
核心特点:
- 局部状态:
@State装饰的变量仅在当前组件内部有效,不会影响父组件或子组件。 - 响应式更新:当变量值发生变化时,依赖该变量的 UI 组件会自动更新。
- 不可变更新:对于数组和对象类型,建议通过重新赋值(创建新对象)的方式触发更新,避免直接修改原对象。
3.2 双向绑定机制
在 Refresh 组件中,refreshing 属性支持双向绑定,使用 $$ 运算符实现。
单向绑定 vs 双向绑定:
- 单向绑定:
refreshing: this.refreshing,仅将组件状态同步到数据模型,数据模型变化不会影响组件状态。 - 双向绑定:
refreshing: $$this.refreshing,实现组件状态与数据模型的双向同步。
双向绑定的优势:
- 自动状态同步:当用户下拉触发刷新时,Refresh 组件会自动将
refreshing设为true。 - 简化代码:无需在
onRefreshing回调中手动设置refreshing = true。 - 状态一致性:确保组件状态与数据模型始终保持一致,避免状态错乱。
3.3 数据更新策略
在实现下拉刷新时,数据更新策略至关重要。常见的策略包括:
策略一:数据替换
this.listData = newData;
直接用新数据替换旧数据,适用于刷新后数据完全更新的场景。
策略二:数据追加(头部)
this.listData = newData.concat(this.listData);
将新数据添加到列表头部,适用于获取最新数据后展示在最前面的场景。
策略三:数据追加(尾部)
this.listData = this.listData.concat(newData);
将新数据添加到列表尾部,适用于上拉加载更多的场景。
策略四:增量更新
const newItems: ListItemData[] = [];
for (let i = 0; i < newData.length; i++) {
const exists = this.listData.some(item => item.id === newData[i].id);
if (!exists) {
newItems.push(newData[i]);
}
}
this.listData = newItems.concat(this.listData);
仅添加不存在的数据,适用于需要去重的场景。
四、完整实现步骤
4.1 项目初始化
首先,确保你的项目已正确配置为 HarmonyOS NEXT API 24 项目。
build-profile.json5 配置:
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
entry/build-profile.json5 配置:
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
}
}
4.2 创建数据模型
定义列表数据项的接口:
interface ListItemData {
id: number;
title: string;
subtitle: string;
}
接口设计要点:
- id 字段:必须提供唯一标识,用于 ForEach 的 keyGenerator 函数。
- 字段类型:使用明确的类型标注,避免使用 any 或 unknown。
- 扩展性:预留必要的字段,便于后续功能扩展。
4.3 实现页面组件
创建完整的下拉刷新列表页面:
/**
* Refresh + List 下拉刷新布局示例
* API 版本:HarmonyOS 6.1.1 (API 24)
*/
interface ListItemData {
id: number;
title: string;
subtitle: string;
}
@Entry
@Component
struct Index {
/**
* 刷新状态标志位
* true 表示正在刷新,Refresh组件会显示刷新动画
* false 表示刷新完成或未处于刷新状态
*/
@State refreshing: boolean = false;
/**
* 列表数据数组
* 存储要展示的列表项数据
*/
@State listData: ListItemData[] = [];
/**
* 组件初始化时调用
* 用于初始化列表数据
*/
aboutToAppear() {
this.loadInitialData();
}
/**
* 加载初始数据
* 生成模拟数据用于展示列表
*/
loadInitialData() {
let tempData: ListItemData[] = [];
for (let i: number = 1; i <= 20; i++) {
tempData.push({
id: i,
title: `列表项 ${i}`,
subtitle: `这是第 ${i} 个列表项的描述信息`
});
}
this.listData = tempData;
}
/**
* 刷新数据方法
* 模拟网络请求延迟,生成新数据并添加到列表顶部
*/
refreshData() {
// 使用$$this.refreshing双向绑定后,Refresh组件会自动将refreshing设为true
// 模拟网络请求延迟(2秒)
setTimeout(() => {
// 生成新数据(添加到列表顶部)
let newData: ListItemData[] = [];
let maxId: number = this.listData.length > 0 ? this.listData[0].id : 0;
for (let i: number = 1; i <= 5; i++) {
newData.push({
id: maxId + i,
title: `新列表项 ${maxId + i}`,
subtitle: `这是刷新后新增的第 ${i} 个列表项`
});
}
// 将新数据添加到原有数据前面
this.listData = newData.concat(this.listData);
// 设置刷新状态为false,停止刷新动画
this.refreshing = false;
}, 2000);
}
/**
* 构建页面布局
* Refresh 包裹 List,实现下拉刷新功能
*/
build() {
// 最外层容器,占据整个屏幕
Column() {
// 页面标题区域
Text('下拉刷新列表')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#181818')
.margin({ top: 20, bottom: 10 });
// 说明文字
Text('在列表区域向下滑动即可触发刷新')
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 10 });
/**
* Refresh 组件:提供下拉刷新能力
* - refreshing:控制刷新状态,使用 $$this.refreshing 实现双向绑定
* - onRefreshing:进入刷新状态时触发的回调函数
* - layoutWeight(1):填充标题下方剩余的空间,避免布局溢出
*/
Refresh({
refreshing: $$this.refreshing
}) {
/**
* List 组件:展示可滚动的列表数据
* - space:列表项之间的间距
*/
List({ space: 10 }) {
/**
* ForEach:遍历数组生成列表项
* - 参数1:要遍历的数据数组
* - 参数2:生成唯一标识的函数(必须,用于列表项复用优化)
* - 参数3:渲染每一项的函数
*/
ForEach(this.listData, (item: ListItemData) => {
/**
* ListItem:列表项组件
* 作为List的直接子组件使用,提供点击反馈和布局容器
*/
ListItem() {
// 列表项内容布局
Row() {
// 图标占位区域
Column() {
Text(item.id.toString())
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#ffffff');
}
.width(50)
.height(50)
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.backgroundColor('#007DFF');
// 文本内容区域
Column() {
// 标题
Text(item.title)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#181818');
// 副标题
Text(item.subtitle)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 4 });
}
.padding({ left: 12 });
}
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.width('100%')
.backgroundColor('#ffffff')
.borderRadius(12);
}
// 设置列表项点击事件
.onClick(() => {
console.info(`点击了列表项: ${item.title}`);
});
}, (item: ListItemData) => item.id.toString());
}
// 设置List的宽度和高度
.width('100%')
.height('100%')
// 设置List的内边距
.padding({ left: 16, right: 16 })
// 设置分割线样式
.divider({
strokeWidth: 1,
color: '#f5f5f5',
startMargin: 16,
endMargin: 16
});
}
.onRefreshing(() => {
this.refreshData();
})
// 使用layoutWeight(1)填充剩余空间,避免布局溢出
.layoutWeight(1)
.width('100%');
}
// 设置最外层容器的背景色
.backgroundColor('#f8f8f8')
// 设置最外层容器占据全屏
.width('100%')
.height('100%');
}
}
4.4 代码详解
4.4.1 组件结构
@Entry
@Component
struct Index {
// ...
}
@Entry:标记该组件为应用入口组件。@Component:标记该组件为可复用的 UI 组件。
4.4.2 状态定义
@State refreshing: boolean = false;
@State listData: ListItemData[] = [];
refreshing:控制刷新动画的显示与隐藏。listData:存储列表数据,当数据变化时触发列表重新渲染。
4.4.3 生命周期方法
aboutToAppear() {
this.loadInitialData();
}
aboutToAppear():组件即将显示时调用,用于初始化数据。
4.4.4 数据加载方法
loadInitialData() {
let tempData: ListItemData[] = [];
for (let i: number = 1; i <= 20; i++) {
tempData.push({
id: i,
title: `列表项 ${i}`,
subtitle: `这是第 ${i} 个列表项的描述信息`
});
}
this.listData = tempData;
}
- 生成 20 条模拟数据,用于初始展示。
- 通过重新赋值触发状态更新,确保列表正确渲染。
4.4.5 刷新逻辑
refreshData() {
setTimeout(() => {
let newData: ListItemData[] = [];
let maxId: number = this.listData.length > 0 ? this.listData[0].id : 0;
for (let i: number = 1; i <= 5; i++) {
newData.push({
id: maxId + i,
title: `新列表项 ${maxId + i}`,
subtitle: `这是刷新后新增的第 ${i} 个列表项`
});
}
this.listData = newData.concat(this.listData);
this.refreshing = false;
}, 2000);
}
- 使用
setTimeout模拟网络请求延迟(2 秒)。 - 生成 5 条新数据,添加到列表顶部。
- 更新数据后将
refreshing设为false,停止刷新动画。
4.4.6 布局构建
build() {
Column() {
// 标题区域
// ...
Refresh({
refreshing: $$this.refreshing
}) {
List({ space: 10 }) {
ForEach(this.listData, (item: ListItemData) => {
ListItem() {
// 列表项布局
}
}, (item: ListItemData) => item.id.toString());
}
}
.onRefreshing(() => {
this.refreshData();
})
.layoutWeight(1);
}
.width('100%')
.height('100%');
}
- Column:最外层容器,垂直排列子组件。
- Refresh:包裹 List 组件,提供下拉刷新能力。
- List:展示列表数据,设置列表项间距和分割线。
- ForEach:遍历数据数组,生成列表项。
- ListItem:定义列表项的布局和样式。
五、高级特性与自定义
5.1 自定义刷新提示文本
API 12+ 支持自定义刷新区域显示的文本:
@State promptText: string = '下拉刷新';
Refresh({
refreshing: $$this.refreshing,
promptText: this.promptText
}) {
// ...
}
.onStateChange((refreshStatus: RefreshStatus) => {
switch (refreshStatus) {
case RefreshStatus.Drag:
this.promptText = '下拉刷新数据';
break;
case RefreshStatus.OverDrag:
this.promptText = '松开立即刷新';
break;
case RefreshStatus.Refresh:
this.promptText = '正在刷新...';
break;
case RefreshStatus.Done:
this.promptText = '刷新完成';
break;
default:
this.promptText = '下拉刷新';
}
});
实现效果:
- Drag 状态:用户正在下拉,显示"下拉刷新数据"。
- OverDrag 状态:下拉距离超过阈值,显示"松开立即刷新"。
- Refresh 状态:进入刷新状态,显示"正在刷新…"。
- Done 状态:刷新完成,显示"刷新完成"。
5.2 自定义刷新区域内容
API 12+ 支持完全自定义刷新区域的内容:
@Builder
customRefreshContent() {
Column() {
LoadingProgress()
.width(30)
.height(30)
.color('#007DFF');
Text('正在获取最新数据...')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 8 });
}
.padding(16);
}
Refresh({
refreshing: $$this.refreshing,
refreshingContent: this.customRefreshContent
}) {
// ...
}
@Builder 装饰器的作用:
- 代码复用:将复杂的 UI 逻辑封装为可复用的构建函数。
- 代码组织:提高代码可读性,使 build 方法更加简洁。
- 性能优化:Builder 中的代码会被编译为高效的渲染代码。
5.3 调整下拉参数
5.3.1 调整下拉摩擦系数
Refresh({
refreshing: $$this.refreshing,
friction: 80
}) {
// ...
}
friction值越大,下拉时组件跟随手势的反应越灵敏。- 默认值为 62,取值范围为 0-100。
5.3.2 调整刷新偏移量
Refresh({
refreshing: $$this.refreshing,
offset: 100
}) {
// ...
}
offset控制刷新组件静止时距离父组件顶部的距离。- 默认值为 16vp。
5.4 上拉加载更多
结合 List 组件的 onReachEnd 事件实现上拉加载更多功能:
@State isLoadingMore: boolean = false;
@State hasMoreData: boolean = true;
@State currentPage: number = 1;
loadMoreData() {
if (this.isLoadingMore || !this.hasMoreData) {
return;
}
this.isLoadingMore = true;
setTimeout(() => {
let newData: ListItemData[] = [];
let maxId: number = this.listData.length > 0 ? this.listData[this.listData.length - 1].id : 0;
// 模拟每页加载10条数据
for (let i: number = 1; i <= 10; i++) {
newData.push({
id: maxId + i,
title: `列表项 ${maxId + i}`,
subtitle: `这是第 ${maxId + i} 个列表项的描述信息`
});
}
this.listData = this.listData.concat(newData);
this.currentPage++;
// 模拟最多5页数据
if (this.currentPage > 5) {
this.hasMoreData = false;
}
this.isLoadingMore = false;
}, 1500);
}
// 在List组件中添加
List({ space: 10 }) {
ForEach(this.listData, (item: ListItemData) => {
ListItem() {
// ...
}
}, (item: ListItemData) => item.id.toString());
// 加载更多提示
ListItem() {
Row() {
if (this.isLoadingMore) {
LoadingProgress()
.width(20)
.height(20)
.color('#007DFF');
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 8 });
} else if (!this.hasMoreData) {
Text('已加载全部数据')
.fontSize(14)
.fontColor('#999999');
} else {
Text('上拉加载更多')
.fontSize(14)
.fontColor('#999999');
}
}
.padding(16)
.justifyContent(FlexAlign.Center);
}
}
.onReachEnd(() => {
this.loadMoreData();
});
上拉加载更多实现要点:
- 状态控制:使用
isLoadingMore防止重复加载。 - 数据判断:使用
hasMoreData判断是否还有更多数据。 - 分页管理:使用
currentPage管理当前页码。 - 加载提示:根据不同状态显示不同的加载提示信息。
六、性能优化策略
6.1 列表项复用
List 组件内置了列表项复用机制,但需要正确使用 ForEach 的 keyGenerator:
ForEach(this.listData, (item: ListItemData) => {
ListItem() {
// ...
}
}, (item: ListItemData) => item.id.toString());
keyGenerator 的重要性:
- 唯一标识:必须为每个列表项提供唯一标识。
- 复用优化:系统根据 key 识别列表项,避免不必要的创建和销毁。
- 性能提升:减少内存分配和垃圾回收,提高滚动流畅度。
6.2 避免频繁状态更新
在刷新过程中,避免频繁更新状态:
// 错误做法
refreshData() {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
this.listData.push(newItem); // 每次 push 都会触发刷新
}, i * 400);
}
}
// 正确做法
refreshData() {
setTimeout(() => {
let newData: ListItemData[] = [];
for (let i = 0; i < 5; i++) {
newData.push(newItem);
}
this.listData = newData.concat(this.listData); // 一次性更新
}, 2000);
}
优化原则:
- 批量更新:将多次状态更新合并为一次。
- 减少触发:避免在循环中更新状态。
- 异步合并:使用 setTimeout 或 Promise 合并多次更新。
6.3 使用 layoutWeight 优化布局
在嵌套布局中,合理使用 layoutWeight 避免布局溢出:
Column() {
Text('标题')
.margin({ bottom: 10 });
Refresh({
refreshing: $$this.refreshing
}) {
List({ space: 10 }) {
// ...
}
}
.layoutWeight(1) // 填充剩余空间
.width('100%');
}
.width('100%')
.height('100%');
layoutWeight 的作用:
- 空间分配:按比例分配剩余空间。
- 避免溢出:自动适应父容器大小,避免内容溢出。
- 响应式布局:适配不同屏幕尺寸和方向。
6.4 避免在列表项中使用复杂计算
// 错误做法
ListItem() {
Text(`列表项 ${item.id}: ${this.calculateValue(item)}`) // 每次渲染都会执行计算
.fontSize(16);
}
// 正确做法
// 在数据加载时预先计算
interface ListItemData {
id: number;
title: string;
subtitle: string;
calculatedValue: number; // 预先计算的值
}
优化策略:
- 预计算:在数据加载时完成计算。
- 缓存结果:使用变量缓存计算结果。
- 避免动态计算:不在组件渲染时执行复杂计算。
七、常见问题与解决方案
7.1 下拉刷新不触发
问题现象: 用户下拉列表时,刷新动画不显示,onRefreshing 回调也不触发。
可能原因:
- Refresh 组件未正确包裹 List:确保 List 是 Refresh 的直接子组件。
- List 内容不满一屏:当 List 内容不足以滚动时,下拉手势可能被拦截。
- 手势冲突:其他组件的手势可能与 Refresh 的下拉手势冲突。
- 双向绑定未正确使用:使用了
this.refreshing而不是$$this.refreshing。
解决方案:
// 确保 Refresh 直接包裹 List
Refresh({
refreshing: $$this.refreshing // 使用双向绑定
}) {
List({ space: 10 }) {
// ...
}
.edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true }) // 确保手势响应
}
.onRefreshing(() => {
this.refreshData();
});
7.2 刷新状态不更新
问题现象: 刷新完成后,刷新动画仍然显示。
可能原因:
- 未设置 refreshing = false:在刷新完成后未将状态设为 false。
- 异步更新问题:在异步回调中更新状态时出现问题。
- 状态管理错误:使用了错误的状态管理方式。
解决方案:
refreshData() {
setTimeout(() => {
// 更新数据
this.listData = newData.concat(this.listData);
// 必须设置 refreshing = false
this.refreshing = false;
}, 2000);
}
7.3 列表项点击事件不响应
问题现象: 点击列表项时,onClick 回调不触发。
可能原因:
- ListItem 未正确使用:列表项必须使用 ListItem 组件。
- 布局遮挡:其他组件可能遮挡了点击区域。
- 手势冲突:自定义手势可能与点击事件冲突。
解决方案:
ListItem() {
Row() {
// 列表项内容
}
.width('100%')
.padding(16);
}
.onClick(() => {
console.info('点击了列表项');
});
7.4 数据更新后列表不刷新
问题现象: 更新 listData 后,列表内容没有变化。
可能原因:
- 直接修改数组:使用
push、splice等方法直接修改数组。 - 对象引用未变化:修改了数组中的对象属性,但数组引用未变。
- 状态管理错误:未使用
@State装饰器。
解决方案:
// 正确做法:重新赋值数组
this.listData = newData.concat(this.listData);
// 或者创建新数组
let tempList: ListItemData[] = [...this.listData];
tempList.push(newItem);
this.listData = tempList;
八、完整示例:新闻列表应用
8.1 需求分析
创建一个新闻列表应用,具备以下功能:
- 下拉刷新获取最新新闻
- 上拉加载更多历史新闻
- 显示新闻标题、来源、时间和配图
- 支持置顶新闻标识
- 点击新闻可查看详情
8.2 数据模型设计
interface NewsItem {
id: number;
title: string;
source: string;
time: string;
image?: string;
isTop?: boolean;
}
8.3 完整代码实现
interface NewsItem {
id: number;
title: string;
source: string;
time: string;
image?: string;
isTop?: boolean;
}
@Entry
@Component
struct NewsListPage {
@State refreshing: boolean = false;
@State isLoadingMore: boolean = false;
@State hasMoreData: boolean = true;
@State currentPage: number = 1;
@State newsList: NewsItem[] = [];
aboutToAppear() {
this.loadInitialData();
}
loadInitialData() {
let tempData: NewsItem[] = [];
for (let i: number = 1; i <= 15; i++) {
tempData.push({
id: i,
title: `鸿蒙 HarmonyOS NEXT 开发实践 ${i}:构建高性能应用`,
source: '鸿蒙开发者社区',
time: `2026-07-${10 + i % 20}`,
isTop: i === 1 || i === 3
});
}
this.newsList = tempData;
}
refreshData() {
setTimeout(() => {
let newData: NewsItem[] = [];
let maxId: number = this.newsList.length > 0 ? this.newsList[0].id : 0;
for (let i: number = 1; i <= 5; i++) {
newData.push({
id: maxId + i,
title: `【最新】鸿蒙开发新特性 ${maxId + i}:API 24 深度解析`,
source: '华为开发者联盟',
time: '2026-07-10',
isTop: i === 1
});
}
this.newsList = newData.concat(this.newsList);
this.refreshing = false;
}, 2000);
}
loadMoreData() {
if (this.isLoadingMore || !this.hasMoreData) {
return;
}
this.isLoadingMore = true;
setTimeout(() => {
let newData: NewsItem[] = [];
let maxId: number = this.newsList.length > 0 ? this.newsList[this.newsList.length - 1].id : 0;
for (let i: number = 1; i <= 10; i++) {
newData.push({
id: maxId + i,
title: `鸿蒙应用开发教程 ${maxId + i}:状态管理最佳实践`,
source: '技术博客',
time: `2026-07-${5 + i % 10}`,
isTop: false
});
}
this.newsList = this.newsList.concat(newData);
this.currentPage++;
if (this.currentPage > 5) {
this.hasMoreData = false;
}
this.isLoadingMore = false;
}, 1500);
}
@Builder
newsItemLayout(item: NewsItem) {
Row() {
if (item.image) {
Image(item.image)
.width(80)
.height(60)
.borderRadius(8)
.objectFit(ImageFit.Cover);
} else {
Column() {
Text(item.source.charAt(0))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff');
}
.width(80)
.height(60)
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.backgroundColor('#007DFF');
}
Column() {
Row() {
if (item.isTop) {
Text('置顶')
.fontSize(12)
.fontColor('#FF6B6B')
.backgroundColor('#FFF0F0')
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ right: 8 });
}
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#181818')
.maxLines(2)
.layoutWeight(1);
}
Row() {
Text(item.source)
.fontSize(12)
.fontColor('#999999');
Text('·')
.fontSize(12)
.fontColor('#CCCCCC')
.margin({ left: 4, right: 4 });
Text(item.time)
.fontSize(12)
.fontColor('#999999');
}
.margin({ top: 8 });
}
.padding({ left: 12 })
.layoutWeight(1);
}
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.width('100%')
.backgroundColor('#ffffff')
.borderRadius(12);
}
build() {
Column() {
Text('新闻资讯')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#181818')
.margin({ top: 24, bottom: 8 });
Text('下拉刷新获取最新资讯')
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 12 });
Refresh({
refreshing: $$this.refreshing
}) {
List({ space: 10 }) {
ForEach(this.newsList, (item: NewsItem) => {
ListItem() {
this.newsItemLayout(item);
}
.onClick(() => {
console.info(`点击了新闻: ${item.title}`);
});
}, (item: NewsItem) => item.id.toString());
ListItem() {
Row() {
if (this.isLoadingMore) {
LoadingProgress()
.width(20)
.height(20)
.color('#007DFF');
Text('加载更多新闻...')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 8 });
} else if (!this.hasMoreData) {
Text('已加载全部新闻')
.fontSize(14)
.fontColor('#999999');
} else {
Text('上拉加载更多')
.fontSize(14)
.fontColor('#999999');
}
}
.padding(16)
.justifyContent(FlexAlign.Center);
}
}
.width('100%')
.height('100%')
.padding({ left: 16, right: 16 })
.divider({
strokeWidth: 1,
color: '#f5f5f5',
startMargin: 16,
endMargin: 16
})
.onReachEnd(() => {
this.loadMoreData();
});
}
.onRefreshing(() => {
this.refreshData();
})
.layoutWeight(1)
.width('100%');
}
.backgroundColor('#f8f8f8')
.width('100%')
.height('100%');
}
}
8.4 功能亮点
- 置顶新闻标识:使用红色标签标识置顶新闻。
- 来源图标:根据新闻来源首字母生成图标。
- 时间显示:显示新闻发布时间。
- 加载状态:根据不同状态显示不同的加载提示。
- 分割线样式:自定义分割线样式,提升视觉效果。
九、与其他组件的配合使用
9.1 Refresh + Scroll
当需要刷新的内容不是列表,而是普通滚动区域时,可以使用 Refresh + Scroll:
Refresh({
refreshing: $$this.refreshing
}) {
Scroll() {
Column({ space: 16 }) {
// 滚动内容
Text('内容区域')
.fontSize(16)
.padding(20)
.backgroundColor('#ffffff')
.borderRadius(12);
// 更多内容...
}
.padding(16);
}
}
.onRefreshing(() => {
// 刷新逻辑
});
9.2 Refresh + Grid
在网格布局中实现下拉刷新:
Refresh({
refreshing: $$this.refreshing
}) {
Grid() {
ForEach(this.gridData, (item: GridItem) => {
GridItem() {
// 网格项内容
}
}, (item: GridItem) => item.id.toString());
}
.columnsTemplate('1fr 1fr')
.rowsGap(10)
.columnsGap(10);
}
.onRefreshing(() => {
// 刷新逻辑
});
9.3 Refresh + Swiper
从 API 12 开始,Refresh 支持与垂直滚动的 Swiper 联动:
Refresh({
refreshing: $$this.refreshing
}) {
Swiper({
loop: false // 必须设置为 false,否则 Refresh 无法联动
}) {
// Swiper 页面内容
}
}
.onRefreshing(() => {
// 刷新逻辑
});
十、总结与展望
10.1 核心要点回顾
- Refresh 组件:提供下拉刷新能力,支持双向绑定和状态回调。
- List 组件:高性能列表渲染,支持懒加载和列表项复用。
- 状态管理:使用
@State管理组件状态,通过重新赋值触发更新。 - 双向绑定:使用
$$运算符实现刷新状态的双向同步。 - 性能优化:合理使用 keyGenerator、layoutWeight,避免频繁状态更新。
10.2 最佳实践总结
- 始终使用双向绑定:确保刷新状态与数据模型保持同步。
- 批量更新数据:避免在循环中更新状态,提高性能。
- 正确使用 keyGenerator:为列表项提供唯一标识,优化列表项复用。
- 合理使用 layoutWeight:避免布局溢出,实现响应式布局。
- 处理加载状态:显示加载提示,提升用户体验。
10.3 未来发展方向
随着 HarmonyOS NEXT 的持续发展,Refresh 和 List 组件也在不断进化:
- 更丰富的自定义能力:未来可能支持更多自定义刷新样式和动画。
- 更好的性能优化:持续优化列表渲染性能,支持更大数据量。
- 更多交互模式:可能支持更多刷新交互模式,如侧滑刷新、双击刷新等。
- 更好的跨设备适配:优化在折叠屏、大屏等设备上的显示效果。
附录:API 版本兼容性参考
Refresh 组件 API 版本变化
| 特性 | API 版本 | 说明 |
|---|---|---|
| 基本 Refresh 功能 | API 8+ | 基础下拉刷新能力 |
| 双向绑定 $$ | API 9+ | 支持刷新状态双向绑定 |
| promptText 属性 | API 12+ | 自定义刷新提示文本 |
| refreshingContent 属性 | API 12+ | 自定义刷新区域内容 |
| onOffsetChange 事件 | API 12+ | 监听下拉偏移量变化 |
| Swiper/Web 联动 | API 12+ | 支持与 Swiper、Web 组件联动 |
List 组件 API 版本变化
| 特性 | API 版本 | 说明 |
|---|---|---|
| 基本 List 功能 | API 8+ | 基础列表渲染能力 |
| Scroller 控制器 | API 8+ | 滚动控制能力 |
| onScrollIndex 事件 | API 9+ | 滚动索引监听 |
| onReachStart/onReachEnd 事件 | API 9+ | 滚动到顶部/底部监听 |
| 嵌套滚动支持 | API 10+ | 支持嵌套滚动 |
通过本文的详细讲解,相信你已经掌握了鸿蒙原生 ArkTS 布局方式中 Refresh + List 下拉刷新布局的核心原理和实现方法。在实际开发中,结合项目需求和用户体验,合理运用这些技术,可以打造出高性能、交互流畅的列表应用。
更多推荐




所有评论(0)