引言
在现代开发中,服务卡片是不可或缺的一部分,比如音乐,天气类等应用,官网的介绍中写道:卡片让您便捷地预览服务信息,例如查看天气或日历日程等内容。您可将卡片添加到屏幕上,让这类信息触手可及。您还可按喜好选取不同样式和排列方式,打造个性化桌面。
大体思路
- 创建服务卡片
- 应用与服务卡片的交互
- 刷新卡片内容
- 应用端主动更新卡片信息
创建服务卡片
首先,我们需要打开Edit Configurations,选中Deploy Multi Hap,将DEploy Muliti Hap Packages 的框打上勾
然后应用就可以愉快的进行服务卡片的开发了:
创建步骤:选中Entry,new,Service Widget
在弹出框中选选择想要创建的服务卡片类型,本文中选中默认的文本卡片
创建完成后除了serviceCard页面还会生成一个EntryFormAbility文件,和EntryAbility不同的是,EntryFormAbility继承了FormExtensionAbility,而EntryAbility继承了UIAbility。可以理解他们是2个进程。
创建完服务卡片,先运行起来后效果:
我们可以观察到,创建的服务卡片,在点击卡片后有如下代码
postCardAction(this, {
action: this.ACTION_TYPE,
abilityName: this.ABILITY_NAME,
params: {
message: this.MESSAGE
}
});
postCardAction是给卡片添加意图,this代表当前卡片,action是类型,abilityName是打开哪个Ability,params是需要给应用进程传递的数据(服务卡片在另外一个进程),点击完服务卡片后发现,跳转到了应用首页,那能否跳转到指定页面呢?
应用与服务卡片的交互
在卡片上添加2 个按钮,去掉原来的整个卡片的onClick,添加2个按钮
Button('首页').onClick(()=>{
postCardAction(this,{
'action':'router',
'abilityName':'EntryAbility',
'params':{
targetPage:"Index"
},
})
})
Button('我的').onClick(()=>{
postCardAction(this,{
'action':'router',
'abilityName':'EntryAbility',
'params':{
targetPage:"MinePage"
},
})
})
}
在点击按钮的时候把我们需要跳转的page名称传给应用,那如何接受这targetPage呢?
我们在EntryAbility里看到很多空方法,其实就是Ability的生命周期,有过原生安卓开发经验的,可以将它理解为application的生命周期回调函数。
//要访问的页面
let selectPage =''
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
//启动程序的生命周期
if(want.parameters.params!==undefined){
let params = JSON.parse(want.parameters.params);
console.log('onCreate router targetPage:'+params.targetPage);
selectPage = params.targetPage;
}
}
onWindowStageCreate(windowStage: window.WindowStage) {
//默认进入首页
let target = selectPage || 'Index';
target = 'pages/'+target;
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
//存储窗口实例
windowStage.loadContent(target, (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) ?? '');
});
}
}
我们可以设置一个参数selectPage,通过onCreate方法里的want.parameters.params获取到我们上面点击服务卡片的时候传递的params对象。通过JSON解析后拿到targetPage,在onWindowStageCreate中组合拼装一下路由地址,currentWindow.loadContent(tatgetPage)执行后,有值就根据参数拼接rute地址跳转,否则就默认首页。
运行后当kill掉应用的进程,能够正常跳转到首页和我的页面,但是有个问题,如果只是把应用推到后台,再点击我的,发现无效无法跳转,这是什么原因呢?有些眼尖的同学可能就发现了,我们接受和打开的方法都放在了onCreate里。只有创建的时候才会接收到,如果Ability已经创建了,就无法接收到卡片的通知回调。
那么改如何做呢?答案是通过onNewWant函数,这个函数的意思是,如果UIAbility已在后台运行,在收到Router事件后会触发,改写后的代码:
//要访问的页面
let selectPage =''
//当前的windown对象
let currentWindow = null
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
//启动程序的生命周期
if(want.parameters.params!==undefined){
let params = JSON.parse(want.parameters.params);
console.log('onCreate router targetPage:'+params.targetPage);
selectPage = params.targetPage;
}
}
//如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期的回调
onNewWant(want, launchParam){
if(want.parameters.params!==undefined){
let params = JSON.parse(want.parameters.params);
console.log('onCreate router targetPage:'+params.targetPage);
selectPage = params.targetPage;
}
//进入对应页面
let target = selectPage || 'AddressPage';
target = 'pages/'+target;
currentWindow.loadContent(target, (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) ?? '');
});
}
onWindowStageCreate(windowStage: window.WindowStage) {
//默认进入首页
let target = selectPage || 'AddressPage';
target = 'pages/'+target;
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
//存储窗口实例
currentWindow = windowStage
currentWindow.loadContent(target, (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) ?? '');
});
}
}
创建了一个变量currentWindow用以存储onWindowStageCreate中的windown对象,然后在onNewWant中获取相应的信息,再执行跳转。
刷新卡片内容
到目前为止卡片上的信息都是静态展示的,那我们如何更新卡片上的信息呢?
首先需要在EntryFormAbility的onAddForm里创建一对要更新的数据,通过formBindingData.createFormBindingData创建一个FormBindingData的实力返回回去
onAddForm(want: Want) {
let formData = {
title : 'jay'
}
return formBindingData.createFormBindingData(formData);
}
此时卡片作为接收方需要通过LocalStorage获取传递的数据,具体做法如下:
let storage = new LocalStorage();
@Entry(storage)
@Component
struct MessageCardCard {
@LocalStorageProp('title') title:string='zhoujielun'
build() {
Row() {
Column({space:20}) {
Text('服务卡片:'+this.title)
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.item_title_font'))
}
.width('100%')
}
.height('100% ')
}
}
创建一个LocalStorage实例,然后在Entry中传入storage,在通过@LocalStorageProp('title') title:string='zhoujielun'读取数据,需要注意:1必须要有默认值,2LocalStorageProp包裹的字符串名需要和onAddForm函数中的formData的key保持一致,才能正确接受到数据的更新。
运行后发现,卡片上的数据已经变成了jay
卡片方主动更新服务卡片信息
依然是调用postCardAction 方法,action改为message
Button('更新数据') .onClick(()=>{
postCardAction(this,{
action:'message',
params:{}
})
})
这时候就需要用到EntryFormAbility的onFormEvent函数了
//对应卡片的message事件
onFormEvent(formId: string, message: string) {
let fromData={'title':'黑色毛衣'};
//构建传入的数据对象
let fromInfo = formBindingData.createFormBindingData(fromData)
//指定卡片id,传入最新的数据
formProvider.updateForm(formId,fromInfo)
}
此时运行后点击卡片上的更新数据的按钮,数据已经更新过来了
应用方更新服务卡片信息
因为应用方无法知道服务卡片的formId,那么我就要在服务卡片创建的时候把fromId存起来,然后统一发送事件更新信息。
// @ts-nocheck
import formInfo from '@ohos.app.form.formInfo';
import formBindingData from '@ohos.app.form.formBindingData';
import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';
import Want from '@ohos.app.ability.Want';
import formProvider from '@ohos.app.form.formProvider';
import dataPreferences from '@ohos.data.preferences';
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
//在want中取出卡片的唯一标识formId
let formId:string = want.parameters[formInfo.FormParam.IDENTITY_KEY];
//把formId保存到首选项中
;(async ()=>{
let pre = await dataPreferences.getPreferences(this.context,'formIds');
// @ts-ignore
let formIds:string = await pre.get('formIds','[]');
let formIdsArray = JSON.parse(formIds);
formIdsArray.push(formId);
await pre.put('formIds',JSON.stringify(formIdsArray));
await pre.flush();
})()
let formData = {
title : 'jay'
}
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.
}
onChangeFormVisibility(newStatus: Record<string, number>) {
// Called when the form provider receives form events from the system.
}
//对应卡片的message事件
onFormEvent(formId: string, message: string) {
// Called when a specified message event defined by the form provider is triggered.
let fromData={'title':'黑色毛衣'};
//构建传入的数据对象
let fromInfo = formBindingData.createFormBindingData(fromData)
//指定卡片id,传入最新的数据
formProvider.updateForm(formId,fromInfo)
}
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;
}
};
在Page页面根据取出首选项中的formId,循环调用updateForm更新数据:
import router from '@ohos.router'
import dataPreferences from '@ohos.data.preferences';
import formBindingData from '@ohos.app.form.formBindingData';
import formProvider from '@ohos.app.form.formProvider';
@Entry
@Component
struct AddressPage {
store: any = {}
@State city: string = ''
t:number = 1
async aboutToAppear() {
setInterval(async ()=>{
let formData={time: ++this.t};
//读取首选项中的id集合
let pre = await dataPreferences.getPreferences(getContext(this),'formIds');
// @ts-ignore
let formIds:string = await pre.get('formIds','[]');
let formMsg = formBindingData.createFormBindingData(formData)
//遍历集合,将新数据传到当前应用的所有卡片
JSON.parse(formIds).forEach((currentFormId)=>{
formProvider.updateForm(currentFormId,formMsg)
});
},1000)
}
build() {
Row() {
Column() {
Text('首页')
Button('更新卡片').onClick(async ()=>{
let formData={title:'听妈妈的话'};
//读取首选项中的id集合
let pre = await dataPreferences.getPreferences(getContext(this),'formIds');
// @ts-ignore
let formIds:string = await pre.get('formIds','[]');
let formMsg = formBindingData.createFormBindingData(formData)
//遍历集合,将新数据传到当前应用的所有卡片
JSON.parse(formIds).forEach((currentFormId)=>{
formProvider.updateForm(currentFormId,formMsg)
});
})
}
.width('100%')
}
.height('100%')
}
}
上面的例子中,我直接通过定时间,每隔一秒更新卡片上的数字,模拟充电或者歌曲播放进度效果,运行后:
总结
服务卡片的创建交互,以及刷新卡片信息的内容到此结束,相信应该能满足大部分业务需求,差别无非就是在UI上。
今天是HDC2024华为开发者大会,谨借此篇博客提前庆祝纯血鸿蒙的到来,华为加油,HarmonyOS 加油💪🏻💪🏻💪🏻