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