HarmonyOS NEXT实战:瀑布流和懒加载

简介: 本教程介绍如何在HarmonyOS应用中实现瀑布流布局,包含图片与文字展示,并通过懒加载优化性能。内容涵盖数据源构建、自定义组件开发及页面集成,适合初学者掌握HarmonyOS UI开发技巧。

HarmonyOS Next实战##HarmonyOS应用开发##教育

目标:实现瀑布流图片和文字,并通过懒加载加载瀑布流子项。

实现思路:

  1. 创建Card模型
  2. 创建WaterFlowDataSource 数据源
  3. 定制WaterFlowItemComponent自定义组件
  4. 在页面实现WaterFlow和LazyForEach循环

WaterFlow
瀑布流容器,由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。
仅支持FlowItem子组件,支持渲染控制类型(if/else、ForEach、LazyForEach和Repeat)。

实战:
WaterFlowDataSource

// 实现IDataSource接口的对象,用于瀑布流组件加载数据
export class WaterFlowDataSource implements IDataSource {
   
  private dataArray: Card[] = [];
  private listeners: DataChangeListener[] = [];

  constructor() {
   
    this.dataArray.push({
   
      image: $r('app.media.img_1'),
      imageWidth: 162,
      imageHeight: 130,
      text: 'Ice cream is made with carrageenan …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_2'),
      imageWidth: '100%',
      imageHeight: 117,
      text: 'Is makeup one of your daily esse …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_3'),
      imageWidth: '100%',
      imageHeight: 117,
      text: 'Coffee is more than just a drink: It’s …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_4'),
      imageWidth: 162,
      imageHeight: 130,
      text: 'Fashion is a popular style, especially in …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_5'),
      imageWidth: '100%',
      imageHeight: 206,
      text: 'Argon is a great free UI packag …',
      buttonLabel: 'View article'
    });
  }

  // 获取索引对应的数据
  public getData(index: number): Card {
   
    return this.dataArray[index];
  }

  // 通知控制器数据重新加载
  notifyDataReload(): void {
   
    this.listeners.forEach(listener => {
   
      listener.onDataReloaded();
    })
  }

  // 通知控制器数据增加
  notifyDataAdd(index: number): void {
   
    this.listeners.forEach(listener => {
   
      listener.onDataAdd(index);
    })
  }

  // 通知控制器数据变化
  notifyDataChange(index: number): void {
   
    this.listeners.forEach(listener => {
   
      listener.onDataChange(index);
    })
  }

  // 通知控制器数据删除
  notifyDataDelete(index: number): void {
   
    this.listeners.forEach(listener => {
   
      listener.onDataDelete(index);
    })
  }

  // 通知控制器数据位置变化
  notifyDataMove(from: number, to: number): void {
   
    this.listeners.forEach(listener => {
   
      listener.onDataMove(from, to);
    })
  }

  //通知控制器数据批量修改
  notifyDatasetChange(operations: DataOperation[]): void {
   
    this.listeners.forEach(listener => {
   
      listener.onDatasetChange(operations);
    })
  }

  // 获取数据总数
  public totalCount(): number {
   
    return this.dataArray.length;
  }

  // 注册改变数据的控制器
  registerDataChangeListener(listener: DataChangeListener): void {
   
    if (this.listeners.indexOf(listener) < 0) {
   
      this.listeners.push(listener);
    }
  }

  // 注销改变数据的控制器
  unregisterDataChangeListener(listener: DataChangeListener): void {
   
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
   
      this.listeners.splice(pos, 1);
    }
  }

  // 增加数据
  public add1stItem(card: Card): void {
   
    this.dataArray.splice(0, 0, card);
    this.notifyDataAdd(0);
  }

  // 在数据尾部增加一个元素
  public addLastItem(card: Card): void {
   
    this.dataArray.splice(this.dataArray.length, 0, card);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  public addDemoDataAtLast(): void {
   
    this.dataArray.push({
   
      image: $r('app.media.img_1'),
      imageWidth: 162,
      imageHeight: 130,
      text: 'Ice cream is made with carrageenan …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_2'),
      imageWidth: '100%',
      imageHeight: 117,
      text: 'Is makeup one of your daily esse …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_3'),
      imageWidth: '100%',
      imageHeight: 117,
      text: 'Coffee is more than just a drink: It’s …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_4'),
      imageWidth: 162,
      imageHeight: 130,
      text: 'Fashion is a popular style, especially in …',
      buttonLabel: 'View article'
    });
    this.dataArray.push({
   
      image: $r('app.media.img_5'),
      imageWidth: '100%',
      imageHeight: 206,
      text: 'Argon is a great free UI packag …',
      buttonLabel: 'View article'
    });
  }

  // 在指定索引位置增加一个元素
  public addItem(index: number, card: Card): void {
   
    this.dataArray.splice(index, 0, card);
    this.notifyDataAdd(index);
  }

  // 删除第一个元素
  public delete1stItem(): void {
   
    this.dataArray.splice(0, 1);
    this.notifyDataDelete(0);
  }

  // 删除第二个元素
  public delete2ndItem(): void {
   
    this.dataArray.splice(1, 1);
    this.notifyDataDelete(1);
  }

  // 删除最后一个元素
  public deleteLastItem(): void {
   
    this.dataArray.splice(-1, 1);
    this.notifyDataDelete(this.dataArray.length);
  }

  // 在指定索引位置删除一个元素
  public deleteItem(index: number): void {
   
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index);
  }

  // 重新加载数据
  public reload(): void {
   
    this.dataArray.splice(1, 1);
    this.dataArray.splice(3, 2);
    this.notifyDataReload();
  }
}

export interface Card {
   
  image: Resource //图片
  imageWidth: Length //图片宽度
  imageHeight: Length //图片高度
  text: string //文字
  buttonLabel: string //按钮文字
}

WaterFlowItemComponent

import {
    Card } from "./WaterFlowDataSource";

// @Reusable
@Component
export struct WaterFlowItemComponent {
   
  @Prop item: Card

  // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
  aboutToReuse(params: Record<string, Card>) {
   
    this.item = params.item;
    console.info('Reuse item:' + JSON.stringify(this.item));
  }

  aboutToAppear() {
   
    console.info('new item:' + JSON.stringify(this.item));
  }

  build() {
   
    if (this.item.imageWidth == '100%') {
   
      Column() {
   
        Image(this.item.image)
          .width(this.item.imageWidth)
          .height(this.item.imageHeight)
        Column() {
   
          Text(this.item.text)
            .fontWeight(400)
            .fontColor('#32325D')
            .fontSize(14)
            .lineHeight(18)
          Text(this.item.buttonLabel)
            .fontWeight(700)
            .fontColor('#5E72E4')
            .fontSize(12)
            .lineHeight(17)
        }
        .width('100%')
        .padding(12)
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Start)
    } else {
   
      Row() {
   
        Image(this.item.image)
          .width(this.item.imageWidth)
          .height(this.item.imageHeight)

        Column() {
   
          Text(this.item.text)
            .fontWeight(400)
            .fontColor('#32325D')
            .fontSize(14)
            .lineHeight(18)
          Text(this.item.buttonLabel)
            .fontWeight(700)
            .fontColor('#5E72E4')
            .fontSize(12)
            .lineHeight(17)
        }
        .height('100%')
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .padding(12)
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width('100%')
      .height('100%')
    }
  }
}

WaterFlowDemoPage

import {
    Card, WaterFlowDataSource } from './WaterFlowDataSource';
import {
    WaterFlowItemComponent } from './WaterFlowItemComponent';

@Entry
@Component
export struct WaterFlowDemoPage {
   
  minSize: number = 80;
  maxSize: number = 180;
  fontSize: number = 24;
  scroller: Scroller = new Scroller();
  dataSource: WaterFlowDataSource = new WaterFlowDataSource();
  dataCount: number = this.dataSource.totalCount();
  private itemHeightArray: number[] = [];
  @State sections: WaterFlowSections = new WaterFlowSections();
  sectionMargin: Margin = {
   
    top: 10,
    left: 20,
    bottom: 10,
    right: 20
  };

  // 设置FlowItem的高度数组
  setItemSizeArray() {
   
    this.itemHeightArray.push(130);
    this.itemHeightArray.push(212);
    this.itemHeightArray.push(212);
    this.itemHeightArray.push(130);
    this.itemHeightArray.push(268);
  }

  aboutToAppear() {
   
    this.setItemSizeArray();
    this.addSectionOptions(true);
    for (let index = 0; index < 10; index++) {
   
      this.dataSource.addDemoDataAtLast();
      this.setItemSizeArray();
      this.addSectionOptions();
    }
  }

  addSectionOptions(isFirstAdd: boolean = false) {
   
    this.sections.push({
   
      itemsCount: 1,
      crossCount: 1,
      margin: isFirstAdd ? {
   
        top: 20,
        left: 20,
        bottom: 10,
        right: 20
      } : this.sectionMargin,
      onGetItemMainSizeByIndex: (index: number) => {
   
        return 130;
      }
    })
    this.sections.push({
   
      itemsCount: 2,
      crossCount: 2,
      rowsGap: '20vp',
      margin: this.sectionMargin,
      onGetItemMainSizeByIndex: (index: number) => {
   
        return 212;
      }
    })
    this.sections.push({
   
      itemsCount: 1,
      crossCount: 1,
      margin: this.sectionMargin,
      onGetItemMainSizeByIndex: (index: number) => {
   
        return 130;
      }
    })
    this.sections.push({
   
      itemsCount: 1,
      crossCount: 1,
      rowsGap: '20vp',
      columnsGap: '20vp',
      margin: this.sectionMargin,
      onGetItemMainSizeByIndex: (index: number) => {
   
        return 268;
      }
    })
  }

  build() {
   
    Column({
    space: 2 }) {
   
      WaterFlow({
    scroller: this.scroller, sections: this.sections }) {
   
        LazyForEach(this.dataSource, (item: Card, index: number) => {
   
          FlowItem() {
   
            WaterFlowItemComponent({
    item: item })
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(6)
          .clip(true)
        }, (item: Card, index: number) => index.toString())
      }
      // .columnsTemplate('1fr 1fr') // 瀑布流使用sections参数时该属性无效
      .columnsGap(14)
      .rowsGap(20)
      .backgroundColor('#F8F9FE')
      .width('100%')
      .height('100%')
      .layoutWeight(1)
    }
  }
}
目录
相关文章
|
4月前
|
容器
HarmonyOS NEXT仓颉开发语言实战案例:外卖App
仓颉语言实战分享,教你如何用仓颉开发外卖App界面。内容包括页面布局、导航栏自定义、搜索框实现、列表模块构建等,附完整代码示例。轻松掌握Scroll、List等组件使用技巧,提升HarmonyOS应用开发能力。
|
3月前
|
移动开发 前端开发 JavaScript
鸿蒙NEXT时代你所不知道的全平台跨端框架:CMP、Kuikly、Lynx、uni-app x等
本篇基于当前各大活跃的跨端框架的现状,对比当前它们的情况和未来的可能,帮助你在选择框架时更好理解它们的特点和差异。
312 0
|
4月前
|
安全 API 开发工具
【HarmonyOS NEXT】一键扫码功能
这些Kit为我们应用开发提升了极大地效率。很多简单的功能,如果不需要太深的定制化需求,直接调用kit提供的API就可以实现,在android或者ios上需要很多代码才能实现的功能效果。
119 0
HarmonyOS NEXT仓颉开发语言实战案例:电影App
周末好!本文分享使用仓颉语言重构ArkTS实现的电影App案例,对比两者在UI布局、组件写法及语法差异。内容包括页面结构、列表分组、分类切换与电影展示等。通过代码演示仓颉在HarmonyOS开发中的应用。##仓颉##ArkTS##HarmonyOS开发
|
4月前
|
容器
HarmonyOS NEXT仓颉开发语言实战案例:健身App
本期分享一个健身App首页的布局实现,顶部采用Stack容器实现重叠背景与偏移效果,列表部分使用List结合Scroll实现可滚动内容。代码结构清晰,适合学习HarmonyOS布局技巧。
HarmonyOS NEXT仓颉开发语言实战案例:小而美的旅行App
本文分享了一个旅行App首页的设计与实现,使用List容器搭配Row、Column布局完成个人信息、功能列表及推荐模块的排版,详细展示了HarmonyOS下的界面构建技巧。
|
18天前
|
存储 缓存 5G
鸿蒙 HarmonyOS NEXT端云一体化开发-云存储篇
本文介绍用户登录后获取昵称、头像的方法,包括通过云端API和AppStorage两种方式,并实现上传头像至云存储及更新用户信息。同时解决图片缓存问题,添加上传进度提示,支持自动登录判断,提升用户体验。
90 0
|
18天前
|
存储 负载均衡 数据库
鸿蒙 HarmonyOS NEXT端云一体化开发-云函数篇
本文介绍基于华为AGC的端云一体化开发流程,涵盖项目创建、云函数开通、应用配置及DevEco集成。重点讲解云函数的编写、部署、调用与传参,并涉及环境变量设置、负载均衡、重试机制与熔断策略等高阶特性,助力开发者高效构建稳定云端服务。
178 0
鸿蒙 HarmonyOS NEXT端云一体化开发-云函数篇
|
18天前
|
存储 JSON 数据建模
鸿蒙 HarmonyOS NEXT端云一体化开发-云数据库篇
云数据库采用存储区、对象类型、对象三级结构,支持灵活的数据建模与权限管理,可通过AGC平台或本地项目初始化,实现数据的增删改查及端侧高效调用。
50 0
|
18天前
|
存储 开发者 容器
鸿蒙 HarmonyOS NEXT星河版APP应用开发-ArkTS面向对象及组件化UI开发使用实例
本文介绍了ArkTS语言中的Class类、泛型、接口、模块化、自定义组件及状态管理等核心概念,并结合代码示例讲解了对象属性、构造方法、继承、静态成员、访问修饰符等内容,同时涵盖了路由管理、生命周期和Stage模型等应用开发关键知识点。
150 0
鸿蒙 HarmonyOS NEXT星河版APP应用开发-ArkTS面向对象及组件化UI开发使用实例