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

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

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

示例如下:

pages\widget\StaticWidgetDemo.ets

/*
 * 静态卡片
 * 长按 app 的图标,则可以添加卡片
 * 如需在应用内添加卡片,请参见 AddFormMenuItemDemo.ets 中的相关说明
 *
 * 静态卡片仅支持 UI 组件布局
 * 动态卡片除了支持 UI 组件布局外,还支持组件的通用事件方法以及自定义动画效果,可以用于需要交互的场景
 *
 *
 * 1、需要在 src/main/module.json5 中添加卡片类型的 extension ability,类似如下
 * {
 *   "module": {
 *     "extensionAbilities": [
 *       {
 *         "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 中的配置
 *           }
 *         ]
 *       }
 *     ],
 *   }
 * }
 *
 * 2、卡片的相关配置,在 src/main/resources/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", // 通过 ArkTS 实现卡片
 *     "window": {
 *       "designWidth": 720, // 设计的基准宽度,与 lpx 相关,缺省值为 720
 *       "autoDesignWidth": true // 是否根据屏幕的像素密度自动计算 designWidth 的值,此值配置为 true 时则会忽略 designWidth 配置的值,缺省值为 false
 *     },
 *     "colorMode": "auto", // 卡片的深色浅色模式(auto, dark, light)
 *     "isDynamic": false, // 是否是动态卡片
 *     "isDefault": true, // 是否是默认卡片,每一个卡片类型的 extension ability 只能有一个默认卡片
 *     "updateEnabled": true, // 是否允许刷新
 *     "scheduledUpdateTime": "10:30", // 定点刷新(仅 updateDuration 配置为 0 时 scheduledUpdateTime 才会生效)
 *     "updateDuration": 1, // 周期刷新(单位为 30 分钟,比如配置为 2 则为每 60 分钟刷新一次,配置为 0 则禁用 updateDuration 刷新)
 *     "supportDimensions": [ // 卡片支持的外观规格(1*2, 2*2, 2*4, 4*4, 6*4)
 *       "2*2",
 *       "4*4"
 *     ],
 *     "defaultDimension": "2*2" // 卡片默认的外观规格
 *   }
 * ]
 *
 * 3、卡片对应的 extension ability 的代码详见 /entryformability/EntryFormAbility.ets 中的说明
 *
 * 4、卡片的具体实现详见 /widget/pages/StaticWidgetCard.ets 中的说明
 *
 * 注:以上是开发时手动创建卡片的方式,如果要通过 DevEco Studio 自动创建卡片,则先选中指定的 hap 模块,然后在 File -> New -> Service Widget 中创建即可
 */

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

@Entry
@Component
struct StaticWidgetDemo {

  @State message: string = "静态卡片,具体实现请参见:\n" +
    "src/main/module.json5\n" +
    "src/main/resources/profile/form_config.json\n" +
    "/entryformability/EntryFormAbility.ets\n" +
    "/widget/pages/StaticWidgetCard.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\StaticWidgetCard.ets

/*
 * 卡片的具体实现(本例演示的是静态卡片)
 *
 * 静态卡片仅支持 UI 组件布局
 * 动态卡片除了支持 UI 组件布局外,还支持组件的通用事件方法以及自定义动画效果,可以用于需要交互的场景
 *
 * 注:静态卡片不支持组件的通用事件方法以及自定义动画效果
 */

@Entry
@Component
struct StaticWidgetCard {

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

  @LocalStorageProp('formId') cardId: string = ''
  @LocalStorageProp('width') cardWidth: string = ''
  @LocalStorageProp('height') cardHeight: string = ''
  @LocalStorageProp('dimension') cardDimension: string = ''

  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true))

  build() {
    // 关于 FormLink 的详细说明请参见 FormLinkCard.ets
    FormLink({
      action: 'router', // 点击后跳转到指定的 ability
      abilityName: 'com.webabcd.harmonydemo.EntryAbility', // 需要打开的 ability 的名称
    }) {
      Row() {
        Column() {
          // 可以直接显示本地资源中的图片
          Image($r('app.media.son')).width(20).height(20)

          // 如果是沙箱文件,则需要使用此方式显示
          //   memory:// 代表显示的是内存中的图片
          //   this.myImage 代表显示的图片的文件描述符(需要在 FormExtensionAbility 打开图片,然后把文件描述符传过来,请参见 /entryformability/EntryFormAbility.ets 中的说明)
          // 注:如果需要显示一个网络图片,则先把图片下载后保存到沙箱中,然后再显示即可
          Image('memory://' + this.myImage).width(20).height(20)

          // 在卡片上更新一个文本(需要在 FormExtensionAbility 把更新后的文本传过来,请参见 /entryformability/EntryFormAbility.ets 中的说明)
          Text(this.content)

          // 卡片中也支持 Canvas 组件
          Canvas(this.context).width(20).height(20).onReady(() => {
            this.context.fillStyle = '#ff0000';
            this.context.fillRect(0, 0, 20, 20);
          })

          // 卡片 id
          Text(`formId: ${this.cardId}`)
          // 卡片的规格(1代表1*2, 2代表2*2, 3代表2*4, 4代表4*4, 7代表6*4)
          Text(`cardDimension: ${this.cardDimension}`)
          // 卡片的宽
          Text(`cardWidth: ${this.cardWidth}`)
          // 卡片的高
          Text(`cardHeight: ${this.cardHeight}`)
        }
        .width('100%')
      }
      .height('100%')
    }
  }
}

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

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