Skip to main content

07服务卡片

服务卡片开发


1-创建卡片

  1. 创建卡片当前有两种入口:
  • 创建工程时,选择Application,可以在创建工程后右键新建卡片。
  • 创建工程时,选择Atomic Service(元服务),也可以在创建工程后右键新建卡片。

createCard

在已有的应用工程中,可以通过右键新建ArkTS卡片,具体的操作方式如下。

右键新建卡片。

alt text

说明

API 10及以上 Stage模型的工程中,在Service Widget菜单可直接选择创建动态或静态服务卡片。创建服务卡片后,也可以在卡片的form_config.json配置文件中,通过isDynamic参数修改卡片类型:

  • isDynamic置空或赋值为"true",则该卡片为动态卡片;
  • isDynamic赋值为"false",则该卡片为静态卡片。

2 根据实际业务场景,选择一个卡片模板。

choose

3 在选择卡片的开发语言类型(Language)时,选择ArkTS选项,然后单击“Finish”,即可完成ArkTS卡片创建。

setProject

建议根据实际使用场景命名卡片名称,ArkTS卡片创建完成后,工程中会新增如下卡片相关文件:卡片生命周期管理文件(EntryFormAbility.ets)、卡片页面文件(DemoCardCard.ets)和卡片配置文件(form_config.json)。

CardFiles


2-设置卡片数据

可以简单调整DemoCardCard卡片页面文件UI代码。先暂时展示默认数据:标题和描述。

@Entry
@Component
struct DemoCardCard {
// 卡片数据
readonly title: string = '标题:帝心卡片';
readonly description: string = '描述:学习卡片开发知识';

readonly actionType: string = 'router';
readonly abilityName: string = 'EntryAbility';
readonly message: string = 'add detail';

build() {
Column({ space: 30 }) {
Text(this.title)
.fontSize(14)
Text(this.description)
.fontSize(14)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.onClick(() => {
postCardAction(this, {
action: this.actionType,
abilityName: this.abilityName,
params: {
message: this.message
}
});
})
}
}

展示卡片

长按应用图标/元服务右上角功能键,进行卡片添加。将卡片添加到桌面进行显示。值得注意的是,在添加卡片时,会自动触发卡片生命周期文件 src/main/ets/entryformability/EntryFormAbility.ets中的onAddForm方法。

  • 片在创建时,会触发onAddForm生命周期,此时返回数据可以直接传递给卡片
  • 另外卡片在被卸载时,会触发onRemoveForm生命周期 showCard

设置卡片数据

以上展示的卡片数据是在卡片页面文件中硬编码的静态数据,而卡片如果需要展示动态数据,则需要通过其他能力来获取。例如,EntryFormAbility/EntryAbility/应用或者元服务的页面文件

因为卡片不支持import,所以没法通过引入@ohos.net.http等包来进行数据请求,因此需要通过借助外部能力获取数据后,再传入卡片内部进行展示

动态展示卡片数据的场景:

  1. 添加卡片到桌面时设置初始数据:在EntryFormAbilityonAddForm生命周期函数中通过formBindingData.createFormBindingData(formData)绑定数据到卡片页面中。
  2. 在应用或者元服务页面获取完数据后,主动将数据更新到卡片中

添加卡片时设置卡片数据

EntryFormAbilityonAddForm生命周期函数中通过formBindingData.createFormBindingData(formData)绑定到卡片页面中的数据默认会存储在LocalStorage中。因此卡片页面可以从LocalStorage中使用@LocalStorageProp取出数据。

EntryFormAbility.ets

// 添加卡片生命周期函数
onAddForm(want: Want) {
//绑定的数据要求:键值对类型的对象或者json
let formData:Record<string,string> = {
"title":"初始化数据",
"description":"数据绑定卡片成功,同时存储在localStorage中"
};
return formBindingData.createFormBindingData(formData);
}

DemoCardCard

const  localStorageObj = new LocalStorage()

@Entry(localStorageObj)
@Component
struct DemoCardCard {
// 卡片数据
@LocalStorageProp('title') title: string = '标题:帝心卡片';
@LocalStorageProp('description') description: string = '描述:学习卡片开发知识';

readonly actionType: string = 'router';
readonly abilityName: string = 'EntryAbility';
readonly message: string = 'add detail';

build() {
Column({ space: 30 }) {
Text(this.title)
.fontSize(14)
Text(this.description)
.fontSize(14)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.onClick(() => {
postCardAction(this, {
action: this.actionType,
abilityName: this.abilityName,
params: {
message: this.message
}
});
})
}
}

setCardData

3-卡片内容更新

ArkTS卡片框架为提供方提供了updateForm接口,用来触发卡片的页面更新能力。 提供方主动刷新卡片流程示意: alt update

卡片提供方应用运行过程中,如果识别到有要更新卡片数据的诉求,可以主动通过formProvider提供的updateForm接口更新卡片。

使用异步方式模拟在卡片提供方运行的过程中更新数据。添加卡片的过程中,会先显示初始化数据,定时结束会更新数据。

EntryFormAbility.ets

// 添加卡片生命周期函数
onAddForm(want: Want) {
//绑定的数据要求:键值对类型的对象或者json
let formData: Record<string, string> = {
"title": "初始化数据",
"description": "数据绑定卡片成功,同时存储在localStorage中"
};
// 卡片接收异步数据
setTimeout(() => {
// 准备新的待绑定的数据
formData = {
"title": "异步数据",
"description": "卡片更新数据:异步"
};
// 定义需要传递给卡片的数据对象
let bindData = formBindingData.createFormBindingData(formData)
console.log(`dxin => bindData ${JSON.stringify(bindData)}`)
// 获取当前添加到桌面的卡片id
let wantParams: Record<string, object> | undefined = want.parameters
console.log(`dxin =>wantParams ${JSON.stringify(wantParams)}`)
if (wantParams) {
let formId = wantParams[formInfo.FormParam.IDENTITY_KEY]
console.log(`dxin => formId ${formId}`)
// 更新数据到卡片上
formProvider.updateForm(formId.toString(), bindData)
.then(() => {
console.log(`dxin => 卡片数据更新成功 `)
})
.catch(() => {
console.log(`dxin => 卡片数据更新失败`)
})
} else {
console.log(`dxin => wantParams 获取失败。结果为undefined`)
}
}, 5000)
return formBindingData.createFormBindingData(formData);
}

卡片id被存储在want.parameters对象中。

片ID是字符串类型的数字。

want.parameters返回的wantParams对象格式。

{
"isDynamic": true,
"moduleName": "entry",
"ohos.extra.param.key.form_border_width": 0,
"ohos.extra.param.key.form_customize": [],
"ohos.extra.param.key.form_dimension": 2,
"ohos.extra.param.key.form_height": 513.5,
"ohos.extra.param.key.form_identity": "1767654127",
"ohos.extra.param.key.form_launch_reason": 1,
"ohos.extra.param.key.form_location": 2,
"ohos.extra.param.key.form_name": "demoCard",
"ohos.extra.param.key.form_obscured_mode": false,
"ohos.extra.param.key.form_rendering_mode": 0,
"ohos.extra.param.key.form_temporary": false,
"ohos.extra.param.key.form_width": 513.5,
"ohos.extra.param.key.module_name": "entry",
"ohos.inner.key.font_follow_system": true
}

获取卡片ID的方式也可以进行简写

// 获取当前添加到桌面的卡片id
let formId:string= want.parameters![formInfo.FormParam.IDENTITY_KEY].toString()

4-卡片事件

针对动态卡片,ArkTS卡片中提供了postCardAction接口用于卡片内部和提供方应用间的交互,当前支持routermessagecall三种类型的事件,仅在卡片中可以调用。

动态卡片事件能力说明

alt DynamicCard

动态卡片事件的主要使用场景如下:

  • router事件:可以使用router事件跳转到指定UIAbility,并通过router事件刷新卡片内容。
  • call事件:可以使用call事件拉起指定UIAbility到后台,并通过call事件刷新卡片内容。
  • message事件:可以使用message拉起FormExtensionAbility,并通过FormExtensionAbility刷新卡片内容。

router类型

拉起应用指定页面,可以做为快捷入口进行使用。

alt entryType

  1. 创建应用页面文件方便通过卡片入口拉起,模拟收集管家拉起不同页面

alt routerPage

模拟手机管家的电池管理页面

@Entry
@Component
struct Battery {
build() {
Column() {
Text('电池管理页面').fontSize(30).fontColor('red')
}
.width('100%')
.height('100%')
}
}

模拟手机管家的骚扰拦截页面

@Entry
@Component
struct Intercept {
build() {
Column() {
Text('骚扰拦截页面').fontSize(30).fontColor('green')
}
.width('100%')
.height('100%')
}
}
  1. 在卡片页面准备不同的入口按钮。并在其点击事件中调用postCardAction接口。传递不同的参数。以方便在EntryAbility中获取不同的参数,进行不同的页面跳转

模拟手机管家的卡片页面

Button('电池管理')
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
targetPage: 'Battery'
}
});
})
Button('骚扰拦截')
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
targetPage: 'Intercept'
}
});
})
  1. 卡片页面中postCardAction接口使用router拉起页面时会跳转到提供方应用的指定UIAbility。所以携带的参数会出现在EntryAbility中,通过want对象获取。

want对象数据格式

{
"deviceId": "",
"bundleName": "com.dxin.DxinCard",
"abilityName": "EntryAbility",
"moduleName": "entry",
"uri": "",
"type": "",
"flags": 0,
"action": "",
"parameters": {
"formID": 1996538372,
"isCallBySCB": false,
"isShellCall": false,
"moduleName": "entry",
"ohos.aafwk.param.callerAbilityName": "com.ohos.sceneboard.MainAbility",
"ohos.aafwk.param.callerAppId": "com.ohos.sceneboard_BN5A7NPX8f5Y1CcU5/41YJg0mKSlTaYtl/ZdbgUdgoKFTy03BXMhv4m/1Es7dTBOa9XOBMGogbnP3YVIa06TCqciNzqEX+VfUqxKGPQLDCymV13eg4qZioi0DCA3vrbdLg==",
"ohos.aafwk.param.callerAppIdentifier": "5765880207853062833",
"ohos.aafwk.param.callerBundleName": "com.ohos.sceneboard",
"ohos.aafwk.param.callerPid": 1188,
"ohos.aafwk.param.callerToken": 537544831,
"ohos.aafwk.param.callerUid": 20020012,
"ohos.aafwk.param.displayId": 0,
"ohos.extra.param.key.appCloneIndex": 0,
"ohos.extra.param.key.form_identity": 1996538372,
"ohos.param.callerAppCloneIndex": 0,
"params": "{"targetPage":"Battery"}",
"specifyTokenId": 537206241,
"targetPage": "Battery"
},
"fds": {

},
"entities": [

]
}

需要注意区分两种情况。

  1. 当应用未被拉起时,即初次启动,在onCreate中获取参数。onCreate生命周期函数执行后,默认自动执行onWindowStageCreate生命周期函数。
  2. 当应用已经被拉起过,非初次启动,在onNewWant中获取参数。此时需要手动执行onWindowStageCreate生命周期。

src/main/ets/entryability/EntryAbility.ets

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';


export default class EntryAbility extends UIAbility {
private selectPage: string = '';
private currentWindowStage: window.WindowStage | null = null;

// 初次启动应用
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want?.parameters?.params) {
// want.parameters.params 对应 postCardAction() 中 params 内容
let params: Record<string, Object> = JSON.parse(want.parameters.params as string);
this.selectPage = params.targetPage as string;
}
}

// 再次拉起应用
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want?.parameters?.params) {
// want.parameters.params 对应 postCardAction() 中 params 内容
let params: Record<string, Object> = JSON.parse(want.parameters.params as string);
this.selectPage = params.targetPage as string;
}
if (this.currentWindowStage !== null) {
this.onWindowStageCreate(this.currentWindowStage);
}
}

onWindowStageCreate(windowStage: window.WindowStage): void {

let targetPage: string;
// 根据传递的targetPage不同,选择拉起不同的页面
switch (this.selectPage) {
case 'Battery':
targetPage = 'pages/Battery'; //与实际的UIAbility页面路径保持一致
break;
case 'Intercept':
targetPage = 'pages/Intercept'; //与实际的UIAbility页面路径保持一致
break;
default:
targetPage = 'pages/Index'; //与实际的UIAbility页面路径保持一致
}
if (this.currentWindowStage === null) {
this.currentWindowStage = windowStage;
}
windowStage.loadContent(targetPage, (err) => {
if (err.code) {
return;
}
});
}
}

call类型

alt addNumber

在服务卡片拉起应用的功能,例如音乐卡片界面点击暂停/播放上一首/下一首。例如计分器,点击按钮数字增加等场景。

"action""call" 类型时,

  • 提供方应用需要具备后台运行权限(ohos.permission.KEEP_BACKGROUND_RUNNING)。
  • params需填入参数'method',且类型需为string类型,用于触发UIAbility中对应的方法。
  1. 配置权限

src/main/module.json5

 "requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
  1. 准备卡片页面UI代码

计分器服务卡片页面布局

let localStorage = new LocalStorage()


@Entry(localStorage)
@Component
struct DemoCardCard {
// 当前歌曲id ,
@LocalStorageProp('count') count: number = 1
// 当前卡片id
@LocalStorageProp('formId') formId: string = '666666'

build() {
Column() {
Text('计分器').fontSize(20)
Text(this.count + '').fontSize(30)
Button('+')
.fontSize(14)
.width('75%')
.height(40)
.onClick(() => {
postCardAction(this, {
action: 'call',
abilityName: 'EntryAbility',
params: {
// 需要调用的方法名称
method: 'changeCount',
count: this.count,
formId: this.formId
}
});
})

}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
}
}
  1. EntryFormAbility中初始化服务卡片数据

src/main/ets/entryformability/EntryFormAbility.ets

  // 添加卡片生命周期函数
onAddForm(want: Want) {
// 获取当前添加到桌面的卡片id
let formId: string = want.parameters![formInfo.FormParam.IDENTITY_KEY].toString()
console.log(`dxin =>formId ${formId}`)
// 准备一个临时数据结构 存放formId 和数字
interface FromData {
formId: string,
count: number
}
// 准备初始化数据
let formData: FromData = {
formId: formId,
count: 1
}
// 返回给卡片
return formBindingData.createFormBindingData(formData);
}
  1. EntryAbility中处理卡片页面触发的call事件

EntryAbility.ets

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.callee.on('changeCount', (data) => {
// 准备一个临时数据结构 存放formId 和数字
interface FromData {
formId: string,
count: number
}
// 从data中获取数据
let params = JSON.parse(data.readString()) as FromData
console.log(`dxin =>onCreate params.count ${params.count}`)

const newCount: Record<string, number> = {
// 此处切勿使用++ 那是给 params.count 增加 而非 "count"
"count": params.count+1
}
// 将这个新的数据响应给卡片
const formData = formBindingData.createFormBindingData(newCount)
formProvider.updateForm(params.formId, formData)
console.log(`dxin =>onCreate params.formId ${params.formId}`)
return null
})
} catch (err) {
console.log(`dxin => onCreate call 事件出错了 `)
}
}

message类型

卡片更新自己卡片页面数据,不会拉起应用,一定时间内(10s)如果没有更新的事件,会被销毁,适合做不太耗时的任务

当卡片组件发起message事件时,我们可以通过onFormEvent监听到

alt messageType

  1. 准备卡片页面结构

message案例卡片页面

let localStorage = new LocalStorage()


@Entry(localStorage)
@Component
struct DemoCardCard {
// 当前歌曲id ,
@LocalStorageProp('count') count: number = 1
// 当前卡片id
@LocalStorageProp('formId') formId: string = '666666'

build() {
Column() {
Text('message').fontSize(20)
Text(this.count + '').fontSize(30)
Button('刷新')
.fontSize(14)
.width('75%')
.height(40)
.onClick(() => {
postCardAction(this, {
action: 'message',
abilityName: 'EntryAbility',
params: {

count: this.count,
formId: this.formId
}
});
})

}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
}
}

2.在EntryFormAbility中的onFormEvent中处理message事件

EntryFormAbility

 // 响应卡片页面的 message 事件
onFormEvent(formId: string, message: string) {
// formId 哪个卡片点的。就是哪个卡片的id
//message : {"method":"changeCount","count":5,"formId":"1341186179","action":"message","params":{"method":"changeCount","count":5,"formId":"1341186179"}}

console.log(`dxin =>onFormEvent formId ${formId}`)
// 准备一个临时数据结构
interface MessageParamsType {
method: string
count: number
formId: string
action: string
}

//获取参数
const params = JSON.parse(message) as MessageParamsType
let count = params.count
// 更新数据 准备数据
const newCount: Record<string, number> = {
"count": count + 10
}
const formData = formBindingData.createFormBindingData(newCount)
formProvider.updateForm(formId, formData)
}

定点刷新

alt setTimeUpdate

指定时间点更新数据,例如上图所示,09:37分定时更新数据,由数字1更新为666


  1. 准备卡片页面 alt prepareCardPage

卡片页面布局效果

let localStorage = new LocalStorage()


@Entry(localStorage)
@Component
struct DemoCardCard {
// 当前歌曲id ,
@LocalStorageProp('count') count: number = 1
// 当前卡片id
@LocalStorageProp('formId') formId: string = '666666'

build() {
Column() {
Text('定时刷新').fontSize(20)
Text(this.count + '').fontSize(30)
Text('id: ' + this.formId)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
}
}
  1. 修改配置文件,启动更新能力和设置更新时间。

当同时配置了定时刷新updateDuration和定点刷新scheduledUpdateTime时,定时刷新的优先级更高且定点刷新不会执行。如果想要配置定点刷新,则需要将updateDuration配置为0

即:使用scheduledUpdateTime定点刷新的时候,要设置updateDuration定时刷新的值为0

src/main/resources/base/profile/form_config.json

"updateEnabled": true,
"scheduledUpdateTime": "09:07",
"updateDuration": 0,
  1. EntryFormAbilityonUpdateForm生命周期函数中设置更新数据

EntryFormAbility

  // 定时刷新
onUpdateForm(formId: string) {
// 准备新数据
const newData: Record<string, string | number> = {
"formId": formId,
"count": 666
}
// 更新数据
const formData = formBindingData.createFormBindingData(newData)
formProvider.updateForm(formId, formData)
.then(() => {
console.log(`dxin => 定时更新了数据`)
})
.catch(() => {
console.log(`dxin => 定时更新数据出错了`)
})
}

定时刷新

定时刷新:表示在一定时间间隔内调用onUpdateForm的生命周期回调函数自动刷新卡片内容。可以在form_config.json配置文件的updateDuration字段中进行设置。例如,可以将updateDuration字段的值设置为2,表示刷新时间设置为每小时一次。

卡片定时刷新的更新周期单位为30分钟。应用市场配置的刷新周期范围是1~336,即最短为半小时(1 * 30min)刷新一次,最长为一周(336 * 30min)刷新一次。

  1. 设置定时刷新,值为1代表30分钟刷新一次。

src/main/resources/base/profile/form_config.json

 "updateDuration": 1,
  1. 卡片UI布局和onUpdateForm生命周期同定点刷新