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對象的創建、相關信息的描述// 獲取卡片 IDconst 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=0build() {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 {// 每一張卡片創建時都會被分配一個唯一的idformId: 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: numberaa: number}interface IRes extends IData {params: IData,action: "message"formId: string}const params = JSON.parse(message) as IResconsole.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=0build() {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', // 只能跳轉到當前應用下的UIAbilityparams: {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 因為我們直接點擊圖標,也會拉起應用,此時不會有formIdif (want.parameters && want.parameters[formInfo.FormParam.IDENTITY_KEY] !== undefined) {// 獲取卡片的formIdconst 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,會觸發onNewWantonNewWant(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.targetPageconsole.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 resourceshilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');}onForeground(): void {// Ability has brought to foregroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');}onBackground(): void {// Ability has back to backgroundhilog.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 = 100build() {Column() {Button("call事件" + this.num).onClick(() => {postCardAction(this, {action: 'call',abilityName: 'EntryAbility', // 只能跳轉到當前應用下的UIAbilityparams: {// 如果事件類型是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: stringnum: number}// 讀取參數const params = JSON.parse(data.readString() as string) as IResinterface 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 foregroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');}onBackground(): void {// Ability has back to backgroundhilog.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.jpgconst 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.pnglet tempDir = this.context.getApplicationContext().tempDir;let imgMap: Record<string, number> = {};// 打開本地圖片并獲取其打開后的fdlet file = fileIo.openSync(tempDir + '/' + '123.png');//file.fd 打開的文件描述符。imgMap['imgBear'] = file.fd;class FormDataClass {// 卡片需要顯示圖片場景, 必須和下列字段formImages 中的key 'imgBear' 相同。imgName: string = 'imgBear';// 卡片需要顯示圖片場景, 必填字段(formImages 不可缺省或改名), 'imgBear' 對應 fdformImages: 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 對應 fdformImages: 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 {}
}