【鸿蒙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)
    }
    }

  2. 应用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 {
  }
}

相关推荐
__Benco2 小时前
OpenHarmony - 小型系统内核(LiteOS-A)(十三),LMS调测
人工智能·harmonyos
梁下轻语的秋缘8 小时前
华为云loT物联网介绍与使用
物联网·学习·华为·华为云
HMS Core9 小时前
HarmonyOS SDK助力鸿蒙版今日水印相机,真实地址防护再升级
数码相机·华为·harmonyos
__Benco12 小时前
OpenHarmony - 小型系统内核(LiteOS-A)(完),内核编码规范
人工智能·harmonyos
23zhgjx-NanKon16 小时前
华为eNSP:IS-IS认证
网络·华为·智能路由器
Bruce_Liuxiaowei21 小时前
HarmonyOS Next~鸿蒙系统流畅性技术解析:预加载与原生架构的协同进化
华为·架构·harmonyos
特立独行的猫a1 天前
HarmonyOS NEXT 诗词元服务项目开发上架全流程实战(二、元服务与应用APP签名打包步骤详解)
华为·打包发布·harmonyos·应用签名·appgallery
A富得流油的咸鸭蛋1 天前
harmonyOS 手机,双折叠,平板,PC端屏幕适配
智能手机·电脑·harmonyos
周胡杰1 天前
鸿蒙文件上传-从前端到后端详解,对比jq请求和鸿蒙arkts请求区别,对比new FormData()和鸿蒙arktsrequest.uploadFile
前端·华为·harmonyos·鸿蒙·鸿蒙系统