头图

HarmonyOS Next 实战卡片开发 03

在前面两张,我们基本掌握了卡片的使用流程,本章节就通过一个实战来加强对卡片使用的理解。

要完成的案例

image-20241024175411585


PixPin_2024-10-24_17-54-18

新建项目和新建服务卡片

image-20241031111045865


image-20241031111130741

设置沉浸式

entry/src/main/ets/entryability/EntryAbility.ets

image-20241031111326923

首页显示轮播图数据

PixPin_2024-10-31_11-27-05

1. 申请网络权限

entry/src/main/module.json5

image-20241031112238553

2. 新建工具文件 /utils/index.ets

entry/src/main/ets/utils/index.ets
export const swiperInit = () => {
  AppStorage.setOrCreate("swiperList", [
    "https://env-00jxhf99mujs.normal.cloudstatic.cn/card/1.webp?expire_at=1729734506&er_sign=e51cb3b4f4b28cb2da96fd53701eaa69",
    "https://env-00jxhf99mujs.normal.cloudstatic.cn/card/2.webp?expire_at=1729734857&er_sign=b2ffd42585568a094b9ecfb7995a9763",
    "https://env-00jxhf99mujs.normal.cloudstatic.cn/card/3.webp?expire_at=1729734870&er_sign=50d5f210191c113782958dfd6681cd2d",
  ]);
  AppStorage.setOrCreate("activeIndex", 0);
};

3. 初始化

entry/src/main/ets/entryability/EntryAbility.ets

image-20241031111803516

4. 页面中使用

entry/src/main/ets/pages/Index.ets

@Entry
@Component
struct Index {
  @StorageProp("swiperList")
  swiperList: string[] = []
  @StorageLink("activeIndex")
  activeIndex: number = 0


  build() {
    Column() {
      Swiper() {
        ForEach(this.swiperList, (img: string) => {
          Image(img)
            .width("80%")
        })
      }
      .loop(true)
      .autoPlay(true)
      .interval(3000)

      .onChange(index => this.activeIndex = index)
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundImage(this.swiperList[this.activeIndex])
    .backgroundBlurStyle(BlurStyle.Thin)
    .backgroundImageSize(ImageSize.Cover)
    .animation({ duration: 500 })
  }
}

5. 效果

PixPin_2024-10-31_11-27-05

创建卡片时,获取卡片 id

PixPin_2024-10-31_13-06-50

image-20241031222230532

1. 获取和返回卡片 id

这里解析下为什么要返回 id 给卡片组件,因为后期卡片想要向应用通信时,应用响应数据要根据卡片 id 来响应。

另外 formExtensionAbility 进程不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务,在生命周期调度完成后会继续存在 10 秒,如 10 秒内没有新的

生命周期回调触发则进程自动退出。针对可能需要 10 秒以上才能完成的业务逻辑,建议拉起主应用进行处理,处理完成后使用updateForm通知卡片进行刷新

entry/src/main/ets/entryformability/EntryFormAbility.ets
  onAddForm(want: Want) {
    class FormData {
      // 获取卡片id
      formId: string = want.parameters!['ohos.extra.param.key.form_identity'].toString();
    }

    let formData = new FormData()
    return formBindingData.createFormBindingData(formData);
  }

2. 接受和显示卡片 id

entry/src/main/ets/widget/pages/WidgetCard.ets
const localStorage = new LocalStorage()

@Entry(localStorage)
@Component
struct WidgetCard {
  @LocalStorageProp("formId")
  formId: string = ""

  build() {
    Row() {
      Text(this.formId)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)

  }
}

3. 效果

PixPin_2024-10-31_13-06-50

记录卡片 id,持久化存储

image-20241031223836426

主要流程如下:

  1. 封装持久化存储卡片 id 的工具类
  2. 初始化卡片 id 工具类
  3. 卡片主动上传卡片 id
  4. 应用 Aibility 接收卡片 id
  5. 接收卡片 id 并且持久化
  6. 移除卡片时,删除卡片 id

1. 封装持久化存储卡片 id 的工具类

此时接收到卡片 id 后,需要将卡片 id 持久化存储,避免重新打卡手机时,无法联系到已经创建的卡片

entry/src/main/ets/utils/index.ets
export class FormIdStore {
  static key: string = "wsy_collect";
  static dataPreferences: preferences.Preferences | null = null;
  static context: Context | null = null;

  //  初始化
  static init(context?: Context) {
    if (!FormIdStore.dataPreferences) {
      if (context) {
        FormIdStore.context = context;
      }
      FormIdStore.dataPreferences = preferences.getPreferencesSync(
        FormIdStore.context || getContext(),
        { name: FormIdStore.key }
      );
    }
  }

  //  获取卡片id 数组
  static getList() {
    FormIdStore.init();
    const str = FormIdStore.dataPreferences?.getSync(FormIdStore.key, "[]");
    const list = JSON.parse(str as string) as string[];
    console.log("list卡片", list);
    return list;
  }

  // 新增卡片数组
  static async set(item: string) {
    FormIdStore.init();
    const list = FormIdStore.getList();
    if (!list.includes(item)) {
      list.push(item);
      FormIdStore.dataPreferences?.putSync(
        FormIdStore.key,
        JSON.stringify(list)
      );
      await FormIdStore.dataPreferences?.flush();
    }
  }

  // 删除元素
  static async remove(item: string) {
    FormIdStore.init();
    const list = FormIdStore.getList();
    const index = list.indexOf(item);
    if (index !== -1) {
      list.splice(index, 1);
      FormIdStore.dataPreferences?.putSync(
        FormIdStore.key,
        JSON.stringify(list)
      );
      await FormIdStore.dataPreferences?.flush();
    }
  }
}

2. 初始化卡片 id 工具类

  1. onCreate 中初始化

    entry/src/main/ets/entryability/EntryAbility.ets
      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        FormIdStore.init(this.context)
  2. onAddForm 中初始化

    onAddForm(want: Want) {
      FormIdStore.init(this.context)

3. 卡片主动上传卡片 id

利用 watch 监听器来触发上传

entry/src/main/ets/widget/pages/WidgetCard.ets
const localStorage = new LocalStorage()

@Entry(localStorage)
@Component
struct WidgetCard {
  @LocalStorageProp("formId")
  @Watch("postData")
  formId: string = ""

  // 上传卡片id
  postData() {
    postCardAction(this, {
      action: 'call',
      abilityName: 'EntryAbility',
      params: {
        method: 'createCard',
        formId: this.formId
      }
    });
  }

  build() {
    Row() {
      Text(this.formId)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)

  }
}

4. 应用 Aibility 接收卡片 id

entry/src/main/ets/entryability/EntryAbility.ets
// callee中要求返回的数据类型
class MyPara implements rpc.Parcelable {
  marshalling(dataOut: rpc.MessageSequence): boolean {
    return true
  }

  unmarshalling(dataIn: rpc.MessageSequence): boolean {
    return true
  }
}

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    FormIdStore.init(this.context)
    // 监听事件
    this.callee.on("createCard", (data: rpc.MessageSequence) => {
      // 接收id
      const formId = (JSON.parse(data.readString() as string) as Record<string, string>).formId

      return new MyPara()
    })
  }

5. 接收卡片 id 并且持久化

  1. 开启后台运行权限 "ohos.permission.KEEP_BACKGROUND_RUNNING"

    entry/src/main/module.json5
        "requestPermissions": [
          {
            "name": "ohos.permission.INTERNET"
          },
          {
            "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
          }
        ],
  2. 持久化

     onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        FormIdStore.init(this.context)
        // 监听事件
        this.callee.on("createCard", (data: rpc.MessageSequence) => {
          // 接收id
          const formId = (JSON.parse(data.readString() as string) as Record<string, string>).formId
          // 2 持久化
          FormIdStore.set(formId)
    
          return new MyPara()
        })
      }
    

6. 移除卡片时,删除卡片 id

entry/src/main/ets/entryformability/EntryFormAbility.ets
  onRemoveForm(formId: string) {
    FormIdStore.remove(formId)
  }

封装下载图片工具类

将下载图片和拼接卡片需要格式的代码封装到文件中 该工具类可以同时下载多张图片,使用了 Promise.all 来统一接收结果

entry/src/main/ets/utils/CardDonwLoad.ets

1. 封装的工具说明

interface IDownFile {
  fileName: string;
  imageFd: number;
}

// 卡片显示 需要的数据结构
export class FormDataClass {
  // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), fileName 对应 fd
  formImages: Record<string, number>;

  constructor(formImages: Record<string, number>) {
    this.formImages = formImages;
  }
}

export class CardDownLoad {
  context: Context | null;
  then: Function | null = null;
  imgFds: number[] = [];

  constructor(context: Context) {
    this.context = context;
  }

  // 下载单张图片
  async downLoadImage(netFile: string) {}

  // 下载一组图片
  async downLoadImages(netFiles: string[]) {}

  // 私有下载网络图片的方法
  private async _down(netFile: string) {}

  // 手动关闭文件
  async closeFile() {
    this.imgFds.forEach((fd) => fileIo.closeSync(fd));
    this.imgFds = [];
  }
}

2. 封装的实现

import { http } from "@kit.NetworkKit";
import { fileIo } from "@kit.CoreFileKit";

interface IDownFile {
  fileName: string;
  imageFd: number;
}

// 卡片显示 需要的数据结构
export class FormDataClass {
  // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), fileName 对应 fd
  formImages: Record<string, number>;

  constructor(formImages: Record<string, number>) {
    this.formImages = formImages;
  }
}

export class CardDownLoad {
  context: Context | null;
  then: Function | null = null;
  imgFds: number[] = [];

  constructor(context: Context) {
    this.context = context;
  }

  // 下载单张图片
  async downLoadImage(netFile: string) {
    const obj = await this._down(netFile);
    let imgMap: Record<string, number> = {};
    imgMap[obj.fileName] = obj.imageFd;
    if (!this.imgFds.includes(obj.imageFd)) {
      this.imgFds.includes(obj.imageFd);
    }
    return new FormDataClass(imgMap);
  }

  // 下载一组图片
  async downLoadImages(netFiles: string[]) {
    let imgMap: Record<string, number> = {};

    const promiseAll = netFiles.map((url) => {
      const ret = this._down(url);
      return ret;
    });
    const resList = await Promise.all(promiseAll);
    resList.forEach((v) => {
      imgMap[v.fileName] = v.imageFd;
      if (!this.imgFds.includes(v.imageFd)) {
        this.imgFds.includes(v.imageFd);
      }
    });

    return new FormDataClass(imgMap);
    // return resList.map(v => `memory://${v.fileName}`)
  }

  // 私有下载网络图片的方法
  private async _down(netFile: string) {
    let tempDir = this.context!.getApplicationContext().tempDir;
    let fileName = "file" + Date.now();
    let tmpFile = tempDir + "/" + fileName;

    let httpRequest = http.createHttp();
    let data = await httpRequest.request(netFile);
    if (data?.responseCode == http.ResponseCode.OK) {
      let imgFile = fileIo.openSync(
        tmpFile,
        fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE
      );

      await fileIo.write(imgFile.fd, data.result as ArrayBuffer);

      const obj: IDownFile = {
        fileName,
        imageFd: imgFile.fd,
      };
      // setTimeout(() => {
      // }, 0)
      // fileIo.close(imgFile);
      httpRequest.destroy();
      return obj;
    } else {
      httpRequest.destroy();
      return Promise.reject(null);
    }
  }

  // 手动关闭文件
  async closeFile() {
    this.imgFds.forEach((fd) => fileIo.closeSync(fd));
    this.imgFds = [];
  }
}

卡片发起通知,获取网络图片

PixPin_2024-10-31_20-52-33

image-20241031224019379

  1. 准备好卡片代码,用来接收返回的网络图片数据
  2. 应用 Ability 接收卡片通知,下载网络图片,并且返回给卡片

1. 准备好卡片代码,用来接收返回的网络图片数据

const localStorage = new LocalStorage()

@Entry(localStorage)
@Component
struct WidgetCard {
  // 用来显示图片的数组
  @LocalStorageProp("imgNames")
  imgNames: string[] = []
  // 卡片id
  @LocalStorageProp("formId")
  @Watch("postData")
  formId: string = ""
  // 当前显示的大图 -  和 应用-首页保持同步
  @LocalStorageProp("activeIndex")
  activeIndex: number = 0

  postData() {
    postCardAction(this, {
      action: 'call',
      abilityName: 'EntryAbility',
      params: {
        method: 'createCard',
        formId: this.formId
      }
    });
  }

  build() {
    Row() {
      ForEach(this.imgNames, (url: string, index: number) => {
        Image(url)
          .border({ width: 1 })
          .layoutWeight(this.activeIndex === index ? 2 : 1)
          .height(this.activeIndex === index ? "90%" : "60%")
          .borderRadius(this.activeIndex === index ? 12 : 5)
          .animation({ duration: 300 })
      })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
    .padding(10)
    .backgroundImage(this.imgNames[this.activeIndex])
    .backgroundBlurStyle(BlurStyle.Thin)
    .backgroundImageSize(ImageSize.Cover)
    .animation({ duration: 300 })
  }
}

2. 应用 Ability 接收卡片通知,下载网络图片,并且返回给卡片

entry/src/main/ets/entryability/EntryAbility.ets
// callee中要求返回的数据类型

class MyPara implements rpc.Parcelable {
  marshalling(dataOut: rpc.MessageSequence): boolean {
    return true;
  }

  unmarshalling(dataIn: rpc.MessageSequence): boolean {
    return true;
  }
}

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 监听事件
    this.callee.on("createCard", (data: rpc.MessageSequence) => {
      // 接收id
      const formId = (
        JSON.parse(data.readString() as string) as Record<string, string>
      ).formId;
      // 持久化
      FormIdStore.set(formId);

      class FormData {
        imgName?: string[] = [];
        activeIndex?: number = AppStorage.get("activeIndex")!;
      }

      const formInfo = formBindingData.createFormBindingData(new FormData());
      // 先响应空数据 等待网络图片下载完毕后,再响应网络图片数据
      formProvider.updateForm(formId, formInfo);
      const cardDownLoad = new CardDownLoad(this.context);
      cardDownLoad
        .downLoadImages(AppStorage.get("swiperList") as string[])
        .then((ret) => {
          const urls = Object.keys(ret.formImages).map((v) => `memory://${v}`);
          // 返回卡片数组
          class CimgNames {
            imgNames: string[] = urls;
            formImages: Record<string, number> = ret.formImages;
          }

          const formInfo = formBindingData.createFormBindingData(
            new CimgNames()
          );
          formProvider.updateForm(formId, formInfo);
          //   关闭文件
          cardDownLoad.closeFile();
        });

      // 临时处理、防止报错
      return new MyPara();
    });
  }
}

3. 效果

PixPin_2024-10-31_20-52-33

卡片同步轮播

image-20241031224212664

该功能主要是首页在图片轮播时,通知所有的卡片同时更新

entry/src/main/ets/pages/Index.ets

1. 监听轮播图 onChange 事件,设置当前显示的下标

      Swiper() {
        ForEach(this.swiperList, (img: string) => {
          Image(img)
            .width("80%")
        })
      }
      .loop(true)
      .autoPlay(true)
      .interval(3000)
      .onChange(index => this.activeIndex = index)

2. 监听下标的改变,通知持久化存储中所有的卡片进行更新

  @StorageLink("activeIndex")
  @Watch("changeIndex")
  activeIndex: number = 0

  // 通知所有卡片一并更新
  changeIndex() {
    const list = FormIdStore.getList()
    const index = this.activeIndex
    list.forEach(id => {
      class FdCls {
        activeIndex: number = index
      }

      const formInfo = formBindingData.createFormBindingData(new FdCls())
      formProvider.updateForm(id, formInfo)
    })
  }

3. 效果

PixPin_2024-10-31_22-18-40

总结

FormExtensionAbility 进程不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务,在生命周期调度完成后会继续存在 10 秒,如 10 秒内没有新的

生命周期回调触发则进程自动退出。针对可能需要 10 秒以上才能完成的业务逻辑,建议拉起主应用进行处理,处理完成后使用updateForm通知卡片进行刷

新。

1. 项目开发流程

  1. 新建项目与服务卡片:创建新的项目和服务卡片,为后续开发搭建基础框架。
  2. 设置沉浸式体验:在EntryAbility.ets中进行相关设置,优化用户视觉体验。

2. 首页轮播图数据显示

  1. 申请网络权限:在module.json5中申请,为数据获取做准备。
  2. 新建工具文件:在/utils/index.ets中创建swiperInit函数,用于初始化轮播图数据,包括设置轮播图列表和初始索引。
  3. 初始化操作:在EntryAbility.ets中进行初始化。
  4. 页面使用:在Index.ets中构建轮播图组件,通过SwiperForEach等实现轮播效果,轮播图可自动播放、循环,并能响应索引变化。

3. 卡片 id 的处理

  1. 获取与返回卡片 id:在EntryFormAbility.etsonAddForm函数中获取卡片 id,并返回给卡片组件。原因是后期卡片向应用通信时,应用需根据卡片 id 响应,同时注意formExtensionAbility进程的后台限制。
  2. 接受与显示卡片 id:在WidgetCard.ets中接受并显示卡片 id。
  3. 卡片 id 的持久化存储

    • 封装工具类:在/utils/index.ets中封装FormIdStore类,实现初始化、获取卡片 id 列表、新增和删除卡片 id 等功能。
    • 初始化工具类:在EntryAbility.etsonCreateonAddForm中初始化。
    • 卡片主动上传:在WidgetCard.ets中利用watch监听器触发上传卡片 id。
    • 应用接收与持久化:在EntryAbility.ets中接收卡片 id 并持久化,同时需开启后台运行权限。
    • 移除卡片时处理:在EntryFormAbility.etsonRemoveForm中删除卡片 id。

4. 图片相关操作

  1. 封装下载图片工具类:在CardDonwLoad.ets中封装,包括下载单张或一组图片的功能,以及手动关闭文件功能,涉及网络请求和文件操作。
  2. 卡片发起通知获取网络图片

    • 卡片准备接收数据:在WidgetCard.ets中准备接收网络图片数据的代码,包括显示图片数组、卡片 id 等相关变量和操作。
    • 应用处理与返回数据:在EntryAbility.ets中接收卡片通知,下载网络图片并返回给卡片,先响应空数据,下载完成后再更新卡片数据。

5. 卡片同步轮播功能

  1. 监听轮播图 onChange 事件:在Index.ets中通过Swiper组件的onChange事件设置当前显示下标。
  2. 通知卡片更新:在Index.ets中监听下标改变,通知持久化存储中的所有卡片更新,实现首页与卡片轮播同步。

万少
66 声望5 粉丝