开天辟地 HarmonyOS(鸿蒙) - 卡片: 动态卡片

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

开天辟地 HarmonyOS(鸿蒙) - 卡片: 动态卡片

示例如下:

pages\widget\DynamicWidgetDemo.ets

/*
 * 动态卡片
 * 长按 app 的图标,则可以添加卡片
 * 如需在应用内添加卡片,请参见 AddFormMenuItemDemo.ets 中的相关说明
 *
 * 静态卡片仅支持 UI 组件布局
 * 动态卡片除了支持 UI 组件布局外,还支持组件的通用事件方法以及自定义动画效果,可以用于需要交互的场景
 *
 *
 * 1、需要在 src/main/module.json5 中添加卡片类型的 extension ability
 * 2、卡片的相关配置,在 src/main/resources/profile/form_config.json
 * 3、卡片对应的 extension ability 的代码详见 /entryformability/EntryFormAbility.ets
 * 4、卡片的具体实现详见 /widget/pages/DynamicWidgetCard.ets
 */

import { TitleBar } from '../TitleBar';

@Entry
@Component
struct DynamicWidgetDemo {

  @State message: string = "动态卡片,具体实现请参见:\n" +
    "src/main/module.json5\n" +
    "src/main/resources/profile/form_config.json\n" +
    "/entryformability/EntryFormAbility.ets\n" +
    "/widget/pages/DynamicWidgetCard.ets"

  build() {
    Column({space:10}) {
      TitleBar()
      Text(this.message)
    }
  }
}

\entry\src\main\module.json5

{
  "module": {
    "name": "entry", // 当前 module 的名称
    "type": "entry", // 当前 module 的类型
    "srcEntry": "./ets/MyAbilityStage.ets", // 当前 module 的对应的 AbilityStage 的代码的地址
    "appStartup": "$profile:startup_config", // 启动任务的配置文件
    "description": "$string:module_desc", // 当前 module 的描述
    "mainElement": "com.webabcd.harmonydemo.EntryAbility", // 当前 module 的入口 ability 的名称(ability 必须是 exported 为 true 的)
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages", // 用于描述页面的信息(参见 /entry/src/main/resources/base/profile/main_pages.json)
    "querySchemes": [ // 当前应用可以通过 canOpenLink() 判断当前设备中是否存在支持指定协议(这个协议必须在 querySchemes 中配置,最多 50 个)的应用
      "webabcd"
    ],
    "abilities": [
      {
        "name": "com.webabcd.harmonydemo.EntryAbility", // 当前 ability 的名称(自定义标识)
        "srcEntry": "./ets/entryability/EntryAbility.ets", // 当前 ability 的代码的地址
        "description": "$string:EntryAbility_desc", // 描述
        "icon": "$media:layered_image", // app 的图标(需要配置 entity.system.home, action.system.home),如果不指定此字段的话则 app 的图标会使用 AppScope/app.json5 中的 icon
        "label": "$string:EntryAbility_label", // app 的标题(需要配置 entity.system.home, action.system.home),如果不指定此字段的话则 app 的标题会使用 AppScope/app.json5 中的 label
        "startWindowIcon": "$media:startIcon", // 启动屏上显示的图标
        "startWindowBackground": "$color:start_window_background", // 启动屏的背景
        "exported": true, // 用于标识当前 ability 是否可以被其他应用调用
        "orientation": "portrait", // 屏幕方向
        "preferMultiWindowOrientation": "landscape_auto", // 悬浮窗方向
        "skills": [ { "entities": [ "entity.system.home" ], "actions": [ "action.system.home" ] } ], // 这个设置说明当前 ability 是入口 ability(主:有了这个配置则 module 的 mainElement 标签将失效)
        "backgroundModes": [ // 长时任务的类型
          "dataTransfer", // 数据上传下载
          // "audioPlayback", // 音频、视频播放
          // "audioRecording", // 录音、录屏
          // "location", // 定位
          // "bluetoothInteraction", // 蓝牙传输
        ],
        "removeMissionAfterTerminate": true // 当调用 terminateSelf() 杀死当前 UIAbility 时,是否需要将其从在最近任务列表中删除(默认值为 false)
      },
      {
        "name": "com.webabcd.harmonydemo.EntryAbility2",
        "srcEntry": "./ets/entryability/EntryAbility2.ets",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background"
      },
      {
        "name": "com.webabcd.harmonydemo.EntryAbility_singleton",
        "srcEntry": "./ets/entryability/EntryAbility_singleton.ets",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "launchType": "singleton" // 指定当前 ability 的启动方式为 singleton 方式(详见 /basic/LaunchTypeDemo.ets 中的相关说明)
      },
      {
        "name": "com.webabcd.harmonydemo.EntryAbility_multiton",
        "srcEntry": "./ets/entryability/EntryAbility_multiton.ets",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "launchType": "multiton" // 指定当前 ability 的启动方式为 multiton 方式(详见 /basic/LaunchTypeDemo.ets 中的相关说明)
      },
      {
        "name": "com.webabcd.harmonydemo.EntryAbility_specified",
        "srcEntry": "./ets/entryability/EntryAbility_specified.ets",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "launchType": "specified" // 指定当前 ability 的启动方式为 specified 方式(详见 /basic/LaunchTypeDemo.ets 中的相关说明)
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      },
      {
        "name": "com.webabcd.harmonydemo.MyWorkSchedulerExtensionAbility", // 自定义标识
        "srcEntry": "./ets/pages/background/MyWorkSchedulerExtensionAbility.ets", // 延迟任务对应的代码的地址
        "type": "workScheduler" // 当前的 ExtensionAbility 的类型为延迟任务
      },
      {
        "name": "com.webabcd.harmonydemo.EntryFormAbility", // 自定义标识
        "srcEntry": "./ets/entryformability/EntryFormAbility.ets", // 卡片对应的 extension ability 的代码地址
        "label": "$string:EntryAbility_label",
        "description": "$string:EntryAbility_desc",
        "type": "form", // 当前的 ExtensionAbility 的类型为卡片
        "metadata": [
          {
            "name": "ohos.extension.form", // 卡片的 metadata 的 name 必须是 ohos.extension.form
            "resource": "$profile:form_config" // 卡片的相关配置,详见 src/main/resources/profile/form_config.json 中的配置
          }
        ]
      }
    ],
    "routerMap": "$profile:route_map", // 指定路由表,详见 src/main/resources/profile/route_map.json 中的配置
    "requestPermissions":[
      {
        "name": "ohos.permission.INTERNET", // 请求 Internet 网络的权限
        "reason": "$string:hello_webabcd", // 申请此权限的原因
        "usedScene": {
          "abilities": [ ], // 需要使用此权限的 ability 的名称,配置为空则所有 ability 均可以使用此权限
          "when":"always" // inuse(使用时允许使用此权限),always(始终允许使用此权限)
        }
      },
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING", // 请求长时任务的权限
        "reason": "$string:hello_webabcd", // 申请此权限的原因
        "usedScene": {
          "abilities": [ ], // 需要使用此权限的 ability 的名称,配置为空则所有 ability 均可以使用此权限
          "when": "always" // inuse(使用时允许使用此权限),always(始终允许使用此权限)
        }
      },
      {
        "name": "ohos.permission.PUBLISH_AGENT_REMINDER", // 请求提醒任务的权限
        "reason": "$string:hello_webabcd", // 申请此权限的原因
        "usedScene": {
          "abilities": [ ], // 需要使用此权限的 ability 的名称,配置为空则所有 ability 均可以使用此权限
          "when": "always" // inuse(使用时允许使用此权限),always(始终允许使用此权限)
        }
      }
    ]
  }
}

\entry\src\main\resources\base\profile\form_config.json

{
  "forms": [
    {
      "name": "static widget",
      "displayName": "$string:staticWidget_display_name",
      "description": "$string:staticWidget_desc",
      "src": "./ets/widget/pages/StaticWidgetCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDynamic": false,
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "09:30",
      "updateDuration": 1,
      "supportDimensions": [
        "1*2",
        "2*2",
        "2*4",
        "4*4",
        "6*4"
      ],
      "defaultDimension": "2*2"
    },
    {
      "name": "dynamic widget",
      "displayName": "$string:dynamicWidget_display_name",
      "description": "$string:dynamicWidget_desc",
      "src": "./ets/widget/pages/DynamicWidgetCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDynamic": true,
      "isDefault": false,
      "updateEnabled": true,
      "scheduledUpdateTime": "09:30",
      "updateDuration": 1,
      "supportDimensions": [
        "4*4"
      ],
      "defaultDimension": "4*4"
    },
    {
      "name": "FormLink",
      "displayName": "$string:formLink_display_name",
      "description": "$string:formLink_desc",
      "src": "./ets/widget/pages/FormLinkCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDynamic": false,
      "isDefault": false,
      "updateEnabled": true,
      "scheduledUpdateTime": "09:30",
      "updateDuration": 1,
      "supportDimensions": [
        "4*4"
      ],
      "defaultDimension": "4*4"
    }
  ]
}

\entry\src\main\ets\entryformability\EntryFormAbility.ets

/*
 * 卡片对应的 extension ability
 * 用于管理卡片的生命周期,以及和卡片做数据交互
 */

import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { Helper } from '../utils/Helper';
import { MyLog } from '../utils/MyLog';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';

export default class EntryFormAbility extends FormExtensionAbility {

  // 将指定的资源文件复制到指定的沙箱目录
  copyFile(src: Resource, dst: string) {
    let resourceManager = this.context.getApplicationContext().resourceManager
    let buffer = resourceManager.getMediaContentSync(src.id).buffer
    let outputStream = fs.createStreamSync(dst, 'w+');
    let writeLength = outputStream.writeSync(buffer, {
      offset: 0,
      length: buffer.byteLength
    })
    outputStream.closeSync();
  }

  // 卡片创建时的回调
  onAddForm(want: Want) {

    // 通过 want.parameters 可以获取卡片的规格,以及宽和高等
    MyLog.d(`onAddForm ${JSON.stringify(want.parameters)}`)

    // FormExtensionContext - FormExtensionAbility 的上下文(在 FormExtensionAbility 内,可以通过 this.context 获取 FormExtensionContext 对象)
    let context = this.context
    let abilityName = context.extensionAbilityInfo.name
    MyLog.d(`extension ability name: ${abilityName}`)

    // 将指定的资源文件复制到指定的沙箱目录
    // 如果需要卡片显示一个网络图片,则可以先将图片下载到沙箱目录(注:卡片每次活过来后,最多在后台存在 5 秒),然后再参照本例后续的方法
    let imagePath = this.context.getApplicationContext().filesDir + '/icon.png'
    this.copyFile($r('app.media.app_icon'), imagePath)

    // 创建一个保存多张图片的字典表,用于在卡片中显示图片
    // key 代表图片的标识
    // value 代表图片的文件描述符,打开文件到内存后,把文件描述符传给卡片,然后卡片再根据文件描述符显示内存中的图片
    let formImages: Record<string, number> = {};
    let file = fs.openSync(imagePath);
    formImages['myImage_0'] = file.fd; // 注意:如果图片更新了,则这里需要指定一个和之前不同的 key 以便卡片可以显示更新后的图片

    /*
     * formBindingData.createFormBindingData() - 创建一个 FormBindingData 对象
     * formBindingData.FormBindingData - 需要传递给卡片的数据
     *   此对象中的字段的值,可以在卡片中通过 @LocalStorageProp 引用
     */
    let formData: Record<string, string | Record<string, number>> = {
      'formId': `${want.parameters!['ohos.extra.param.key.form_identity']}`, // 卡片 id
      'dimension': `${want.parameters!['ohos.extra.param.key.form_dimension']}`, // 卡片的规格(1代表1*2, 2代表2*2, 3代表2*4, 4代表4*4, 7代表6*4)
      'width': `${want.parameters!['ohos.extra.param.key.form_width']}`, // 卡片的宽
      'height': `${want.parameters!['ohos.extra.param.key.form_height']}`, // 卡片的高
      'content': Helper.getTimestampString(), // 卡片中可以通过 @LocalStorageProp('content') 引用此值
      'myImage': 'myImage_0', // 指定在 formImages 中的指定 key 的图片,然后在卡片中可以通过 @LocalStorageProp('myImage') 引用此图片
      'formImages': formImages // 保存多张图片的字典表
    };
    return formBindingData.createFormBindingData(formData);
  }

  // 刷新时会执行 onUpdateForm() 回调
  // 当时间符合 scheduledUpdateTime 或 updateDuration 的条件时会触发刷新
  // 当系统语言或深色浅色模式发生变化时也会触发刷新
  // 卡片每次活过来后,最多在后台存在 5 秒
  onUpdateForm(formId: string) {

    MyLog.d(`onUpdateForm: ${formId}`)

    /*
     * formBindingData.createFormBindingData() - 创建一个 FormBindingData 对象
     * formBindingData.FormBindingData - 需要传递给卡片的数据
     *   此对象中的字段的值,可以在卡片中通过 @LocalStorageProp 引用
     * formProvider.updateForm() - 更新指定的卡片
     *   formId - 卡片 id
     *   formBindingData - 为卡片绑定的 FormBindingData 对象
     * formProvider.setFormNextRefreshTime() - 指定下一次刷新的时间(注:卡片每天最多刷新 50 次)
     *   formId - 卡片 id
     *   minute - 此时间后刷新(单位:分钟),最短为 5 分钟
     */
    let formData: Record<string, string> = {
      'content': Helper.getTimestampString() // 卡片中可以通过 @LocalStorageProp('content') 引用此值
    };
    let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData)
    formProvider.updateForm(formId, formInfo)

    try {
      // 设置过 5 分钟后刷新卡片
      formProvider.setFormNextRefreshTime(formId, 5, (err: BusinessError) => {

      })
    } catch (err) {

    }
  }

  // 卡片通过 message 的方式传递数据时,会触发此回调
  onFormEvent(formId: string, message: string) {
    MyLog.d(`onFormEvent: ${formId}, ${message}`) // 此处的 message 参数就是 message 方式传递过来的数据

    // 更新指定的卡片
    let formData: Record<string, string> = {
      'content': Helper.getTimestampString() // 卡片中可以通过 @LocalStorageProp('content') 引用此值
    };
    let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData)
    formProvider.updateForm(formId, formInfo)
  }

  // 卡片销毁时的回调
  onRemoveForm(formId: string) {
    MyLog.d(`onRemoveForm: ${formId}`)
  }
};

\entry\src\main\ets\widget\pages\DynamicWidgetCard.ets

/*
 * 卡片的具体实现(本例演示的是动态卡片)
 *
 * 静态卡片仅支持 UI 组件布局
 * 动态卡片除了支持 UI 组件布局外,还支持组件的通用事件方法以及自定义动画效果,可以用于需要交互的场景
 *
 * 注:
 * 请先参见静态卡片 StaticWidgetCard.ets 中的相关说明,动态卡片在静态卡片的基础上增加了对组件的通用事件方法以及自定义动画效果的支持
 * 动态卡片通过 postCardAction 实现类似 FormLink 的功能,可以参考 FormLinkCard.ets 中的相关说明
 */
import { Helper } from '../../utils/Helper'

@Entry
@Component
struct DynamicWidgetCard {

  @LocalStorageProp('content') content: string = ''

  @State rotateAngle: number = 0

  build() {
    Column() {
      // 动态卡片在静态卡片的基础上增加了对组件的通用事件方法以及自定义动画效果的支持
      Button(`click me ${this.content}`)
        .onClick(() => {
          this.rotateAngle = this.rotateAngle === 0 ? 360 : 0

          /*
           * postCardAction() - 在动态卡片中实现类似静态卡片中的 FormLink 的功能,从而为动态卡片提供与应用交互的能力
           *   action - 交互方式,可以参考 FormLinkCard.ets 中的相关说明
           *   abilityName - 需要拉起的 ability 的名称(只能拉起当前应用下的 ability)
           *   uri - deep linking 或 app linking 地址
           *   params - 需要传递的数据
           *
           * 注:本例不详演示 postCardAction() 了,请参考 FormLinkCard.ets 中的相关说明
           */
          postCardAction(this, {
            action: 'message',
            // abilityName: 'com.webabcd.harmonydemo.EntryAbility',
            // uri: "webabcd://a.b.c/api?p1=xyz",
            params: {
              'k1': 'v1',
              'k2': `${Helper.getTimestampString()}`,
            }
          });
        })
        .rotate({ angle: this.rotateAngle })
        .animation({ playMode: PlayMode.Normal} )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

posted @ 2025-04-15 09:53  webabcd  阅读(119)  评论(0)    收藏  举报