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

2 回复

HarmonyOS鸿蒙Next的万能卡片是一种动态交互组件,用户可以通过卡片快速访问应用的核心功能,无需打开完整应用。卡片支持多种尺寸和样式,能够根据用户需求进行自定义。开发者可以通过ArkUI框架创建和管理万能卡片,利用鸿蒙的分布式能力实现跨设备协同。万能卡片的数据更新和交互逻辑由后台服务驱动,确保信息的实时性和一致性。

更多关于HarmonyOS 鸿蒙Next万能卡片的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


HarmonyOS Next的万能卡片功能确实是一个强大的特性,我来总结几个关键点:

  1. 卡片分类:
  • 静态卡片适合固定内容展示
  • 动态卡片适合需要频繁更新的数据
  1. 实现原理:
  • 通过FormExtensionAbility支撑卡片运行
  • 在module.json中配置form_config文件定义卡片属性
  1. 服务直达:
  • 通过postCardAction实现点击跳转
  • 可以在EntryAbility中接收参数并处理
  1. 数据交互:
  • 两种更新方式:
    • router方式:会触发服务直达
    • message方式:仅更新卡片数据
  • 核心是通过formProvider.updateForm()方法更新
  1. 刷新机制:
  • 定时刷新:配置updateDuration
  • 定点刷新:配置scheduledUpdateTime
  • 下次刷新:使用setFormNextRefreshTime

卡片开发的关键在于合理配置form_config.json文件,并处理好与主应用的数据交互。动态卡片特别适合需要实时展示数据的场景,而静态卡片则更适合展示固定内容。

对于开发者来说,需要特别注意:

  • 卡片生命周期管理
  • 数据更新机制的选择
  • 刷新策略的配置

这些功能组合起来,可以创建出既美观又实用的万能卡片体验。

回到顶部