【鸿蒙HarmonyOS】一文详解华为的服务卡片

7.服务卡片

1.什么是卡片

Form Kit(卡片开发服务)提供一种界面展示形式,可以将应用的重要信息或操作前置到服务卡片(以下简称“卡片”),以达到服务直达、减少跳转层级的体验效果。卡片常用于嵌入到其他应用(当前被嵌入方即卡片使用方只支持系统应用,例如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互能力。

2.卡片的一些配置参数

entry/src/main/resources/base/profile/form_config.json

3. 卡片的生命周期

//卡片生命周期
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';

export default class EntryFormAbility extends FormExtensionAbility {
  // 	卡片被创建时触发
  onAddForm(want: Want) {
    // formBindingData:提供卡片数据绑定的能力,包括FormBindingData对象的创建、相关信息的描述
    // 获取卡片 ID
    const formId = want.parameters && want.parameters['ohos.extra.param.key.form_identity'].toString()
    return formBindingData.createFormBindingData({
      title: '获取数据中~'
    });
    // Called to return a FormBindingData object.
    const formData = '';
    return formBindingData.createFormBindingData(formData);
  }

  // 卡片转换成常态卡片时触发
  onCastToNormalForm(formId: string) {
    // Called when the form provider is notified that a temporary form is successfully
    // converted to a normal form.
  }

  // 卡片被更新时触发(调用 updateForm 时)
  onUpdateForm(formId: string) {
    // Called to notify the form provider to update a specified form.
  }

  // 卡片发起特定事件时触发(message)
  onFormEvent(formId: string, message: string) {
    // Called when a specified message event defined by the form provider is triggered.
  }

  //卡片被卸载时触发
  onRemoveForm(formId: string) {
    // Called to notify the form provider that a specified form has been destroyed.
  }

  // 卡片状态发生改变时触发
  onAcquireFormState(want: Want) {
    // Called to return a {@link FormState} object.
    return formInfo.FormState.READY;
  }
}

4.卡片的通信

1.卡片之间通信

卡片在创建时,会触发onAddForm生命周期,此时返回数据可以直接传递给卡片

另外卡片在被卸载时,会触发onRemoveForm生命周期

1.卡片创建时传递数据

2.卡片向卡片的生命周期通信

卡片页面中可以通过postCardAction接口触发message事件拉起FormExtensionAbility中的onUpdateForm

onUpdateForm中通过updateForm来返回数据

const localStorage = new LocalStorage()
// 卡片组件通过LocalStorage来接收onAddForm中返回的数据
@Entry(localStorage)
@Component
struct WidgetCard {
  // 接收onAddForm中返回的卡片Id
  @LocalStorageProp("formId")
  formId: string = "xxx"
@LocalStorageProp('num')
num:number=0
  build() {
    Column() {
      Button(this.formId)
      Text(`${this.num}`).fontSize(15)
      Button('点击数字+100')
        .onClick(() => {
         postCardAction(this, {
           action: 'message',
           // 提交过去的参数
           params: { num: this.num, aa: 200, formId: this.formId }
         })
        })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

记得要携带formId过去,因为返回数据时需要根据formId找到对应的卡片

//卡片生命周期
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { JSON } from '@kit.ArkTS';

export default class EntryFormAbility extends FormExtensionAbility {
  // 	卡片被创建时触发
  onAddForm(want: Want) {
    // formBindingData:提供卡片数据绑定的能力,包括FormBindingData对象的创建、相关信息的描述
    class FormData {
      // 每一张卡片创建时都会被分配一个唯一的id
      formId: string = want.parameters!['ohos.extra.param.key.form_identity'].toString();
    }

    let formData = new FormData()
    // console.log('测试',JSON.stringify(formData))
    // 返回数据给卡片
    return formBindingData.createFormBindingData(formData);

  }

  // 卡片转换成常态卡片时触发
  onCastToNormalForm(formId: string) {
    // Called when the form provider is notified that a temporary form is successfully
    // converted to a normal form.
  }

  // 卡片被更新时触发(调用 updateForm 时)
  onUpdateForm(formId: string) {
    // Called to notify the form provider to update a specified form.
    console.log('测试','卡片更新了')
  }

  // 卡片发起特定事件时触发(message)
  onFormEvent(formId: string, message: string) {
    //   接收到卡片通过message事件传递的数据
    // message {"num":0,"aa":200,"params":{"num":100,"aa":200},"action":"message"}
    interface IData {
      num: number
      aa: number
    }

    interface IRes extends IData {
      params: IData,
      action: "message"
      formId: string
    }

    const params = JSON.parse(message) as IRes
    console.log('测试',JSON.stringify(params))
    interface IRet {
      num: number
    }

    const data: IRet = {
      num: params.num + 100
    }

    const formInfo = formBindingData.createFormBindingData(data)
    console.log('测试',JSON.stringify(formInfo))
    // 返回数据给对应的卡片
    formProvider.updateForm(params.formId, formInfo)

  }

  //卡片被卸载时触发
  onRemoveForm(formId: string) {
    // Called to notify the form provider that a specified form has been destroyed.
  }

  // 卡片状态发生改变时触发
  onAcquireFormState(want: Want) {
    // Called to return a {@link FormState} object.
    return formInfo.FormState.READY;
  }
}

当卡片组件发起message事件时,我们可以通过onFormEvent监听到

数据接收要声明对应的接口

formProvider.updateForm(params.formId, formInfo)更新卡片

2.卡片与应用之间的通信

1.router 通信

router事件的特定是会拉起应用,前台会展示页面,会触发应用的onCreateonNewWant生命周期

我们可以利用这个特性做唤起特定页面并且传递数据。

当触发router事件时,

  1. 如果应用没有在运行,便触发 onCreate事件
  2. 如果应用正在运行,便触发onNewWant事件
const localStorage = new LocalStorage()
// 卡片组件通过LocalStorage来接收onAddForm中返回的数据
@Entry(localStorage)
@Component
struct WidgetCard {
  // 接收onAddForm中返回的卡片Id
  @LocalStorageProp("formId")
  formId: string = "xxx"
@LocalStorageProp('num')
num:number=0
  build() {
    Column() {
      //卡片与卡片的声明周期
      Button(this.formId)
      Text(`${this.num}`).fontSize(15)
      Button('点击数字+100')
        .onClick(() => {
         postCardAction(this, {
           action: 'message',
           // 提交过去的参数
           params: { num: this.num, aa: 200, formId: this.formId }
         })
        })
      //router通信
      Button("跳转到主页")
        .margin({top:10})
        .onClick(() => {
          postCardAction(this, {
            action: 'router',
            abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
            params: {
              targetPage: 'pages-3路由与axios/Index',
            }
          });
        })

    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}
                                                                                      

解析传递过来的卡片 id 与卡片的参数

分别在应用的onCreate和onNewWant编写逻辑实现跳转页面

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { display, router, window } from '@kit.ArkUI';
import { formInfo } from '@kit.FormKit';

export default class EntryAbility extends UIAbility {

  // 要跳转的页面 默认是首页
  targetPage: string = "pages/Demo"
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 判断是否带有formId 因为我们直接点击图标,也会拉起应用,此时不会有formId
    if (want.parameters && want.parameters[formInfo.FormParam.IDENTITY_KEY] !== undefined) {
      // 获取卡片的formId
      const formId = want.parameters![formInfo.FormParam.IDENTITY_KEY].toString();
      // 获取卡片传递过来的参数
      interface IData {
        targetPage: string
      }

      const params: IData = (JSON.parse(want.parameters?.params as string))
      console.log('测试','应用没有运行')
      this.targetPage = params.targetPage
      //   我们也可以在这里通过 updateForm(卡片id,数据) 来返回内容给卡片
    }

  }
  // 如果应用已经在运行,卡片的router事件不会再触发onCreate,会触发onNewWant
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    const formId = want.parameters![formInfo.FormParam.IDENTITY_KEY].toString();
    // 获取卡片传递过来的参数
    interface IData {
      targetPage: string
    }

    const params: IData = (JSON.parse(want.parameters?.params as string))
    this.targetPage = params.targetPage
    console.log('测试','应用已经在运行')
    // 跳转页面
    router.pushUrl({
      url: this.targetPage
    })
    //   我们也可以在这里通过 updateForm(卡片id,数据) 来返回内容给卡片
  }

  onDestroy(): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    //模拟器启动
    windowStage.loadContent(this.targetPage, (err) => {
    console.log('测试',this.targetPage)
    });
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

2.call 通信

call会拉起应用,但是会在后台的形式运行。需要申请后台运行权限,可以进行比较耗时的任务

需要申请后台运行应用权限

{
  "module": {
	// ...
    "requestPermissions": [
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
      }
    ],
  1. 卡片组件触发call事件,参数中必须携带method属性,用来区分不同的方法
export const localStorage = new LocalStorage()

@Entry(localStorage)
@Component
struct WidgetCard {
  // 接收onAddForm中返回的卡片Id
  @LocalStorageProp("formId")
  formId: string = "xxx"
  @LocalStorageProp("num")
  num: number = 100

  build() {
    Column() {
      Button("call事件" + this.num)
        .onClick(() => {
          postCardAction(this, {
            action: 'call',
            abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
            params: {
              // 如果事件类型是call,必须传递method属性,用来区分不同的事件
              method: "inc",
              formId: this.formId,
              num: this.num,
            }
          });
        })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}
  1. 应用EntryAbility在onCreate中,通过 callee来监听不同的method事件
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { router, window } from '@kit.ArkUI';
import { formBindingData, formInfo, formProvider } from '@kit.FormKit';
import { rpc } from '@kit.IPCKit';

// 占位 防止语法出错,暂无实际作用
class MyParcelable implements rpc.Parcelable {
  marshalling(dataOut: rpc.MessageSequence): boolean {
    return true
  }

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


export default class EntryAbility extends UIAbility {
  // 要跳转的页面 默认是首页
  targetPage: string = "pages/Index"

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    //   监听call事件中的特定方法
    this.callee.on("inc", (data: rpc.MessageSequence) => {
      // data中存放的是我们的参数
      params: {
        // 如果事件类型是call,必须传递method属性,用来区分不同的事件
        // method: "inc",
        // formId: this.formId,
        // num: this.num,
        interface IRes {
          formId: string
          num: number
        }

        // 读取参数
        const params = JSON.parse(data.readString() as string) as IRes
        interface IData {
          num: number
        }

        // 修改数据
        const info: IData = {
          num: params.num + 100
        }
        // 响应数据
        const dataInfo = formBindingData.createFormBindingData(info)
        formProvider.updateForm(params.formId, dataInfo)
      }

      // 防止语法报错,暂无实际应用
      return new MyParcelable()
    })

  }


  onWindowStageCreate(windowStage: window.WindowStage): void {

    // 跳转到对应的页面
    windowStage.loadContent(this.targetPage, (err) => {
      if (err.code) {
        return;
      }
    });
  }


  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

5.卡片与图片的通信

1.传递本地图片

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  async aboutToAppear() {
    //-------------------------------------------------------------- 1.初始化图片配置项
    // 创建一个新的 PhotoSelectOptions 实例来配置图片选择器的行为
    let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    // 设置 MIME 类型为图像类型,这样用户只能选择图像文件
    PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    // 设置用户可以选择的最大图片数量为 1 张
    PhotoSelectOptions.maxSelectNumber = 1;
    //----------------------------------------------------------- 2.打开图片选择器,拿到图片
    // 创建一个新的 PhotoViewPicker 实例,用于打开图片选择器
    let photoPicker = new photoAccessHelper.PhotoViewPicker();
    // 使用前面配置好的选项打开图片选择器,并等待用户完成选择
    // 注意这里的 select 方法是一个异步方法,所以需要使用 await 关键字等待其结果
    const PhotoSelectResult = await photoPicker.select(PhotoSelectOptions);
    // 获取用户选择的第一张图片的 URI(统一资源标识符)
    // 假设这里只关心用户选择的第一张图片
    // uri file://media/Photo/3/IMG_1729864738_002/screenshot_20241025_215718.jpg
    const uri = PhotoSelectResult.photoUris[0];
    promptAction.showToast({ message: `${uri}` })
    //------------------------------------------------------------- 3.拷贝图片到临时目录
    // 获取应用的临时目录
    let tempDir = getContext(this).getApplicationContext().tempDir;
    // 生成一个新的文件名
    const fileName = 123 + '.png'
    // 通过缓存路径+文件名 拼接出完整的路径
    const copyFilePath = tempDir + '/' + fileName
    // 将文件 拷贝到 临时目录
    const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY)
    fileIo.copyFileSync(file.fd, copyFilePath)

  }

  build() {
    RelativeContainer() {

    }
    .height('100%')
    .width('100%')
  }
}

一旦保存到本地缓存除非卸载应用不然就一直有的

import { Want } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
import { formBindingData, FormExtensionAbility } from '@kit.FormKit';

export default class EntryFormAbility extends FormExtensionAbility {
  // 在添加卡片时,打开一个本地图片并将图片内容传递给卡片页面显示
  onAddForm(want: Want): formBindingData.FormBindingData {
    // 假设在当前卡片应用的tmp目录下有一个本地图片 123.png
    let tempDir = this.context.getApplicationContext().tempDir;
    let imgMap: Record<string, number> = {};
    // 打开本地图片并获取其打开后的fd
    let file = fileIo.openSync(tempDir + '/' + '123.png');
    //file.fd 打开的文件描述符。
    imgMap['imgBear'] = file.fd;

    class FormDataClass {
      // 卡片需要显示图片场景, 必须和下列字段formImages 中的key 'imgBear' 相同。
      imgName: string = 'imgBear';
      // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), 'imgBear' 对应 fd
      formImages: Record<string, number> = imgMap;
    }

    let formData = new FormDataClass();
    console.log("formDataformData", JSON.stringify(formData))
    // 将fd封装在formData中并返回至卡片页面
    return formBindingData.createFormBindingData(formData);
  }
}
let storageWidgetImageUpdate = new LocalStorage();

@Entry(storageWidgetImageUpdate)
@Component
struct WidgetCard {
  @LocalStorageProp('imgName') imgName: ResourceStr = "";

  build() {
    Column() {
      Text(this.imgName)
    }
    .width('100%').height('100%')
    .backgroundImage('memory://' + this.imgName)
    .backgroundImageSize(ImageSize.Cover)
  }
}

Image组件通过入参(memory://fileName)中的(memory://)标识来进行远端内存图片显示,其中fileName需要和EntryFormAbility传递对象('formImages': {key: fd})中的key相对应。

2.传递网络图片

import { Want } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';

export default class EntryFormAbility extends FormExtensionAbility {
  // 将网络图片传递给
  onAddForm(want: Want) {
    // 注意:FormExtensionAbility在触发生命周期回调时被拉起,仅能在后台存在5秒
    // 建议下载能快速下载完成的小文件,如在5秒内未下载完成,则此次网络图片无法刷新至卡片页面上
    const formId = want.parameters![formInfo.FormParam.IDENTITY_KEY] as string
        // 需要在此处使用真实的网络图片下载链接
    let netFile =
      'https://env-00jxhf99mujs.normal.cloudstatic.cn/card/3.webp?expire_at=1729871552&er_sign=0eb3f6ac3730703039b1565b6d3e59ad'; 


    let httpRequest = http.createHttp()
    // 下载图片
    httpRequest.request(netFile)
      .then(async (data) => {
        if (data?.responseCode == http.ResponseCode.OK) {
          // 拼接图片地址
          let tempDir = this.context.getApplicationContext().tempDir;
          let fileName = 'file' + Date.now();
          let tmpFile = tempDir + '/' + fileName;
          let imgMap: Record<string, number> = {};

          class FormDataClass {
            // 卡片需要显示图片场景, 必须和下列字段formImages 中的key fileName 相同。
            imgName: string = fileName;
            // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), fileName 对应 fd
            formImages: Record<string, number> = imgMap;
          }

          // 打开文件
          let imgFile = fileIo.openSync(tmpFile, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
          imgMap[fileName] = imgFile.fd;
          // 写入文件
          await fileIo.write(imgFile.fd, data.result as ArrayBuffer);
          let formData = new FormDataClass();
          let formInfo = formBindingData.createFormBindingData(formData);
          // 下载完网络图片后,再传递给卡片
          formProvider.updateForm(formId, formInfo)
          fileIo.closeSync(imgFile);
          httpRequest.destroy();
          console.log("============")
        }
      })
      .catch((e: BusinessError) => {
        console.log("eeee", e.message)
      })

    class FormData {
      formId: string = ""
    }

    // 先返回基本数据
    return formBindingData.createFormBindingData(new FormData);

  }


  onFormEvent(formId: string, message: string): void {
  }
}

华为鸿蒙HarmonyOS开发整理资料汇总,共38份。 1学前必读:HarmonyOS学习资源主题分享 2学前必读:OpenHarmony-联盟生态资料合集 3-1.HarmonyOS概述:技术特性 3-2.HarmonyOS概述:开发工具与平台 3-3.HarmonyOS概述:系统安全 3-4.HarmonyOS概述:系统定义 3-5.HarmonyOS概述:下载与安装软件 3-6.HarmonyOS概述:应用开发基础知识 3-7.HarmonyOS概述:最全HarmonyOS文档和社区资源使用技巧 4-1.生态案例:【开发者说】重塑经典,如何在HarmonyOS手机上还原贪吃蛇游戏 4-2.生态案例:HarmonyOLabo涂鸦鸿蒙亲子版 4-3.生态案例:HarmonyOS分镜头APP案例 4-4.生态案例:HarmonyOS时光序历史学习案例 4-5.生态案例:HarmonyOS先行者说 宝宝巴士携手HarmonyOS共同打造儿童教育交互新体验 4-6.生态案例:HarmonyOS智能农场物联网连接实践 4-7.生态案例:分布式开发样例,带你玩转多设备 4-8.生态案例:华为分布式日历应用开发实践 5-1.【Codelab】HarmonyOS基于图像模块实现图库图片的四种常见操作 5-2.【CodeLab】手把手教你创建第一个手机“Hello World” 5-3.【Codelab】如此简单!一文带你学会15个HarmonyOS JS组件 5-4.【Codelab】懒人“看”书新法—鸿蒙语音播报,到底如何实现? 5-5.【Codelab】基于AI通用文字识别的图像搜索,这波操作亮了 5-6.【Codelab】开发样例概览 6-1.技术解读之HarmonyOS轻量JS开发框架与W3C标准差异分析 6-2.技术解读之HarmonyOS驱动加载过程分析 6-3.技术解读之HarmonyOS组件库使用实践 6-4.技术解读之华为架构师解读:HarmonyOS低时延高可靠消息传输原理 6-5.技术解读之解密HarmonyOS UI框架 6-6.技术解读之如何从OS框架层面实现应用服务功能解耦 7-1.常见问题之HarmonyOS服务的设计与开发解析 7-2.常见问题之Java开发 7-3.常见问题之JS开发 7-4.常见问题之模拟器登录 7-5.常见问题之模拟器运行 7-6.常见问题之如何使用JsJava开发HarmonyOS UI 7-7.常见问题之应用配置 7-8.常见问题之预览器运行 8【视频合集】入门到进阶视频学习资料合集30+
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值