项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

在移动应用开发中,下拉刷新是一种极为常见的交互模式,它允许用户通过简单的手势操作获取最新数据。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,实现组件状态与数据模型的双向同步。

双向绑定的优势:

  1. 自动状态同步:当用户下拉触发刷新时,Refresh 组件会自动将 refreshing 设为 true
  2. 简化代码:无需在 onRefreshing 回调中手动设置 refreshing = true
  3. 状态一致性:确保组件状态与数据模型始终保持一致,避免状态错乱。

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 回调也不触发。

可能原因:

  1. Refresh 组件未正确包裹 List:确保 List 是 Refresh 的直接子组件。
  2. List 内容不满一屏:当 List 内容不足以滚动时,下拉手势可能被拦截。
  3. 手势冲突:其他组件的手势可能与 Refresh 的下拉手势冲突。
  4. 双向绑定未正确使用:使用了 this.refreshing 而不是 $$this.refreshing

解决方案:

// 确保 Refresh 直接包裹 List
Refresh({
  refreshing: $$this.refreshing // 使用双向绑定
}) {
  List({ space: 10 }) {
    // ...
  }
  .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true }) // 确保手势响应
}
.onRefreshing(() => {
  this.refreshData();
});

7.2 刷新状态不更新

问题现象: 刷新完成后,刷新动画仍然显示。

可能原因:

  1. 未设置 refreshing = false:在刷新完成后未将状态设为 false。
  2. 异步更新问题:在异步回调中更新状态时出现问题。
  3. 状态管理错误:使用了错误的状态管理方式。

解决方案:

refreshData() {
  setTimeout(() => {
    // 更新数据
    this.listData = newData.concat(this.listData);
    
    // 必须设置 refreshing = false
    this.refreshing = false;
  }, 2000);
}

7.3 列表项点击事件不响应

问题现象: 点击列表项时,onClick 回调不触发。

可能原因:

  1. ListItem 未正确使用:列表项必须使用 ListItem 组件。
  2. 布局遮挡:其他组件可能遮挡了点击区域。
  3. 手势冲突:自定义手势可能与点击事件冲突。

解决方案:

ListItem() {
  Row() {
    // 列表项内容
  }
  .width('100%')
  .padding(16);
}
.onClick(() => {
  console.info('点击了列表项');
});

7.4 数据更新后列表不刷新

问题现象: 更新 listData 后,列表内容没有变化。

可能原因:

  1. 直接修改数组:使用 pushsplice 等方法直接修改数组。
  2. 对象引用未变化:修改了数组中的对象属性,但数组引用未变。
  3. 状态管理错误:未使用 @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 功能亮点

  1. 置顶新闻标识:使用红色标签标识置顶新闻。
  2. 来源图标:根据新闻来源首字母生成图标。
  3. 时间显示:显示新闻发布时间。
  4. 加载状态:根据不同状态显示不同的加载提示。
  5. 分割线样式:自定义分割线样式,提升视觉效果。

九、与其他组件的配合使用

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 核心要点回顾

  1. Refresh 组件:提供下拉刷新能力,支持双向绑定和状态回调。
  2. List 组件:高性能列表渲染,支持懒加载和列表项复用。
  3. 状态管理:使用 @State 管理组件状态,通过重新赋值触发更新。
  4. 双向绑定:使用 $$ 运算符实现刷新状态的双向同步。
  5. 性能优化:合理使用 keyGenerator、layoutWeight,避免频繁状态更新。

10.2 最佳实践总结

  1. 始终使用双向绑定:确保刷新状态与数据模型保持同步。
  2. 批量更新数据:避免在循环中更新状态,提高性能。
  3. 正确使用 keyGenerator:为列表项提供唯一标识,优化列表项复用。
  4. 合理使用 layoutWeight:避免布局溢出,实现响应式布局。
  5. 处理加载状态:显示加载提示,提升用户体验。

10.3 未来发展方向

随着 HarmonyOS NEXT 的持续发展,Refresh 和 List 组件也在不断进化:

  1. 更丰富的自定义能力:未来可能支持更多自定义刷新样式和动画。
  2. 更好的性能优化:持续优化列表渲染性能,支持更大数据量。
  3. 更多交互模式:可能支持更多刷新交互模式,如侧滑刷新、双击刷新等。
  4. 更好的跨设备适配:优化在折叠屏、大屏等设备上的显示效果。

附录: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 下拉刷新布局的核心原理和实现方法。在实际开发中,结合项目需求和用户体验,合理运用这些技术,可以打造出高性能、交互流畅的列表应用。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐