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事件的特定是会拉起应用,前台会展示页面,会触发应用的onCreate和onNewWant生命周期
我们可以利用这个特性做唤起特定页面并且传递数据。
当触发router事件时,
- 如果应用没有在运行,便触发 onCreate事件
- 如果应用正在运行,便触发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"
}
],
- 卡片组件触发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)
}
}
- 应用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 {
}
}