HarmonyOS 鸿蒙Next万能卡片
HarmonyOS 鸿蒙Next万能卡片
概念
卡片是鸿蒙中提出的一个非常重要的服务。
可以让你的应用中数据展示在卡片上面。
卡片可以做到预览效果,服务直达,点击卡片上面的数据,可以直接进入app中指定的页面。
卡片分类
1、静态卡片:适合于进行页面静态布局
2、动态卡片:适合动态展示数据
实现原理
静态卡片
WidgetCard.ets
FormLink({
action: this.ACTION_TYPE,
abilityName: this.ABILITY_NAME,
params: {
message: this.MESSAGE
}
})
EntryFormAbility
只要创建一个卡片,卡片就会产生页面,这个页面要运行那就必须FromExtensionAbility来支撑
卡片在桌面上创建出来,也可以理解就是一个应用展示页面。必须提供ability来支撑运行。
在module.json文件中
{
"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"
}
]
}
其中 "resource": "$profile:form_config"
设定了卡片的配置信息来源于from.config
在profile文件夹下面存在form_config
{
"forms": [
{
"name": "widget",
"displayName": "$string:widget_display_name",
"description": "$string:widget_desc",
"src": "./ets/widget/pages/WidgetCard.ets",
"uiSyntax": "arkts",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"colorMode": "auto",
"isDynamic": false,
"isDefault": true,
"updateEnabled": false,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*4",
"supportDimensions": [
"2*4"
]
}
]
}
动态卡片
DynamicwidgetCard.ets
Row() {
Column() {
Text(this.TITLE)
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.item_title_font'))
}
.width(this.FULL_WIDTH_PERCENT)
}
.height(this.FULL_HEIGHT_PERCENT)
.onClick(() => {
postCardAction(this, {
action: this.ACTION_TYPE,
abilityName: this.ABILITY_NAME,
params: {
message: this.MESSAGE
}
});
})
静态卡片和动态卡片都在form_cinfig文件中进行配置
其中不一样在于跳转方式
静态卡片跳转
FormLink({
action: this.ACTION_TYPE,
abilityName: this.ABILITY_NAME,
params: {
message: this.MESSAGE
}
})
动态卡片跳转
.onClick(() => {
postCardAction(this, {
action: this.ACTION_TYPE,
abilityName: this.ABILITY_NAME,
params: {
message: this.MESSAGE
}
});
})
服务直达
点击卡片任何地方进入主应用
卡片代码:指定触发事件,指定跳转ability名字
params代表传递的参数,这个参数在ability中获取
在entryAbility中
Row() {
Column() {
Text(this.TITLE)
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.item_title_font'))
}
.width(this.FULL_WIDTH_PERCENT)
}
.height(this.FULL_HEIGHT_PERCENT)
.onClick(() => {
// 可以实现拉起ability
postCardAction(this, {
// router事件拉起应用
action: this.ACTION_TYPE,
// ability名字
abilityName: this.ABILITY_NAME,
// 传递的参数
params: {
message: this.MESSAGE
}
});
})
在entryAbility中
export default class EntryAbility extends UIAbility {
/**
* 初始化的时候要执行的生命周期
* @param want
* @param launchParam
*/
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
console.log(`---onCreate---${JSON.stringify(want)}`)
console.log(`---onCreate---${want.parameters?.params}`)
}
/**
* EntryAbility已经运行到后台,唤起这个窗口
* @param want
* @param launchParam
*/
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.log(`---onNewWant---${JSON.stringify(want)}`)
console.log(`---onNewWant---${want.parameters?.params}`)
}
}
onCreate和onNewWant触发时机不一样,如果没有开启窗口,默认进入onCreate否则进入onNewWant
可以在entryAbility中获取到卡片传递的参数。进行数据的动态更新。
在卡片中指定两个按钮:
@Entry
@Component
struct DynamicwidgetCard {
/*
* The title.
*/
readonly title: string = 'Hello World';
/*
* The action type.
*/
readonly actionType: string = 'router';
/*
* The ability name.
*/
readonly abilityName: string = 'EntryAbility';
/*
* The message.
*/
readonly message: string = 'add detail';
/*
* The width percentage setting.
*/
readonly fullWidthPercent: string = '100%';
/*
* The height percentage setting.
*/
readonly fullHeightPercent: string = '100%';
build() {
Row() {
Column() {
Text(this.title)
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('sys.color.font_primary'))
Flex({direction:FlexDirection.Row,justifyContent:FlexAlign.SpaceBetween}){
Button('主页').onClick((event: ClickEvent) => {
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
targetPage:'Index'
}
})
})
Button('数据').onClick((event: ClickEvent) => {
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
targetPage:'DataPage'
}
})
})
}.width('100%')
}
.width(this.fullWidthPercent)
}
.height(this.fullHeightPercent)
}
}
每个按钮执行postCardAction来调用窗口进入entryAbility
传递参数不一样。根据参数可以区分你跳转地址
在EntryAbility生命周期函数中
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { JSON } from '@kit.ArkTS';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
// 跳转路径
private selectPage:string='';
// 指定当前窗口对象
private currentWindowStage:window.WindowStage | null =null
/**
* 初始化的时候要执行的生命周期
* @param want
* @param launchParam
*/
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
if(want.parameters !== undefined){
let params:Record<string,string> = (JSON.parse(JSON.stringify(want.parameters))) as Record<string,string>
this.selectPage=params.targetPage
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
console.log(`---onCreate---${JSON.stringify(want)}`)
console.log(`---onCreate---${want.parameters?.params}`)
}
/**
* EntryAbility已经运行到后台,唤起这个窗口
* @param want
* @param launchParam
*/
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if(want.parameters !== undefined){
let params:Record<string,string> = (JSON.parse(JSON.stringify(want.parameters))) as Record<string,string>
this.selectPage=params.targetPage
}
if(this.currentWindowStage!==null){
this.onWindowStageCreate(this.currentWindowStage)
}
console.log(`---onNewWant---${JSON.stringify(want)}`)
console.log(`---onNewWant---${want.parameters?.params}`)
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
let targetPage:string=''
switch (this.selectPage){
case 'Index':
targetPage='pages/Index'
break
case 'DataPage':
targetPage='fileupload/DataPage'
break
default :
targetPage='pages/Index'
}
if(this.currentWindowStage===null){
this.currentWindowStage=windowStage
}
windowStage.loadContent(targetPage, (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
其中最核心的思路是根据传递过来的参数,我们决定窗口应该加载那个页面。
卡片数据交互
更新卡片的数据
1、通过UIAbility调用formProvide的updateForm来实现卡片数据的更新。
2、通过FormExtensionAbility调用formProvide的UpdateFrom来实现卡片更新
通过router的方式来更新卡片数据,默认采用第一种方案
通过message事件的方式来更新拉片,采用第二种方案
核心源理:卡片上数据要动态变化,目前核心原理就是用到了本地存储方案
通过router事件更新
卡片代码中实现
let storageUpdateRouter=new LocalStorage()
@Entry(storageUpdateRouter)
@Component
struct DynamicwidgetCard {
/*
* The title.
*/
readonly title: string = 'Hello World';
/*
* The action type.
*/
readonly actionType: string = 'router';
/*
* The ability name.
*/
readonly abilityName: string = 'EntryAbility';
/*
* The message.
*/
readonly message: string = 'add detail';
/*
* The width percentage setting.
*/
readonly fullWidthPercent: string = '100%';
/*
* The height percentage setting.
*/
readonly fullHeightPercent: string = '100%';
@LocalStorageProp('str') str:string='xxxxx'
build() {
Row() {
Column() {
Text(this.title)
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('sys.color.font_primary'))
Text(this.str)
.fontSize(20)
.fontColor(Color.Black)
Flex({direction:FlexDirection.Row,justifyContent:FlexAlign.SpaceBetween}){
Button('主页').onClick((event: ClickEvent) => {
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
targetPage:'Index'
}
})
})
Button('数据').onClick((event: ClickEvent) => {
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
targetPage:'DataPage'
}
})
})
}.width('100%')
Button('数据更新').onClick(()=>{
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
str:'1111111'
}
})
})
}
.width(this.fullWidthPercent)
}
.height(this.fullHeightPercent)
}
}
EntryAbility
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { JSON } from '@kit.ArkTS';
import { formBindingData, formInfo, formProvider } from '@kit.FormKit';
import { BusinessError } from '@kit.BasicServicesKit';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
// 跳转路径
private selectPage:string='';
// 指定当前窗口对象
private currentWindowStage:window.WindowStage | null =null
/**
* 更新函数
* @param want
* @param source
*/
handleFormRouterEvent(want: Want, source: string): void {
// 判断是否有parameters,以及是否有更新的key
if (want.parameters && want.parameters[formInfo.FormParam.IDENTITY_KEY] !== undefined) {
// curFormId获取卡片的编号
let curFormId = want.parameters[formInfo.FormParam.IDENTITY_KEY].toString()
// 我们自己传递过来的key
// let message: string = (JSON.parse(want.parameters?.params as string))?.routerDetail
let message:Record<string,string> = (JSON.parse(JSON.stringify(want.parameters))) as Record<string,string>
// 定义要更新的卡片数据
let formData: Record<string, string> = {
'str': message.str + ' ' + source + ' UIAbility' // 和卡片布局中对应
}
// 要修改的本地存储的对象
let formMsg = formBindingData.createFormBindingData(formData)
// 执行更新
formProvider.updateForm(curFormId, formMsg).then((data) => {
console.log('updateForm success !!');
}).catch((error: BusinessError) => {
console.log('updateForm failed!!')
})
}
}
/**
* 初始化的时候要执行的生命周期
* @param want
* @param launchParam
*/
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
if(want.parameters !== undefined){
let params:Record<string,string> = (JSON.parse(JSON.stringify(want.parameters))) as Record<string,string>
this.selectPage=params.targetPage
}
// 调用更新函数
this.handleFormRouterEvent(want,'onCreate')
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
console.log(`---onCreate---${JSON.stringify(want)}`)
console.log(`---onCreate---${want.parameters?.params}`)
}
/**
* EntryAbility已经运行到后台,唤起这个窗口
* @param want
* @param launchParam
*/
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if(want.parameters !== undefined){
let params:Record<string,string> = (JSON.parse(JSON.stringify(want.parameters))) as Record<string,string>
this.selectPage=params.targetPage
}
if(this.currentWindowStage!==null){
this.onWindowStageCreate(this.currentWindowStage)
}
// 调用更新函数
this.handleFormRouterEvent(want,'onNewWant')
console.log(`---onNewWant---${JSON.stringify(want)}`)
console.log(`---onNewWant---${want.parameters?.params}`)
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
let targetPage:string=''
switch (this.selectPage){
case 'Index':
targetPage='pages/Index'
break
case 'DataPage':
targetPage='fileupload/DataPage'
break
default :
targetPage='pages/Index'
}
if(this.currentWindowStage===null){
this.currentWindowStage=windowStage
}
windowStage.loadContent(targetPage, (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
总结:卡片上的数据要更新我们采用router事件来实现。
特点:点击按钮通过router实现服务直达。页面也会更新
通过message事件来更新
通过message的方式来更新数据,特点就是不会触发服务直达。只会在卡片中发送请求去获取数据
卡片代码
let storageUpdateRouter=new LocalStorage()
@Entry(storageUpdateRouter)
@Component
struct DynamicwidgetCard {
/*
* The title.
*/
readonly title: string = 'Hello World';
/*
* The action type.
*/
readonly actionType: string = 'router';
/*
* The ability name.
*/
readonly abilityName: string = 'EntryAbility';
/*
* The message.
*/
readonly message: string = 'add detail';
/*
* The width percentage setting.
*/
readonly fullWidthPercent: string = '100%';
/*
* The height percentage setting.
*/
readonly fullHeightPercent: string = '100%';
@LocalStorageProp('str') str:string='xxxxx'
build() {
Row() {
Column() {
Text(this.title)
.fontSize($r('app.float.font_size'))
.fontWeight(FontWeight.Medium)
.fontColor($r('sys.color.font_primary'))
Text(this.str)
.fontSize(20)
.fontColor(Color.Black)
Flex({direction:FlexDirection.Row,justifyContent:FlexAlign.SpaceBetween}){
Button('主页').onClick((event: ClickEvent) => {
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
targetPage:'Index'
}
})
})
Button('数据').onClick((event: ClickEvent) => {
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
targetPage:'DataPage'
}
})
})
Button('router更新').onClick(()=>{
postCardAction(this,{
action:this.actionType,
abilityName:this.abilityName,
params:{
str:'1111'
}
})
})
Button('message更新').onClick(()=>{
postCardAction(this,{
action:'message',
// abilityName参数不需要了,默认进的是EntryFormAbility
// abilityName:this.abilityName,
// params:{
// str:'2222'
// }
})
})
}.width('100%')
}
.width(this.fullWidthPercent)
}
.height(this.fullHeightPercent)
}
}
EntryFormAbility
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
// Called to return a FormBindingData object.
const 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.
}
//当卡片触发message事件的时候,默认进入这个生命周期
onFormEvent(formId: string, message: string) {
// 定义要存储到本地键值对
class FormDataClass{
str:string='EntryFormAbility-onFormEvent'
}
// 实例化对象,保存到本地的对象
let formData= new FormDataClass()
// 得到更新到本地formInfo
let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData)
// updateForm用于更新卡片的数据
formProvider.updateForm(formId, formInfo).then(()=>{
console.log('数据更新成功')
}).catch((err:BusinessError)=>{
console.log('数据更新失败')
})
}
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;
}
}
卡片刷新
卡片定时刷新:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-ui-widget-update-by-time
卡片定点刷新:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-ui-widget-update-by-time-point
定时刷新:表示在一定时间间隔内调用onUpdateForm的生命周期回调函数自动刷新卡片内容。可以在form_config.json配置文件的updateDuration字段中进行设置。例如,可以将updateDuration字段的值设置为2,表示刷新时间设置为每小时一次。
指定每隔多少事件刷新一次。默认事件间隔按照30分钟一次。我们需要配置的属性
"updateEnabled": true,
"updateDuration": 2,
下次刷新
formProvider.setFormNextRefreshTime(formId, FIVE_MINUTE, (err: BusinessError) => {})
setFormNextRefreshTime:这个方法可以设置接口触发后多少时间,更新内容
FIVE_MINUTE:5分钟的意思
定点刷新
指定每天早上10.30分刷新一次卡片,当同时配置了定时刷新和定点刷新,定时刷新的优先级更高。如果想要配置定点刷新,则需要将updateDuration配置为0
"scheduledUpdateTime": "10:30",
更多关于HarmonyOS 鸿蒙Next万能卡片的实战教程也可以访问 https://www.itying.com/category-93-b0.html
HarmonyOS鸿蒙Next的万能卡片是一种动态交互组件,用户可以通过卡片快速访问应用的核心功能,无需打开完整应用。卡片支持多种尺寸和样式,能够根据用户需求进行自定义。开发者可以通过ArkUI框架创建和管理万能卡片,利用鸿蒙的分布式能力实现跨设备协同。万能卡片的数据更新和交互逻辑由后台服务驱动,确保信息的实时性和一致性。
更多关于HarmonyOS 鸿蒙Next万能卡片的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
HarmonyOS Next的万能卡片功能确实是一个强大的特性,我来总结几个关键点:
- 卡片分类:
- 静态卡片适合固定内容展示
- 动态卡片适合需要频繁更新的数据
- 实现原理:
- 通过FormExtensionAbility支撑卡片运行
- 在module.json中配置form_config文件定义卡片属性
- 服务直达:
- 通过postCardAction实现点击跳转
- 可以在EntryAbility中接收参数并处理
- 数据交互:
- 两种更新方式:
- router方式:会触发服务直达
- message方式:仅更新卡片数据
- 核心是通过formProvider.updateForm()方法更新
- 刷新机制:
- 定时刷新:配置updateDuration
- 定点刷新:配置scheduledUpdateTime
- 下次刷新:使用setFormNextRefreshTime
卡片开发的关键在于合理配置form_config.json文件,并处理好与主应用的数据交互。动态卡片特别适合需要实时展示数据的场景,而静态卡片则更适合展示固定内容。
对于开发者来说,需要特别注意:
- 卡片生命周期管理
- 数据更新机制的选择
- 刷新策略的配置
这些功能组合起来,可以创建出既美观又实用的万能卡片体验。