ArkUI 列表与网格:长列表的高性能渲染

列表是 App 里最常写的界面

资讯流、商品列表、聊天记录、通讯录,几乎每个应用都有列表。ArkUI 里列表用 List + ListItem 构建,网格用 Grid + GridItem,但真正决定性能的是懒加载机制。

基础列表 List

List 是滚动列表容器,ListItem 是每一项。垂直列表是默认行为:

@Entry
@Component
struct ListDemo {
  @State items: string[] = ['苹果', '香蕉', '橙子', '葡萄'];

  build() {
    List() {
      ForEach(this.items, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(18)
            Blank()  // 弹性占位,把右侧内容推到右边
            Image($r('app.media.arrow_right'))
              .width(20).height(20)
          }
          .height(60)
          .padding({ left: 16, right: 16 })
        }
      }, (item: string) => item)
    }
    .width('100%')
    .height('100%')
  }
}

要点:

  • ForEach 生成列表项,key 用能唯一标识该项的字段
  • ListItem 里放每一项的内容
  • Blank() 是弹性占位组件,常用于把内容左右分散

懒加载:大数据量的关键

上面的 ForEach 会一次性生成所有 ListItem,数据量小没问题,几千上万条就会卡顿。懒加载 LazyForEach 只渲染可见区域附近的项:

// 数据源实现 IDataSource 接口
class MyDataSource implements IDataSource {
  private list: string[] = [];

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): string {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {}
  unregisterDataChangeListener(listener: DataChangeListener): void {}
}

@Entry
@Component
struct LazyListDemo {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    // 初始化 10000 条数据
    let arr: string[] = [];
    for (let i = 0; i < 10000; i++) {
      arr.push(`${i}`);
    }
    this.data = new MyDataSource();
    (this.data as any).list = arr;
  }

  build() {
    List() {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Text(item).height(50)
        }
      }, (item: string) => item)
    }
    .cachedCount(3)  // 预加载可见区域外的 3 项,减少滚动白屏
  }
}

LazyForEach 的核心:

  • 数据源要自己实现 IDataSource 接口(totalCount + getData
  • cachedCount 设置预加载数量,滚动时提前准备好,避免白屏
  • 只渲染视口附近的项,10000 条和 100 条性能几乎一样

宫格 Grid

Grid 是网格容器,GridItem 是每个格子。商品列表、相册九宫格都用它:

@Entry
@Component
struct GridDemo {
  @State items: string[] = ['1', '2', '3', '4', '5', '6', '7', '8'];

  build() {
    Grid() {
      ForEach(this.items, (item: string) => {
        GridItem() {
          Column() {
            Text(`商品 ${item}`)
              .fontSize(16)
          }
          .width('100%')
          .height(140)
          .backgroundColor('#F0F0F0')
          .borderRadius(8)
        }
      }, (item: string) => item)
    }
    .columnsTemplate('1fr 1fr 1fr')  // 三列等宽
    .columnsGap(12)                   // 列间距
    .rowsGap(12)                      // 行间距
    .padding(12)
  }
}

columnsTemplate('1fr 1fr 1fr') 定义三列等宽布局,fr 是弹性单位。改列数就改这段字符串。

列表的交互:点击和分割线

列表项点击和分割线是最常见的交互:

@Entry
@Component
struct ListInteractive {
  @State selectedIndex: number = -1;

  build() {
    List({ space: 8 }) {  // space 设置列表项间距
      ForEach(['设置', '通知', '关于', '退出'], (item: string, index: number) => {
        ListItem() {
          Row() {
            Text(item).fontSize(16)
          }
          .height(56)
          .padding({ left: 16, right: 16 })
          .backgroundColor(this.selectedIndex === index ? '#FFF0E6' : '#FFFFFF')
        }
        .onClick(() => {
          this.selectedIndex = index;  // 点击高亮
        })
      }, (item: string) => item)
    }
    .divider({ strokeWidth: 1, color: '#F0F0F0' })  // 分割线
  }
}

divider 给列表项之间加分割线,space 是间距,两者二选一按需使用。

轮播 Swiper

Swiper 是轮播容器,banner 图、引导页都用它:

@Entry
@Component
struct SwiperDemo {
  private banners: string[] = ['banner1', 'banner2', 'banner3'];

  build() {
    Swiper() {
      ForEach(this.banners, (banner: string) => {
        Image($r(`app.media.${banner}`))
          .width('100%')
          .height(200)
          .objectFit(ImageFit.Cover)
      }, (banner: string) => banner)
    }
    .autoPlay(true)          // 自动播放
    .interval(3000)          // 间隔 3 秒
    .indicator(true)         // 显示指示器圆点
    .loop(true)              // 循环播放
  }
}

一个完整案例:商品列表页

把列表、懒加载、点击、图片综合起来:

class GoodsDataSource implements IDataSource {
  private list: Goods[] = [];
  totalCount(): number { return this.list.length; }
  getData(index: number): Goods { return this.list[index]; }
  registerDataChangeListener(listener: DataChangeListener): void {}
  unregisterDataChangeListener(listener: DataChangeListener): void {}
}

class Goods {
  id: number;
  name: string;
  price: number;
  constructor(id: number, name: string, price: number) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
}

@Entry
@Component
struct GoodsListPage {
  private data: GoodsDataSource = new GoodsDataSource();

  aboutToAppear() {
    let arr: Goods[] = [];
    for (let i = 0; i < 1000; i++) {
      arr.push(new Goods(i, `商品${i}`, i * 1.5));
    }
    (this.data as any).list = arr;
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('商品列表').fontSize(20).fontWeight(FontWeight.Bold)
      }
      .height(56)
      .padding({ left: 16 })

      // 商品列表
      List() {
        LazyForEach(this.data, (goods: Goods) => {
          ListItem() {
            Row() {
              Image($r('app.media.goods_placeholder'))
                .width(80).height(80)
                .borderRadius(8)
              Column() {
                Text(goods.name).fontSize(16)
                Text(`¥${goods.price}`).fontSize(18).fontColor('#FF6B35')
              }
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 12 })
              Blank()
              Button('购买')
                .height(32)
                .type(ButtonType.Capsule)
            }
            .height(100)
            .padding({ left: 16, right: 16 })
          }
          .onClick(() => {
            console.info(`点击了 ${goods.name}`);
          })
        }, (goods: Goods) => goods.id.toString())
      }
      .cachedCount(2)
      .divider({ strokeWidth: 1, color: '#F5F5F5' })
    }
    .width('100%')
    .height('100%')
  }
}

这个例子涵盖了列表开发的完整套路:数据源实现、懒加载、按需渲染、点击处理、图片展示。

列表性能的几条小经验哦

  1. 大数据量必须用 LazyForEachForEach 一次性渲染所有项,几千条就明显卡顿。LazyForEach 渲染视口附近,数据量对性能几乎没有影响。

  2. key 必须唯一且稳定LazyForEach 的第三个参数返回唯一 key,框架靠它做增量更新。用 id 字段,别用数组下标(删除项时下标会变,导致更新错乱)。

  3. cachedCount 别设太大。预加载项数设 1-3 即可,设太大会一次性渲染太多项,反而增加内存和首屏延迟。

  4. 列表项里避免重组件。单个 ListItem 里别塞太复杂的布局和动效,列表滚动时每项都要布局,项越重滚动越卡。

  5. Blank 是布局利器。左右分散内容时用 Blank(),比手写 margin 更灵活,适配不同屏宽。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐