目次
1. 简介
服务卡片架构
亮点/特征
2. ArkTS卡片开发指导
创建一个ArkTS卡片
配置卡片的配置文件
卡片生命周期管理
3. 功能开发
3.1 刷新图片
方案一:官方文档思路
方案2 ,仅供参考(如有不对,请多指正。)
3.2 卡片动态更新内容
实现原理
实现步调
3.3 点击卡片唤起特定页
基础实现原理
实现步调
总结
1. 简介
Form Kit(卡片开发服务)提供一种界面展示情势,可以将应用的重要信息或操纵前置到服务卡片(以下简称“卡片”),以到达服务直达、镌汰跳转层级的体验结果。卡片常用于嵌入到其他应用(当前被嵌入方即卡片使用方只支持系统应用,例如桌面)中作为其界面显示的一部门,并支持拉起页面、发送消息等基础的交互能力。
服务卡片架构
图1 服务卡片架构
卡片的根本概念:
- 卡片使用方:如上图中的桌面,显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
- 应用图标:应用入口图标,点击后可拉起应用历程,图标内容不支持交互
- 卡片:具备不同规格巨细的界面展示,卡片的内容可以举行交互,如实现按钮举行界面的刷新、应用的跳转等。
- 卡片提供方:包含卡片的应用,提供卡片的显示内容、控件结构以及控件点击处理逻辑。
- FormExtensionAbility:卡片业务逻辑模块,提供卡片创建、烧毁、刷新等生命周期回调。
- 卡片页面:卡片UI模块,包含页面控件、结构、变乱等显示和交互信息。
卡片的常见使用步调如下:
图2 卡片常见使用步调
- 长按“桌面图标”,弹出操纵菜单。
- 点击“服务卡片”选项,进入卡片预览界面。
- 点击“添加到桌面”按钮,即可在桌面上看到新添加的卡片。
亮点/特征
- 服务直达:将元服务/应用的重要信息以卡片情势展示在桌面,用户可以通过快捷手势使用卡片,通过轻量交互行为实现服务直达、镌汰层级跳转的目的。
- 永久在线:提供定时、署理等多种卡片刷新机制,实现卡片永久在线。
- 受限管控:卡片支持的组件、变乱、动效、数据管理、状态管理和API能力均举行了肯定限定,保障性能、功耗及安全可靠。
2. ArkTS卡片开发指导
创建一个ArkTS卡片
在已有的应用工程中,可以通过右键新建ArkTS卡片,具体的操纵方式如下。
1,右键新建卡片。
说明:
在API 10 Stage模型的工程中,在Service Widget菜单可直接选择创建动态或静态服务卡片。创建服务卡片后,也可以在卡片的form_config.json配置文件中,通过isDynamic参数修改卡片类型:isDynamic置空或赋值为"true",则该卡片为动态卡片;isDynamic赋值为"false",则该卡片为静态卡片。
2,根据实际业务场景,选择一个卡片模板。
3,在选择卡片的开发语言类型(Language)时,选择ArkTS选项,然后单击“Finish”,即可完成ArkTS卡片创建。
4. ArkTS卡片创建完成后,工程中会新增如下卡片相干文件:卡片生命周期管理文件(EntryFormAbility.ets)、卡片页面文件(WidgetCard.ets)和卡片配置文件(form_config.json)。
配置卡片的配置文件
本人开发时编辑器(DevEco Studio)会主动配置,无需手工配置!!!
假如有,请忽略这一步;没有的话,请按要求配置。
卡片相干的配置文件主要包含FormExtensionAbility的配置和卡片的配置两部门。
- 卡片需要在module.json5配置文件中的extensionAbilities标签下,配置FormExtensionAbility相干信息。FormExtensionAbility需要填写metadata元信息标签,其中键名称为固定字符串“ohos.extension.form”,资源为卡片的具体配置信息的索引。
配置示例如下:
- {
"module": {
...
"extensionAbilities": [
{
"name": "EntryFormAbility",
"srcEntry": "./ets/entryformability/EntryFormAbility.ets",
"label": "$string:EntryFormAbility_label",
"description": "$string:EntryFormAbility_desc",
"type": "form",
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}
]
}
]
}
}
- 卡片的具体配置信息。在上述FormExtensionAbility的元信息(“metadata”配置项)中,可以指定卡片具体配置信息的资源索引。配置示例如下:
{
"forms": [
{
"name": "widget",
"description": "This is a service widget.",
"src": "./ets/widget/pages/WidgetCard.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": [
"2*2"
],
"formConfigAbility": "ability://com.example.entry.EntryAbility",
"dataProxyEnabled": false,
"isDynamic": true,
"transparencyEnabled": false,
"metadata": []
}
]
}
卡片生命周期管理
在EntryFormAbility.ets中,实现FormExtensionAbility生命周期接口,其中在onAddForm的入参want中可以通过FormParam取出卡片的相干信息。
import { formBindingData, FormExtensionAbility, formInfo } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
export default class EntryFormAbility extends FormExtensionAbility {
// 使用方创建卡片时触发,提供方需要返回卡片数据绑定类
onAddForm(want: Want) {
// Called to return a FormBindingData object.
let 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.
}
// 若卡片支持定时更新/定点更新/卡片使用方主动哀求更新功能,则提供方需要重写该方法以支持数据更新
onUpdateForm(formId: string) {
// Called to notify the form provider to update a specified form.
}
// 需要配置formVisibleNotify为true,且为系统应用才会回调
onChangeFormVisibility(newStatus: Record<string, number>) {
// Called when the form provider receives form events from the system.
}
// 若卡片支持触发变乱,则需要重写该方法并实现对变乱的触发
onFormEvent(formId: string, message: string) {
// Called when a specified message event defined by the form provider is triggered.
}
// 当对应的卡片删除时触发的回调,入参是被删除的卡片ID
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;
}
//主动生成没有这个,有需求需要自己配
onConfigurationUpdate(config: Configuration) {
// 当前formExtensionAbility存活时更新系统配置信息时触发的回调。
// 需留意:formExtensionAbility创建后5秒内无操纵将会被清理。
console.info('[EntryFormAbility] onConfigurationUpdate:' + JSON.stringify(config));
}
};
这是编译器主动生成的文件,我们只需要明白每个生命周期钩子是何时触发的,知道在哪个钩子里完成我们的业务。
3. 功能开发
应用服务卡片功能很多,像卡片定时刷新和定点刷新,根据卡片状态刷新不同的内容,刷新本地和网络图片,卡片署理刷新等 ,另有router,call,message变乱。
本文篇幅有限,就主要介绍我使用过的加载网络图片,刷新图片和点击卡片唤起特定页。
3.1 刷新图片
1, 下载网络图片需要使用到网络能力,需要在main目次下的module.json5文件中配置申请ohos.permission.INTERNET权限。
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
]
官方文档
2,在EntryFormAbility中的onAddForm生命周期回调中实现本地文件的刷新。
const TAG: string = 'WgtImgUpdateEntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class WgtImgUpdateEntryFormAbility extends FormExtensionAbility {
// 在添加卡片时,打开一个本舆图片并将图片内容通报给卡片页面显示
onAddForm(want: Want): formBindingData.FormBindingData {
// 假设在当前卡片应用的tmp目次下有一个本舆图片:head.PNG
let tempDir = this.context.getApplicationContext().tempDir;
// 打开本舆图片并获取其打开后的fd
let file: fileFs.File;
let imgBear: Record<string, number>;
try {
file = fs.openSync(tempDir + '/' + 'head.PNG');
imgBear = {
'imgBear': file.fd
};
} catch (e) {
hilog.error(DOMAIN_NUMBER, TAG, `openSync failed: ${JSON.stringify(e as Base.BusinessError)}`);
}
class FormDataClass {
text: string = 'Image: Bear';
imgName: string = 'imgBear';
loaded: boolean = true;
formImages: Record<string, number> = imgBear;
}
let formData = new FormDataClass();
// 将fd封装在formData中并返回至卡片页面
return formBindingData.createFormBindingData(formData);
}
...
}
3,在EntryFormAbility中的onFormEvent生命周期回调中实现网络文件的刷新。
const TAG: string = 'WgtImgUpdateEntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;
export default class WgtImgUpdateEntryFormAbility extends FormExtensionAbility {
onFormEvent(formId: string, message: string): void {
let param: Record<string, string> = {
'text': '刷新中...'
};
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
formProvider.updateForm(formId, formInfo);
// 留意:FormExtensionAbility在触发生命周期回调时被拉起,仅能在背景存在5秒
// 建议下载能快速下载完成的小文件,如在5秒内未下载完成,则此次网络图片无法刷新至卡片页面上
let netFile = 'https://cn-assets.gitee.com/assets/mini_app-e5eee5a21c552b69ae6bf2cf87406b59.jpg'; // 需要在此处使用真实的网络图片下载链接
let tempDir = this.context.getApplicationContext().tempDir;
let fileName = 'file' + Date.now();
let tmpFile = tempDir + '/' + fileName;
let httpRequest = http.createHttp()
httpRequest.request(netFile, (err, data) => {
if (!err && data.responseCode == http.ResponseCode.OK) {
let imgFile = fs.openSync(tmpFile, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.write(imgFile.fd, data.result as ArrayBuffer).then((writeLen: number) => {
hilog.info(DOMAIN_NUMBER, TAG, "write data to file succeed and size is:" + writeLen);
}).catch((err: Base.BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, "write data to file failed with error message: " + err.message + ", error code: " + err.code);
}).finally(() => {
fs.closeSync(imgFile);
});
hilog.info(DOMAIN_NUMBER, TAG, 'ArkTSCard download complete: %{public}s', tmpFile);
let file: fileFs.File;
let fileInfo: Record<string, string | number> = {};
try {
file = fs.openSync(tmpFile);
fileInfo[fileName] = file.fd;
} catch (e) {
hilog.error(DOMAIN_NUMBER, TAG, `openSync failed: ${JSON.stringify(e as Base.BusinessError)}`);
}
class FormDataClass {
text: string = 'Image: Bear' + fileName;
imgName: string = fileName;
loaded: boolean = true;
formImages: object = fileInfo;
}
let formData = new FormDataClass();
let formInfo = formBindingData.createFormBindingData(formData);
formProvider.updateForm(formId, formInfo).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'FormAbility updateForm success.');
}).catch((error: Base.BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `FormAbility updateForm failed: ${JSON.stringify(error)}`);
});
} else {
hilog.error(DOMAIN_NUMBER, TAG, `ArkTSCard download task failed. Cause: ${JSON.stringify(err)}`);
let param: Record<string, string> = {
'text': '刷新失败'
};
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
formProvider.updateForm(formId, formInfo);
}
httpRequest.destroy();
})
}
...
}
4,在卡片页面通过backgroundImage属性展示EntryFormAbility通报过来的卡片内容。
let storageWidgetImageUpdate = new LocalStorage();
@Entry(storageWidgetImageUpdate)
@Component
struct WidgetImageUpdateCard {
@LocalStorageProp('text') text: ResourceStr = $r('app.string.loading');
@LocalStorageProp('loaded') loaded: boolean = false;
@LocalStorageProp('imgName') imgName: ResourceStr = $r('app.string.imgName');
build() {
Column() {
Column() {
Text(this.text)
.fontColor('#FFFFFF')
.opacity(0.9)
.fontSize(12)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.margin({ top: '8%', left: '10%' })
}.width('100%').height('50%')
.alignItems(HorizontalAlign.Start)
Row() {
Button() {
Text($r('app.string.update'))
.fontColor('#45A6F4')
.fontSize(12)
}
.width(120)
.height(32)
.margin({ top: '30%', bottom: '10%' })
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
postCardAction(this, {
action: 'message',
params: {
info: 'refreshImage'
}
});
})
}.width('100%').height('40%')
.justifyContent(FlexAlign.Center)
}.width('100%').height('100%')
.backgroundImage('memory://' + this.imgName)
.backgroundImageSize(ImageSize.Cover)
}
}
我的写法---仅供参考(如有不对,请多指正)
1,编写图片加载类
import type fileFs from '@ohos.file.fs'
import fs from '@ohos.file.fs'
// 作用:下载需要的图片资源 然后把图片资源处理为下发给card组件使用的标准格式
class FormData {
fileNameList: string[]
formImages: Record<string, string | number>
constructor(fileNameList: string[], formImages: Record<string, string | number>) {
this.fileNameList = fileNameList
this.formImages = formImages
}
}
class LoadImageForFormData {
// 要下载的图片列表
private imageUrls: string[]
// 图片下载完毕执行函数
private finishCb: (formInfo: formBindingData.FormBindingData) => void
// 当前Ability
private ability: FormExtensionAbility
// 当前正在下载的图片下标
private curIndex: number = 0
// 本地地址
private tempDir: string = ''
// 内存中的图片对象
private formImages: Record<string, string | number> = {}
// 图片文件的名称列表
private fileNameList: string[] = []
// 初始FormData数据
initialFormData = new FormData([], {})
constructor(imageUrls: string[], finishCb: (formInfo: formBindingData.FormBindingData) => void,
ability: FormExtensionAbility) {
this.imageUrls = imageUrls
this.finishCb = finishCb
this.ability = ability
this.tempDir = ability.context.getApplicationContext().tempDir
}
// 动态添加要下载的图片
addImage (imageUrls: string[]){
this.imageUrls = imageUrls
return this
}
// 开始下载图片
startLoad () {
if (this.imageUrls.length === 0) {
console.error('please provide download imglist')
return
}
let netFile = this.imageUrls[this.curIndex] // 需要在此处使用真实的网络图片下载链接
let fileName = 'file' + Date.now()
let tmpFile = this.tempDir + '/' + fileName
request.downloadFile(
this.ability.context,
{
url: netFile,
filePath: tmpFile,
enableMetered: true,
enableRoaming: true
}
).then((task) => {
task.on('complete', () => {
let file: fileFs.File
try {
// fs资源读取模块
file = fs.openSync(tmpFile)
this.formImages[fileName] = file.fd
} catch (e) {
console.error(`openSync failed: ${JSON.stringify(e)}`)
}
this.fileNameList.push(fileName)
this.curIndex++
if (this.curIndex < this.imageUrls.length) {
// 假如还没下载完毕,继续下载
this.startLoad()
} else {
// 全部下载完毕更新数据
this.initialFormData.fileNameList = this.fileNameList
this.initialFormData.formImages = this.formImages
let formInfo = formBindingData.createFormBindingData(this.initialFormData)
this.finishCb(formInfo)
}
})
}).catch(() => {
})
}
}
2.,FormExtensionAbility中下发数据
// 要下载的图片
const goodsList = [
'https://dongfangshuxing.oss-cn-beijing.aliyuncs.com/2023/03/14/yuxiangrousi.jpg',
'https://dongfangshuxing.oss-cn-beijing.aliyuncs.com/2023/03/14/yuxiangrousi.jpg',
]
export default class EntryFormAbility extends FormExtensionAbility {
// 手动补充返回值类型
onAddForm(want: Want): formBindingData.FormBindingData {
const ImgData= new LoadImageForFormData(
goodsList,
(formInfo: formBindingData.FormBindingData) => {
// 找到要更新的卡片id
const formId = want.parameters && want.parameters['ohos.extra.param.key.form_identity'].toString()
// 根据卡片id和信息更新卡片内容
formProvider.updateForm(formId, formInfo)
},
this
)
ImgData.startLoad()
// 必须要return
return formBindingData.createFormBindingData(ImgData.initialFormData)
}
}
goodsList放的是我的网络图片,各位可以根据自己业务的需求安放自己的图片数据。
3,卡片组件消费数据
let storageWidgetImageUpdate = new LocalStorage()
@Entry(storageWidgetImageUpdate)
@Component
struct WidgetCard {
@LocalStorageProp('fileNameList') fileNameList: string[] = []
build() {
Column() {
Row() {
ForEach(
this.fileNameList,
(url: string) => {
Row() {
Image('memory://'+url)
.borderRadius(12)
.width(50)
.aspectRatio(1)
}
.backgroundColor('#eee')
.borderRadius(12)
}
)
}
.justifyContent(FlexAlign.SpaceBetween)
.width('100%')
.layoutWeight(1)
.padding({
left: 20,
right: 20
})
.backgroundColor('#fff')
.borderRadius({
topRight: 16,
topLeft: 16
})
.onClick(() => {
})
Row() {
Text('Hello World')
.fontColor('#fff')
.fontSize(16)
}
.height(40)
.width('100%')
.justifyContent(FlexAlign.Center)
}
.linearGradient({
angle: '135',
colors: [
[Color.White, '0%'],
[Color.Blue, '100%']
]
})
.height('100%')
}
}
具体结果如图:
由于篇幅有限,后面的卡片更新和跳转指定页面我就不官方展示官方方案了,有兴趣的请自行去官方文档查看。
文档地址:卡片变乱能力说明 (openharmony.cn)
3.2 卡片动态更新内容
在点击刷新按钮之后,期望可以更新卡片的商品内容,显示最新的推荐内容。
实现原理
在卡片组件中触发message变乱,在FormExtensionAblility的onForEvent钩子中监听变乱,然后执行卡片的updateForm生命周期方法传入要更新的数据即可
- 卡片触发一个变乱 message
- FormAbility中监听变乱触发,监听到之后依旧按照标准的数据格式,把新的图片在此举行下载,继续处理成标准格式,继续调用卡片更新的固定方法传入新的图片内容
实现步调
1,更新UI结构
Row() {
Text('Hello World')
.fontColor('#fff')
.fontSize(16)
Button('OK')
.onClick(()=>{
postCardAction(this,
{
action: 'message',
abilityName:'EntryAbility'
}
)
})
}
.height(40)
.width('100%')
.justifyContent(FlexAlign.Center)
2,FormExtensionAbility中监听变乱并更新卡片
const newGoodsList = [
'https://dongfangshuxing.oss-cn-beijing.aliyuncs.com/2023/03/10/18988304-a76a-46c4-a39f-26ea31e7a179.jpg',
'https://dongfangshuxing.oss-cn-beijing.aliyuncs.com/2023/03/10/18988304-a76a-46c4-a39f-26ea31e7a179.jpg',
]
onFormEvent(formId: string, message: string) {
const lF = new LoadImageForFormData(
newGoodsList,
(formInfo: formBindingData.FormBindingData) => {
// 根据卡片id和信息更新卡片内容
formProvider.updateForm(formId, formInfo)
},
this
)
lF.startLoad()
}
实现结果图如下:
3.3 点击卡片唤起特定页
当我们点击卡片主体内容的时候,期望可以唤起特定的落地页
基础实现原理
- 卡片组件点击之后通过postCardAction触发router变乱并携带参数
- 在应用的UIAbility中接收router变乱,解析参数完成跳转
实现步调
1. 准备落地页(新建一个页面Page)
@Entry
@Component
struct RouterPage {
@State message: string = 'Hello HarmonyOS';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
点击之后通过方法通报参数
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { targetPage: 'RouterPage' }
})
})
3. 在UIAbility中接收router变乱并获取参数,根据通报的params不同,选择拉起不同的页面
1-未启动应用的环境
export default class EntryAbility extends UIAbility {
// 存放拉起页地址
private selectPage: string = ''
// UIAbility假如没有运行,会执行onCreate
async onCreate(want, launchParam) {
if (want.parameters !== undefined) {
let params: Record<string, string> = JSON.parse(JSON.stringify(want.parameters))
this.selectPage = params.targetPage
}
}
onWindowStageCreate(windowStage: window.WindowStage) {
let targetPage: string = this.selectPage ? `pages/${this.selectPage}` : 'pages/Index'
windowStage.loadContent(targetPage, (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
})
}
}
2- 已经启动的环境
留意:已经启动时只执行 onNewWant钩子函数
第一次启动时存下来 windowStage实例,二次启动时直接在onNewWant钩子中通过windowState实例
手动调用onWindowStageCreate方法 执行判定逻辑
export default class EntryAbility extends UIAbility {
private selectPage: string = ''
private currentWindowStage: window.WindowStage | null = null
// UIAbility假如已经运行 会执行onNewWant
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info("卡片 onNewWant:" + JSON.stringify(want))
if (want.parameters?.params !== undefined) {
let params: Record<string, string> = JSON.parse(JSON.stringify(want.parameters))
this.selectPage = params.targetPage
}
if (this.currentWindowStage !== null) {
this.onWindowStageCreate(this.currentWindowStage)
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
let targetPage: string = this.selectPage ? `pages/${this.selectPage}` : 'pages/Index'
if (this.currentWindowStage === null) {
this.currentWindowStage = windowStage
}
windowStage.loadContent(targetPage, (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
}
结果如下(视频上传不了,我就只能放图片了,歉仄)
------》(点击OK)
------》(点击图片)
总结
开发者可以使用声明式范式开发ArkTS卡片页面。如下卡片页面由DevEco Studio模板主动生成,开发者可以根据自身的业务场景举行调整。
ArkTS卡片具备JS卡片的全量能力,而且新增了动效能力和自定义绘制的能力,支持声明式范式的部门组件、变乱、动效、数据管理、状态管理能力,
在功能开发中,主要显现的是我的写法,仅供参考。假如想要按照自己的思路完成功能,请参考官方文档:Form Kit简介 (openharmony.cn)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |