1# 卡片使用方开发指导(仅对系统应用开放)
2
3## 卡片概述
4
5卡片是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达,减少体验层级的目的。
6
7卡片常用于嵌入到其他应用(当前只支持系统应用)中作为其界面的一部分显示,并支持拉起页面,发送消息等基础的交互功能。卡片使用方负责显示卡片。
8
9- 卡片的基本概念:
10
11  - 卡片提供方:提供卡片显示内容原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。
12
13  - 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
14
15  - 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。
16
17   ![formHostMoudle](./figures/widget-host-development-guide-1.png)
18
19## 场景介绍
20
21卡片使用方开发,即基于Stage模型的卡片使用方开发,主要指导如下:
22
23- 卡片组件FormComponent的使用。
24- 通过formHost模块提供的卡片使用方相关接口操作卡片的删除、更新等行为。
25
26## 卡片组件
27
28提供卡片组件,实现卡片的显示功能。详情见[FormComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-formcomponent-sys.md)。
29
30> **说明:**
31>
32> - 该组件从API Version 7开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。
33>
34> - 该组件为卡片组件的使用方。
35>
36> - 该组件使用需要具有系统签名。
37>
38> - 本模块为系统接口。
39
40通过卡片组件成功添加卡片时,会调用到卡片提供方FormExtensionAbility中的[onAddForm](../reference/apis-form-kit/js-apis-app-form-formExtensionAbility.md#onaddform)方法。
41
42### 临时卡片和常态卡片
43
44在卡片组件中的temporary字段可以配置卡片是临时卡片还是常态卡片。true为临时卡片,false为常态卡片。
45
46- 常态卡片:卡片使用方会持久化的卡片。如添加到桌面的卡片。
47
48- 临时卡片:卡片使用方不会持久化的卡片。如上划卡片应用时显示的卡片。
49
50由于临时卡片的数据具有非持久化的特殊性,某些场景例如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片ID不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片ID进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。
51
52## formHost接口
53
54formHost提供一系列的卡片使用方接口,来操作卡片的更新、删除等行为,具体的API介绍详见[接口文档](../reference/apis-form-kit/js-apis-app-form-formHost-sys.md)。
55
56## 卡片使用方示例
57
58```ts
59//Index.ets
60import { HashMap, HashSet } from '@kit.ArkTS';
61import { formHost, formInfo, formObserver } from '@kit.FormKit';
62import { bundleMonitor } from '@kit.AbilityKit';
63import { BusinessError } from '@kit.BasicServicesKit';
64
65@Entry
66@Component
67struct formHostSample {
68  // 卡片尺寸枚举。
69  static FORM_DIMENSIONS_MAP = [
70    '1*2',
71    '2*2',
72    '2*4',
73    '4*4',
74    '2*1',
75    '1*1',
76    '6*4',
77  ]
78
79  // 模拟卡片尺寸。
80  static FORM_SIZE = [
81    [120, 60],    // 1*2
82    [120, 120],   // 2*2
83    [240, 120],   // 2*4
84    [240, 240],   // 4*4
85    [60, 120],    // 2*1
86    [60, 60],     // 1*1
87    [240, 360],   // 6*4
88  ]
89
90  @State message: Resource | string = $r('app.string.Host');
91  formCardHashMap: HashMap<string, formInfo.FormInfo> = new HashMap();
92  @State showFormPicker: boolean = false;
93  @State operation: Resource | string = $r('app.string.formOperation');
94  @State index: number = 2;
95  @State space: number = 8;
96  @State arrowPosition: ArrowPosition = ArrowPosition.END;
97  formIds: HashSet<string> = new HashSet();
98  currentFormKey: string = '';
99  focusFormInfo: formInfo.FormInfo = {
100    bundleName: '',
101    moduleName: '',
102    abilityName: '',
103    name: '',
104    displayName: '',
105    displayNameId: 0,
106    description: '',
107    descriptionId: 0,
108    type: formInfo.FormType.eTS,
109    jsComponentName: '',
110    colorMode: formInfo.ColorMode.MODE_AUTO,
111    isDefault: false,
112    updateEnabled: false,
113    formVisibleNotify: true,
114    scheduledUpdateTime: '',
115    formConfigAbility: '',
116    updateDuration: 0,
117    defaultDimension: 6,
118    supportDimensions: [],
119    supportedShapes: [],
120    customizeData: {},
121    isDynamic: false,
122    transparencyEnabled: false
123  }
124  formInfoRecord: TextCascadePickerRangeContent[] = [];
125  pickerBtnMsg: Resource | string = $r('app.string.formType');
126  @State showForm: boolean = true;
127  @State selectFormId: string = '0';
128  @State pickDialogIndex: number = 0;
129
130  aboutToAppear(): void {
131    try {
132      // 检查系统是否准备好。
133      formHost.isSystemReady().then(() => {
134        console.log('formHost isSystemReady success');
135
136        // 订阅通知卡片不可见的事件和卡片可见通知事件。
137        let notifyInvisibleCallback = (data: formInfo.RunningFormInfo[]) => {
138          console.log(`form change invisibility, data: ${JSON.stringify(data)}`);
139        }
140        let notifyVisibleCallback = (data: formInfo.RunningFormInfo[]) => {
141          console.log(`form change visibility, data: ${JSON.stringify(data)}`);
142        }
143        formObserver.on('notifyInvisible', notifyInvisibleCallback);
144        formObserver.on('notifyVisible', notifyVisibleCallback);
145
146        // 注册监听应用的安装事件。
147        try {
148          bundleMonitor.on('add', (bundleChangeInfo) => {
149            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
150            this.getAllBundleFormsInfo();
151          })
152        } catch (errData) {
153          let message = (errData as BusinessError).message;
154          let errCode = (errData as BusinessError).code;
155          console.log(`errData is errCode:${errCode}  message:${message}`);
156        }
157        // 注册监听应用的更新事件。
158        try {
159          bundleMonitor.on('update', (bundleChangeInfo) => {
160            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
161            this.getAllBundleFormsInfo();
162          })
163        } catch (errData) {
164          let message = (errData as BusinessError).message;
165          let errCode = (errData as BusinessError).code;
166          console.log(`errData is errCode:${errCode}  message:${message}`);
167        }
168        // 注册监听应用的卸载事件。
169        try {
170          bundleMonitor.on('remove', (bundleChangeInfo) => {
171            console.info(`bundleName : ${bundleChangeInfo.bundleName} userId : ${bundleChangeInfo.userId}`);
172            this.getAllBundleFormsInfo();
173          })
174        } catch (errData) {
175          let message = (errData as BusinessError).message;
176          let errCode = (errData as BusinessError).code;
177          console.log(`errData is errCode:${errCode}  message:${message}`);
178        }
179      }).catch((error: BusinessError) => {
180        console.error(`error, code: ${error.code}, message: ${error.message}`);
181      });
182    }
183    catch (error) {
184      console.error(`catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
185    }
186  }
187
188  aboutToDisappear(): void {
189    // 删除所有卡片。
190    this.formIds.forEach((id) => {
191      console.log('delete all form')
192      formHost.deleteForm(id);
193    });
194    // 注销监听应用的安装。
195    try {
196      bundleMonitor.off('add');
197    } catch (errData) {
198      let message = (errData as BusinessError).message;
199      let errCode = (errData as BusinessError).code;
200      console.log(`errData is errCode:${errCode}  message:${message}`);
201    }
202    // 注销监听应用的更新。
203    try {
204      bundleMonitor.off('update');
205    } catch (errData) {
206      let message = (errData as BusinessError).message;
207      let errCode = (errData as BusinessError).code;
208      console.log(`errData is errCode:${errCode}  message:${message}`);
209    }
210    // 注销监听应用的卸载。
211    try {
212      bundleMonitor.off('remove');
213    } catch (errData) {
214      let message = (errData as BusinessError).message;
215      let errCode = (errData as BusinessError).code;
216      console.log(`errData is errCode:${errCode}  message:${message}`);
217    }
218    // 取消订阅通知卡片不可见和通知卡片可见事件。
219    formObserver.off('notifyInvisible');
220    formObserver.off('notifyVisible');
221  }
222
223  // 将所有卡片信息存入formHapRecordMap中。
224  getAllBundleFormsInfo() {
225    this.formCardHashMap.clear();
226    this.showFormPicker = false;
227    let formHapRecordMap: HashMap<string, formInfo.FormInfo[]> = new HashMap();
228    this.formInfoRecord = [];
229    formHost.getAllFormsInfo().then((formList: Array<formInfo.FormInfo>) => {
230      console.log('getALlFormsInfo size:' + formList.length)
231      for (let formItemInfo of formList) {
232        let formBundleName = formItemInfo.bundleName;
233        if (formHapRecordMap.hasKey(formBundleName)) {
234          formHapRecordMap.get(formBundleName).push(formItemInfo)
235        } else {
236          let formInfoList: formInfo.FormInfo[] = [formItemInfo];
237          formHapRecordMap.set(formBundleName, formInfoList);
238        }
239      }
240      for (let formBundle of formHapRecordMap.keys()) {
241        let bundleFormInfo: TextCascadePickerRangeContent = {
242          text: formBundle,
243          children: []
244        }
245        let bundleFormList: formInfo.FormInfo[] = formHapRecordMap.get(formBundle);
246        bundleFormList.forEach((formItemInfo) => {
247          let dimensionName = formHostSample.FORM_DIMENSIONS_MAP[formItemInfo.defaultDimension - 1];
248          bundleFormInfo.children?.push({ text: formItemInfo.name + '#' + dimensionName });
249          this.formCardHashMap.set(formBundle + "#" + formItemInfo.name + '#' + dimensionName, formItemInfo);
250        })
251        this.formInfoRecord.push(bundleFormInfo);
252      }
253      this.formCardHashMap.forEach((formItem: formInfo.FormInfo) => {
254        console.info(`formCardHashmap: ${JSON.stringify(formItem)}`);
255      })
256      this.showFormPicker = true;
257    })
258  }
259
260  build() {
261    Column() {
262      Text(this.message)
263        .fontSize(30)
264        .fontWeight(FontWeight.Bold)
265
266      Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 })
267
268      Row() {
269        // 点击查询所有卡片信息。
270        Button($r('app.string.inquiryForm'))
271          .onClick(() => {
272            this.getAllBundleFormsInfo();
273          })
274
275        // 点击按钮弹出选择界面,点击确定后,添加默认尺寸的所选卡片。
276        Button($r('app.string.selectAddForm'))
277          .enabled(this.showFormPicker)
278          .onClick(() => {
279            console.info("TextPickerDialog: show()")
280            TextPickerDialog.show({
281              range: this.formInfoRecord,
282              selected: this.pickDialogIndex,
283              canLoop: false,
284              disappearTextStyle: { color: Color.Red, font: { size: 10, weight: FontWeight.Lighter } },
285              textStyle: { color: Color.Black, font: { size: 12, weight: FontWeight.Normal } },
286              selectedTextStyle: { color: Color.Blue, font: { size: 12, weight: FontWeight.Bolder } },
287              onAccept: (result: TextPickerResult) => {
288                this.currentFormKey = result.value[0] + "#" + result.value[1];
289                this.pickDialogIndex = result.index[0]
290                console.info(`TextPickerDialog onAccept: ${this.currentFormKey}, ${this.pickDialogIndex}`);
291                if (!this.formCardHashMap.hasKey(this.currentFormKey)) {
292                  console.error(`invalid formItemInfo by form key`)
293                  return;
294                }
295                this.showForm = true;
296                this.focusFormInfo = this.formCardHashMap.get(this.currentFormKey);
297              },
298              onCancel: () => {
299                console.info("TextPickerDialog : onCancel()")
300              },
301              onChange: (result: TextPickerResult) => {
302                this.pickerBtnMsg = result.value[0] + '#' + result.value[1];
303                console.info("TextPickerDialog:onChange:" + this.pickerBtnMsg)
304              }
305            })
306          })
307          .margin({ left: 10 })
308      }
309      .margin({ left: 10 })
310
311      Divider().vertical(false).color(Color.Black).lineCap(LineCapStyle.Butt).margin({ top: 10, bottom: 10 })
312
313      if(this.showForm){
314        Text(this.pickerBtnMsg)
315          .margin({ top: 10, bottom: 10 })
316      }
317
318      if (this.showForm) {
319        Text('formId: ' + this.selectFormId)
320          .margin({ top: 10, bottom: 10 })
321
322        // 卡片组件。
323        FormComponent({
324          id: Number.parseInt(this.selectFormId),
325          name: this.focusFormInfo.name,
326          bundle: this.focusFormInfo.bundleName,
327          ability: this.focusFormInfo.abilityName,
328          module: this.focusFormInfo.moduleName,
329          dimension: this.focusFormInfo.defaultDimension,
330          temporary: false,
331        })
332          .size({
333            width: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][0],
334            height: formHostSample.FORM_SIZE[this.focusFormInfo.defaultDimension - 1][1],
335          })
336          .borderColor(Color.Black)
337          .borderRadius(10)
338          .borderWidth(1)
339          .onAcquired((form: FormCallbackInfo) => {
340            console.log(`onAcquired: ${JSON.stringify(form)}`)
341            this.selectFormId = form.id.toString();
342            this.formIds.add(this.selectFormId);
343          })
344          .onRouter(() => {
345            console.log(`onRouter`)
346          })
347          .onError((error) => {
348            console.error(`onError: ${JSON.stringify(error)}`)
349            this.showForm = false;
350          })
351          .onUninstall((info: FormCallbackInfo) => {
352            this.showForm = false;
353            console.error(`onUninstall: ${JSON.stringify(info)}`)
354            this.formIds.remove(this.selectFormId);
355          })
356
357        // select列表,列出部分formHost接口功能。
358        Row() {
359          Select([{ value: $r('app.string.deleteForm') },
360            { value: $r('app.string.updateForm') },
361            { value: $r('app.string.visibleForms') },
362            { value: $r('app.string.invisibleForms') },
363            { value: $r('app.string.enableFormsUpdate') },
364            { value: $r('app.string.disableFormsUpdate') },
365          ])
366            .selected(this.index)
367            .value(this.operation)
368            .font({ size: 16, weight: 500 })
369            .fontColor('#182431')
370            .selectedOptionFont({ size: 16, weight: 400 })
371            .optionFont({ size: 16, weight: 400 })
372            .space(this.space)
373            .arrowPosition(this.arrowPosition)
374            .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
375            .optionWidth(200)
376            .optionHeight(300)
377            .onSelect((index: number, text?: string | Resource) => {
378              console.info('Select:' + index)
379              this.index = index;
380              if (text) {
381                this.operation = text;
382              }
383            })
384
385          // 根据select列表所选的功能,对当前卡片执行对应操作。
386          Button($r('app.string.execute'), {
387            type: ButtonType.Capsule
388          })
389            .fontSize(16)
390            .onClick(() => {
391              switch (this.index) {
392                case 0:
393                  try {
394                    formHost.deleteForm(this.selectFormId, (error: BusinessError) => {
395                      if (error) {
396                        console.error(`deleteForm error, code: ${error.code}, message: ${error.message}`);
397                      } else {
398                        console.log('formHost deleteForm success');
399                      }
400                    });
401                  } catch (error) {
402                    console.error(`deleteForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
403                  }
404                  this.showForm = false;
405                  this.selectFormId = '';
406                  break;
407                case 1:
408                  try {
409                    formHost.requestForm(this.selectFormId, (error: BusinessError) => {
410                      if (error) {
411                        console.error(`requestForm error, code: ${error.code}, message: ${error.message}`);
412                      }
413                    });
414                  } catch (error) {
415                    console.error(`requestForm catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
416                  }
417                  break;
418                case 2:
419                  try {
420                    formHost.notifyVisibleForms([this.selectFormId], (error: BusinessError) => {
421                      if (error) {
422                        console.error(`notifyVisibleForms error, code: ${error.code}, message: ${error.message}`);
423                      } else {
424                        console.info('notifyVisibleForms success');
425                      }
426                    });
427                  } catch (error) {
428                    console.error(`notifyVisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
429                  }
430                  break;
431                case 3:
432                  try {
433                    formHost.notifyInvisibleForms([this.selectFormId], (error: BusinessError) => {
434                      if (error) {
435                        console.error(`notifyInvisibleForms error, code: ${error.code}, message: ${error.message}`);
436                      } else {
437                        console.info('notifyInvisibleForms success');
438                      }
439                    });
440                  } catch (error) {
441                    console.error(`notifyInvisibleForms catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
442                  }
443                  break;
444                case 4:
445                  try {
446                    formHost.enableFormsUpdate([this.selectFormId], (error: BusinessError) => {
447                      if (error) {
448                        console.error(`enableFormsUpdate error, code: ${error.code}, message: ${error.message}`);
449                      }
450                    });
451                  } catch (error) {
452                    console.error(`enableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
453                  }
454                  break;
455                case 5:
456                  try {
457                    formHost.disableFormsUpdate([this.selectFormId], (error: BusinessError) => {
458                      if (error) {
459                        console.error(`disableFormsUpdate error, code: ${error.code}, message: ${error.message}`);
460                      } else {
461                        console.info('disableFormsUpdate success');
462                      }
463                    });
464                  } catch (error) {
465                    console.error(`disableFormsUpdate catch error, code: ${(error as BusinessError).code}, message: ${(error as BusinessError).message}`);
466                  }
467                  break;
468              }
469            })
470        }
471        .margin({
472          top: 20,
473          bottom: 10
474        })
475      }
476    }
477  }
478}
479```
480
481![screenshot](./figures/widget-host-development-guide-2.jpeg)
482
483## 相关实例
484
485针对卡片使用方开发,有以下实例可供参考:
486
487- [卡片使用方(Stage)(API12)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/DocsSample/Form/FormHost)
488