【HarmonyOS NEXT】服务卡片学习

489 阅读7分钟

服务卡片可以作为应用和元服务的快捷入口进行使用,个人很喜欢这种便捷操作方式。遂学习记录,形成笔记,以备后用,以慧他人。

1-创建卡片

创建卡片当前有两种入口:

  • 创建工程时,选择Application,可以在创建工程后右键新建卡片。
  • 创建工程时,选择Atomic Service(元服务),也可以在创建工程后右键新建卡片。

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

  1. 右键新建卡片。

说明

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

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

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

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


2-设置卡片数据

简单调整卡片页面文件UI代码。

先暂时展示默认数据:标题和描述

// DemoCardCard
@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生命周期

设置卡片数据

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

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

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

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

添加卡片时设置卡片数据

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

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


3-卡片内容更新

ArkTS卡片框架为提供方提供了updateForm接口,用来触发卡片的页面更新能力。

提供方主动刷新卡片流程示意:

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

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

// 添加卡片生命周期函数
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是字符串类型的数字。

{
        "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
let formId:string= want.parameters![formInfo.FormParam.IDENTITY_KEY].toString()

4-卡片接收推送数据

5-卡片事件

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

动态卡片事件能力说明

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

  • <font style="color:rgb(36, 39, 40);">router事件</font>:可以使用<font style="color:rgb(36, 39, 40);">router事件</font>跳转到指定<font style="color:rgb(36, 39, 40);">UIAbility</font>,并通过<font style="color:rgb(36, 39, 40);">router</font>事件刷新卡片内容。
  • <font style="color:rgb(36, 39, 40);">call事件</font>:可以使用<font style="color:rgb(36, 39, 40);">call事件</font>拉起指定<font style="color:rgb(36, 39, 40);">UIAbility</font>到后台,并通过<font style="color:rgb(36, 39, 40);">call事件</font>刷新卡片内容。
  • <font style="color:rgb(36, 39, 40);">message事件</font>:可以使用<font style="color:rgb(36, 39, 40);">message</font>拉起<font style="color:rgb(36, 39, 40);">FormExtensionAbility</font>,并通过<font style="color:rgb(36, 39, 40);">FormExtensionAbility</font>刷新卡片内容。

router类型

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

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

@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对象获取。
{
  "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生命周期。
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类型

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

"action"为"call" 类型时,

  • 提供方应用需要具备后台运行权限(ohos.permission.KEEP_BACKGROUND_RUNNING)。
  • params需填入参数'method',且类型需为string类型,用于触发UIAbility中对应的方法。
  1. 配置权限
 "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中初始化服务卡片数据
  // 添加卡片生命周期函数
  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事件
  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监听到

  1. 准备卡片页面结构
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)
    }
  }
  1. EntryFormAbility中的onFormEvent中处理message事件
 // 响应卡片页面的 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)
  }

定点刷新

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

  1. 准备卡片页面

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

"updateEnabled": true,
"scheduledUpdateTime": "09:07",
"updateDuration": 0,
  1. EntryFormAbilityonUpdateForm生命周期函数中设置更新数据
  // 定时刷新
  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分钟刷新一次。
 "updateDuration": 1,
  1. 卡片UI布局onUpdateForm生命周期同定点刷新