当前位置: 首页 > article >正文

HarmonyOs DevEco Studio小技巧31--卡片的生命周期与卡片的开发

Form Kit简介

Form Kit(卡片开发服务)提供一种界面展示形式,可以将应用的重要信息或操作前置到服务卡片(以下简称“卡片”),以达到服务直达、减少跳转层级的体验效果。卡片常用于嵌入到其他应用(当前被嵌入方即卡片使用方只支持系统应用,例如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互能力。

卡片的基本概念:

  • 卡片使用方:如上图中的桌面,显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
    • 应用图标:应用入口图标,点击后可拉起应用进程,图标内容不支持交互。
    • 卡片:具备不同规格大小的界面展示,卡片的内容可以进行交互,如实现按钮进行界面的刷新、应用的跳转等。
  • 卡片提供方:包含卡片的应用,提供卡片的显示内容、控件布局以及控件点击处理逻辑。
    • FormExtensionAbility:卡片业务逻辑模块,提供卡片创建、销毁、刷新等生命周期回调。
    • 卡片页面:卡片UI模块,包含页面控件、布局、事件等显示和交互信息。

亮点/特征

  • 服务直达:将元服务/应用的重要信息以卡片形式展示在桌面,用户可以通过快捷手势使用卡片,通过轻量交互行为实现服务直达、减少层级跳转的目的。
  • 永久在线:提供定时、代理等多种卡片刷新机制,实现卡片永久在线。
  • 受限管控:卡片支持的组件、事件、动效、数据管理、状态管理和API能力均进行了一定限制,保障性能、功耗及安全可靠。

服务卡片开发指导(Stage模型开发基于ArkTS UI的卡片)

之前我们接触过有Stage模型 还有 FA模型 但是FA模型暂时基本上用不到,所以只讲一下Stage模型

ArkTS卡片运行机制

实现原理

图1 ArkTS卡片实现原理

  • 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置,当前仅系统应用可以作为卡片使用方。

  • 卡片提供方:提供卡片显示内容的应用,控制卡片的显示内容、控件布局以及控件点击事件。

  • 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,提供formProvider的接口能力,同时提供卡片对象的管理与使用以及卡片周期性刷新等能力。

  • 卡片渲染服务:用于管理卡片渲染实例,渲染实例与卡片使用方上的卡片组件一一绑定。卡片渲染服务运行卡片页面代码widgets.abc进行渲染,并将渲染后的数据发送至卡片使用方对应的卡片组件。

图2 ArkTS卡片渲染服务运行原理

与动态卡片相比,静态卡片整体的运行框架和渲染流程是一致的,主要区别在于,卡片渲染服务将卡片内容渲染完毕后,卡片使用方会使用最后一帧渲染的数据作为静态图片显示,其次卡片渲染服务中的卡片实例会释放该卡片的所有运行资源以节省内存。因此频繁的刷新会导致静态卡片运行时资源不断的创建和销毁,增加卡片功耗。

与JS卡片相比,ArkTS卡片支持在卡片中运行逻辑代码,为确保ArkTS卡片发生问题后不影响卡片使用方应用的使用,ArkTS卡片新增了卡片渲染服务用于运行卡片页面代码widgets.abc,卡片渲染服务由卡片管理服务管理。卡片使用方的每个卡片组件都对应了卡片渲染服务里的一个渲染实例,同一应用提供方的渲染实例运行在同一个ArkTS虚拟机运行环境中,不同应用提供方的渲染实例运行在不同的ArkTS虚拟机运行环境中,通过ArkTS虚拟机运行环境隔离不同应用提供方卡片之间的资源与状态。开发过程中需要注意的是globalThis对象的使用,相同应用提供方的卡片globalThis对象是同一个,不同应用提供方的卡片globalThis对象是不同的。

 ArkTS卡片的优势

卡片作为应用的一个快捷入口,ArkTS卡片相较于JS卡片具备如下几点优势:

  • 统一开发范式,提升开发体验和开发效率。

    提供ArkTS卡片能力后,统一了卡片和页面的开发范式,页面的布局可以直接复用到卡片布局中,提升开发体验和开发效率。

    图3 卡片工程结构对比

  • 增强了卡片的能力,使卡片更加万能。

    • 新增了动效的能力:ArkTS卡片开放了属性动画和显式动画的能力,使卡片的交互更加友好。
    • 新增了自定义绘制的能力:ArkTS卡片开放了Canvas画布组件的能力,卡片可以使用自定义绘制的能力构建更多样的显示和交互效果。
    • 允许卡片中运行逻辑代码:开放逻辑代码运行后很多业务逻辑可以在卡片内部自闭环,拓宽了卡片的业务适用场景。

ArkTS卡片的约束

ArkTS卡片相较于JS卡片具备了更加丰富的能力,但也增加了使用卡片进行恶意行为的风险。由于ArkTS卡片显示在使用方应用中,使用方应用一般为桌面应用,为确保桌面的使用体验以及功耗相关考虑,对ArkTS卡片的能力做了以下约束:

  • 当导入模块时,仅支持导入标识“支持在ArkTS卡片中使用”的模块。

  • 不支持导入共享包。

  • 不支持使用native语言开发。

  • 仅支持声明式范式的部分组件、事件、动效、数据管理、状态管理和API能力。

  • 卡片的事件处理和使用方的事件处理是独立的,建议在使用方支持左右滑动的场景下卡片内容不要使用左右滑动功能的组件,以防手势冲突影响交互体验。

除此之外,当前ArkTS卡片还存在如下约束:

  • 暂不支持极速预览。

  • 暂不支持断点调试能力。

  • 暂不支持Hot Reload热重载。

  • 暂不支持setTimeOut。

ArkTS卡片相关模块

图1 ArkTS卡片相关模块

  • FormExtensionAbility:卡片扩展模块,提供卡片创建、销毁、刷新等生命周期回调。

  • FormExtensionContext:FormExtensionAbility的上下文环境,提供FormExtensionAbility具有的接口和能力。

  • formProvider:提供卡片提供方相关的接口能力,可通过该模块提供接口实现更新卡片、设置卡片更新时间、获取卡片信息、请求发布卡片等。

  • formInfo:提供了卡片信息和状态等相关类型和枚举。

  • formBindingData:提供卡片数据绑定的能力,包括FormBindingData对象的创建、相关信息的描述。

  • 页面布局(WidgetCard.ets):提供声明式范式的UI接口能力。

    • ArkTS卡片特有能力:postCardAction用于卡片内部和提供方应用间的交互,仅在卡片中可以调用。
    • ArkTS卡片能力列表:列举了能在ArkTS卡片中使用的API、组件、事件、属性和生命周期调度。
  • 卡片配置:包含FormExtensionAbility的配置和卡片的配置

    • 在module.json5配置文件中的extensionAbilities标签下,配置FormExtensionAbility相关信息。
    • 在resources/base/profile/目录下的form_config.json配置文件中,配置卡片(WidgetCard.ets)相关信息。

 ArkTS卡片开发指导(看这个)

创建一个ArkTS卡片

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

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

1. 右键新建卡片。

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

2. 有四种模板,按照自己的需求来(不过一般都自己写样式)

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

 注意:经过测试,发现你这里全选的话,在创建的时候,他会根据你

这里的配置创建卡片,但是会在你选择卡片之后,其余的卡片全部销毁。

这里建议按实际需求来选择卡片,以免造成不必要的消费

 ArkTS卡片创建完成后,工程中会新增如下卡片相关文件:

卡片生命周期管理文件(EntryFormAbility.ets)、卡片页面文件(WidgetCard.ets)卡片配置文件(form_config.json)

配置卡片的配置文件

卡片相关的配置文件主要包含FormExtensionAbility的配置和卡片的配置两部分。

1.卡片需要在module.json5配置文件中的extensionAbilities标签下,配置FormExtensionAbility相关信息。FormExtensionAbility需要填写metadata元信息标签,其中键名称为固定字符串“ohos.extension.form”,资源为卡片的具体配置信息的索引。

配置示例如下:

{
  "module": {
    //....省略
    "extensionAbilities": [
     //..省略
      {  
        "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"
          }
        ]
      }
    ]
  }
}

2. 卡片的具体配置信息。在上述FormExtensionAbility的元信息(“metadata”配置项)中,可以指定卡片具体配置信息的资源索引。例如当resource指定为$profile:form_config时,会使用开发视图的resources/base/profile/目录下的form_config.json作为卡片profile配置文件。内部字段结构说明如下表所示。

表1 卡片form_config.json配置文件

属性名称含义数据类型是否可缺省
name表示卡片的名称,字符串最大长度为127字节。字符串
displayName表示卡片的显示名称。取值可以是名称内容,也可以是对名称内容的资源索引,以支持多语言。字符串最小长度为1字节,最大长度为30字节。字符串
description表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。字符串可缺省,缺省为空。
src表示卡片对应的UI代码的完整路径。当为ArkTS卡片时,完整路径需要包含卡片文件的后缀,如:"./ets/widget/pages/WidgetCard.ets"。当为JS卡片时,完整路径无需包含卡片文件的后缀,如:"./js/widget/pages/WidgetCard"字符串
uiSyntax

表示该卡片的类型,当前支持如下两种类型:

- arkts:当前卡片为ArkTS卡片。

- hml:当前卡片为JS卡片。

字符串可缺省,缺省值为hml
window用于定义与显示窗口相关的配置。对象可缺省,缺省值见表2。
isDefault

表示该卡片是否为默认卡片,每个UIAbility有且只有一个默认卡片。

- true:默认卡片。

- false:非默认卡片。

布尔值
colorMode

表示卡片的主题样式,取值范围如下:

- auto:跟随系统的颜色模式值选取主题。

- dark:深色主题。

- light:浅色主题。

字符串可缺省,缺省值为“auto”。
supportDimensions

表示卡片支持的外观规格,取值范围:

- 1 * 2:表示1行2列的二宫格。

- 2 * 2:表示2行2列的四宫格。

- 2 * 4:表示2行4列的八宫格。

- 4 * 4:表示4行4列的十六宫格。

- 1 * 1:表示1行1列的圆形卡片。

- 6 * 4:表示6行4列的二十四宫格。

字符串数组
defaultDimension表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。字符串
updateEnabled

表示卡片是否支持周期性刷新(包含定时刷新和定点刷新),取值范围:

- true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,当两者同时配置时,定时刷新优先生效。

- false:表示不支持周期性刷新。

布尔类型
scheduledUpdateTime

表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。

说明:

updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。

字符串可缺省,缺省时不进行定点刷新。
updateDuration

表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。

当取值为0时,表示该参数不生效。

当取值为正整数N时,表示刷新周期为30*N分钟。

说明:

updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。

数值可缺省,缺省值为“0”。
formConfigAbility表示卡片的配置跳转链接,采用URI格式。字符串可缺省,缺省值为空。
metadata表示卡片的自定义信息,参考Metadata数组标签。对象可缺省,缺省值为空。
dataProxyEnabled

表示卡片是否支持卡片代理刷新,取值范围:

- true:表示支持代理刷新。

- false:表示不支持代理刷新。

设置为true时,定时刷新和下次刷新不生效,但不影响定点刷新。

布尔类型可缺省,缺省值为false。
isDynamic

表示此卡片是否为动态卡片(仅针对ArkTS卡片生效)。

- true:为动态卡片 。

- false:为静态卡片。

布尔类型可缺省,缺省值为true。
formVisibleNotify表示是否允许卡片使用卡片可见性通知(仅对系统应用的卡片生效)。布尔类型可缺省,缺省值为false。
transparencyEnabled

表示是否支持卡片使用方设置此卡片的背景透明度(仅对系统应用的ArkTS卡片生效。)。

- true:支持设置背景透明度 。

- false:不支持设置背景透明度。

布尔类型可缺省,缺省值为false。
fontScaleFollowSystem

表示卡片使用方设置此卡片的字体是否支持跟随系统变化。

- true:支持跟随系统字体大小变化 。

- false:不支持跟随系统字体大小变化。

布尔类型可缺省,缺省值为true。
supportShapes

表示卡片的显示形状,取值范围如下:

- rect:表示方形卡片。

- circle:表示圆形卡片。

字符串可缺省,缺省值为“rect”。

表2 window对象的内部结构说明

属性名称含义数据类型是否可缺省
designWidth标识页面设计基准宽度。以此为基准,根据实际设备宽度来缩放元素大小。数值可缺省,缺省值为720px。
autoDesignWidth标识页面设计基准宽度是否自动计算。当配置为true时,designWidth将会被忽略,设计基准宽度由设备宽度与屏幕密度计算得出。布尔值可缺省,缺省值为false。
{
  "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": true,
      "isDefault": true,
      "updateEnabled": false,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "1*2",
      "supportDimensions": [
        "1*2",
        "2*2",
        "2*4",
        "4*4",
        "6*4"
      ]
    }
  ]
}

卡片生命周期管理

创建ArkTS卡片,需实现FormExtensionAbility生命周期接口。

import { formBindingData, FormExtensionAbility, formInfo, formProvider } from "@kit.FormKit";
import { Want, Configuration } from "@kit.AbilityKit";
import { BusinessError } from "@kit.BasicServicesKit";

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

// ArkTS规范中ets文件无法使用Object.keys和for..in...获取Object的key值,请使用自定义函数getObjKeys代替。
// 使用时请将此函数单独抽离至一个ts文件中并导出,在需要用到的ets文件中导入此函数后使用。
function getObjKeys(obj: Object): string[] {
  let keys = Object.keys(obj);
  return keys;
}

export default class EntryFormAbility extends FormExtensionAbility {
  /**
   * 卡片提供方接收创建卡片的通知接口。
   * @param want 请求创建卡片的能力。
   * want表示获取卡片状态的描述。描述包括Bundle名称、能力名称、模块名称、卡片名和卡片维度
   * @return formBindingData.FormBindingData对象,卡片要显示的数据。
   * */
  onAddForm(want: Want) {
    console.log(`${TAG} onAddForm, want: ${want.abilityName}`);
    let dataObj1: Record<string, string> = {
      'temperature': '11c',
      'time': '11:00'
    };
    let obj1: formBindingData.FormBindingData = formBindingData.createFormBindingData(dataObj1);
    return obj1;
  }

  /**
   * 卡片提供方接收临时卡片转常态卡片的通知接口。
   * @param formId 请求转换为常态的卡片标识。
   * @return 无返回值。
   * */
  onCastToNormalForm(formId: string) {
    console.log(`${TAG} onCastToNormalForm, formId: ${formId}`);
  }

  /**
   *卡片提供方接收携带参数的更新卡片的通知接口。获取最新数据后调用formProvider的updateForm接口刷新卡片数据。
   * @param formId 卡片标识,用于更新卡片数据。
   * @param wantParams 携带的参数。
   * */
  onUpdateForm(formId: string, wantParams?: Record<string, Object>) {
    console.log(`${TAG} onUpdateForm, formId: ${formId},
        wantPara: ${wantParams?.['ohos.extra.param.key.host_bg_inverse_color']}`);
    let param: Record<string, string> = {
      'temperature': '22c',
      'time': '22:00'
    }
    let obj2: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
    formProvider.updateForm(formId, obj2).then(() => {
      console.log(`${TAG} context updateForm`);
    }).catch((error: BusinessError) => {
      console.error(`${TAG} context updateForm failed, data: ${error}`);
    });
  }

  /**
   * 卡片提供方接收修改可见性的通知接口。
   * 该接口仅对系统应用生效,且需要将formVisibleNotify配置为true。
   * @param newStatus 卡片可见性变化的状态。
   * */
  nChangeFormVisibility(newStatus: Record<string, number>) {
    console.log(`${TAG} onChangeFormVisibility, newStatus: ${newStatus}`);
    let param: Record<string, string> = {
      'temperature': '22c',
      'time': '22:00'
    }
    let obj2: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);

    let keys: string[] = getObjKeys(newStatus);

    for (let i: number = 0; i < keys.length; i++) {
      console.log(`${TAG} onChangeFormVisibility, key: ${keys[i]}, value= ${newStatus[keys[i]]}`);
      formProvider.updateForm(keys[i], obj2).then(() => {
        console.log(`${TAG} context updateForm`);
      }).catch((error: BusinessError) => {
        console.error(`Operation updateForm failed. Cause: ${JSON.stringify(error)}`);
      });
    }
  }

  /**
   * 卡片提供方接收处理卡片事件的通知接口。
   * @param formId 卡片标识。
   * @param message 卡片事件的消息内容。
   * */
  onFormEvent(formId: string, message: string) {
    console.log(`${TAG} onFormEvent, formId: ${formId}, message: ${message}`);
  }

  /**
   * 卡片提供方接收卡片被移除的通知接口。
   * @param formId 卡片标识。
   * */
  onRemoveForm(formId: string) {
    console.log(`${TAG} onRemoveForm, formId: ${formId}`);
  }

  /**
   * 卡片提供方接收配置更新的通知接口。
   * @param newConfig 新的配置。
   * */
  onConfigurationUpdate(newConfig: Configuration) {
    // 仅当前formExtensionAbility存活时更新配置才会触发此生命周期。
    // 需要注意:formExtensionAbility创建后10秒内无操作将会被清理。
    console.log(`${TAG} onConfigurationUpdate, config: ${JSON.stringify(newConfig)}`);
  }

  /**
   * 卡片提供方接收卡片状态查询的通知接口。
   * @param want 查询卡片状态的能力。
   * want表示获取卡片状态的描述。描述包括Bundle名称、能力名称、模块名称、卡片名和卡片维度
   * */
  onAcquireFormState(want: Want) {
    console.log(`${TAG} onAcquireFormState, want: ${want}`);
    return formInfo.FormState.UNKNOWN;
  }

  /**
   * 当卡片提供方的卡片进程退出时,触发该回调。
   * */
  onStop() {
    console.log(`${TAG} onStop`);
  }
};

说明

FormExtensionAbility进程不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务,在生命周期调度完成后会继续存在10秒,如10秒内没有新的生命周期回调触发则进程自动退出。针对可能需要10秒以上才能完成的业务逻辑,建议拉起主应用进行处理,处理完成后使用updateForm通知卡片进行刷新。

开发卡片页面 

卡片页面能力说明

开发者可以使用声明式范式开发ArkTS卡片页面。如下卡片页面由DevEco Studio模板自动生成,开发者可以根据自身的业务场景进行调整。

ArkTS卡片具备JS卡片的全量能力,并且新增了动效能力和自定义绘制的能力,支持声明式范式的部分组件、事件、动效、数据管理、状态管理能力,详见“ArkTS卡片支持的页面能力”。

ArkTS卡片支持的页面能力

ArkTS卡片支持的页面能力详见学习ArkTS语言和ArkTS声明式开发范式。

只有标识“支持在ArkTS卡片中使用”的组件和接口可用于ArkTS卡片,同时请留意卡片场景下的能力差异说明。

例如:以下说明表示@Component装饰器可在ArkTS卡片中使用。

卡片使用动效能力 

ArkTS卡片开放了使用动画效果的能力,支持显式动画、属性动画、组件内转场能力。需要注意的是,ArkTS卡片使用动画效果时具有以下限制:

表1 动效参数限制

名称参数说明限制描述
duration动画播放时长限制最长的动效播放时长为1秒,当设置大于1秒的时间时,动效时长仍为1秒。
tempo动画播放速度卡片中禁止设置此参数,使用默认值1。
delay动画延迟执行的时长卡片中禁止设置此参数,使用默认值0。
iterations动画播放次数卡片中禁止设置此参数,使用默认值1。

说明

静态卡片不支持使用动效能力。

 以下示例代码实现了按钮旋转的动画效果:

@Entry
@Component
export struct Content_D3 {
  @State rotateAngle: number = 0;

  build() {
    Row() {
      Button('change rotate angle')
        .height('20%')
        .width('90%')
        .margin('5%')
        .onClick(() => {
          this.rotateAngle = (this.rotateAngle === 0 ? 90 : 0);
        })
        .rotate({ angle: this.rotateAngle })
        .animation({
          curve: Curve.EaseOut,
          playMode: PlayMode.Normal,
        })
    }.height('100%').alignItems(VerticalAlign.Center)
  }
}

 

卡片使用自定义绘制能力 

ArkTS卡片开放了自定义绘制的能力,在卡片上可以通过Canvas组件创建一块画布,然后通过CanvasRenderingContext2D对象在画布上进行自定义图形的绘制,如下示例代码实现了在画布的中心绘制了一个笑脸。

@Entry
@Component
struct CustomCanvasDrawingCard {
  private canvasWidth: number = 0;
  private canvasHeight: number = 0;
  // 初始化CanvasRenderingContext2D和RenderingContextSettings
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  build() {
    Column() {
      Row() {
        Canvas(this.context)
          .width('100%')
          .height('100%')
          .onReady(() => {
            // 在onReady回调中获取画布的实际宽和高
            this.canvasWidth = this.context.width;
            this.canvasHeight = this.context.height;
            // 绘制画布的背景
            this.context.fillStyle = '#EEF0FF';
            this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
            // 在画布的中心绘制一个圆
            this.context.beginPath();
            let radius = this.context.width / 3;
            let circleX = this.context.width / 2;
            let circleY = this.context.height / 2;
            this.context.moveTo(circleX - radius, circleY);
            this.context.arc(circleX, circleY, radius, 2 * Math.PI, 0, true);
            this.context.closePath();
            this.context.fillStyle = '#FFBB1B';
            this.context.fill();
            // 绘制笑脸的左眼
            let leftR = radius / 13;
            let leftX = circleX - (radius / 2.3);
            let leftY = circleY - (radius / 4.5);
            this.context.beginPath();
            this.context.arc(leftX, leftY, leftR, 0, 2 * Math.PI, true);
            this.context.closePath();
            this.context.fillStyle = '#000';
            this.context.lineWidth = 15;
            this.context.stroke();
            // 绘制笑脸的右眼
            let rightR = radius / 13;
            let rightX = circleX + (radius / 2.3);
            let rightY = circleY - (radius / 4.5);
            this.context.beginPath();
            this.context.arc(rightX, rightY, rightR, 0, 2 * Math.PI, true);
            this.context.closePath();
            this.context.fillStyle = '#000';
            this.context.lineWidth = 15;
            this.context.stroke();

            // 绘制笑脸的嘴巴
            let mouthR = radius / 2;
            let mouthX = circleX;
            let mouthY = circleY + 10;
            this.context.beginPath();
            this.context.arc(mouthX, mouthY, mouthR, Math.PI / 1.4, Math.PI / 3.4, true);
            this.context.strokeStyle = '#000';
            this.context.lineWidth = 15;
            this.context.stroke();
            this.context.closePath();
          })
      }
    }.height('100%').width('100%')
  }
}

 

开发卡片事件

卡片事件能力说明

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

针对静态卡片,ArkTS卡片提供了FormLink用于卡片内部和提供方应用间的交互。

动态卡片事件能力说明

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

  • router事件:可以使用router事件跳转到指定UIAbility,并通过router事件刷新卡片内容。
  • call事件:可以使用call事件拉起指定UIAbility到后台,并通过call事件刷新卡片内容。
  • message事件:可以使用message拉起FormExtensionAbility,并通过FormExtensionAbility刷新卡片内容。
静态卡片事件能力说明

写到拓展里面了 ---静态卡片能力拓展 

使用router事件跳转到指定UIAbility

在卡片中使用postCardAction接口的router能力,能够快速拉起卡片提供方应用的指定UIAbility,因此UIAbility较多的应用往往会通过卡片提供不同的跳转按钮,实现一键直达的效果。例如相机卡片,卡片上提供拍照、录像等按钮,点击不同按钮将拉起相机应用的不同UIAbility,从而提高用户的体验。

通常使用按钮控件来实现页面拉起,示例代码如下:

  • 在卡片页面中布局两个按钮,点击其中一个按钮时调用postCardAction向指定UIAbility发送router事件,并在事件内定义需要传递的内容。

    @Entry
    @Component
    struct WidgetEventRouterCard {
      build() {
        Column() {
          Text('卡片')
            .fontColor('#fff68383')
            .opacity(0.9)
            .fontSize(14)
            .margin({ top: '8%', left: '10%' })
          Row() {
            Column() {
              Button() {
                Text('打开页面A')
                  .fontColor('#45A6F4')
                  .fontSize(12)
              }
              .width(120)
              .height(32)
              .margin({ top: '20%' })
              .backgroundColor('#FFFFFF')
              .borderRadius(16)
              .onClick(() => {
                postCardAction(this, {
                  action: 'router',
                  abilityName: 'EntryAbility',
                  params: { targetPage: 'PageA' }
                });
              })
    
              Button() {
                Text('打开页面B')
                  .fontColor('#45A6F4')
                  .fontSize(12)
              }
              .width(120)
              .height(32)
              .margin({ top: '8%', bottom: '15vp' })
              .backgroundColor('#FFFFFF')
              .borderRadius(16)
              .onClick(() => {
                postCardAction(this, {
                  action: 'router',
                  abilityName: 'EntryAbility',
                  params: { targetPage: 'PageB' }
                });
              })
            }
          }.width('100%').height('80%')
          .justifyContent(FlexAlign.Center)
        }
        .width('100%')
        .height('100%')
        .alignItems(HorizontalAlign.Start)
        .backgroundImageSize(ImageSize.Cover)
      }
    }
  •  在UIAbility中接收router事件并获取参数,根据传递的params不同,选择拉起不同的页面。
    import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { window } from '@kit.ArkUI';
    
    
    const TAG: string = 'EntryAbility';
    const DOMAIN_NUMBER: number = 0xFF00;
    
    export default class EntryAbility extends UIAbility {
      private selectPage: string = '';
      private currentWindowStage: window.WindowStage | null = null;
    
      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    
        hilog.info(DOMAIN_NUMBER, TAG, `Ability onCreate: ${JSON.stringify(want?.parameters)}`);
        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;
          hilog.info(DOMAIN_NUMBER, TAG, `onCreate selectPage: ${this.selectPage}`);
        }
      }
    
      // 如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期回调
      onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        hilog.info(DOMAIN_NUMBER, TAG, `Ability onNewWant: ${JSON.stringify(want?.parameters)}`);
        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;
          hilog.info(DOMAIN_NUMBER, TAG, `onNewWant selectPage: ${this.selectPage}`);
        }
        if (this.currentWindowStage !== null) {
          this.onWindowStageCreate(this.currentWindowStage);
        }
      }
    
      onDestroy(): void {
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
      }
    
      onWindowStageCreate(windowStage: window.WindowStage): void {
        // Main window is created, set main page for this ability
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
        // Main window is created, set main page for this ability
        let targetPage: string;
        // 根据传递的targetPage不同,选择拉起不同的页面
        switch (this.selectPage) {
          case 'PageA':
            targetPage = 'pages/PageA';
            break;
          case 'PageB':
            targetPage = 'pages/PageB';
            break;
          default:
            targetPage = 'pages/Index';
        }
        if (this.currentWindowStage === null) {
          this.currentWindowStage = windowStage;
        }
    
        windowStage.loadContent(targetPage, (err) => {
          if (err.code) {
            hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
            return;
          }
          hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
        });
      }
    
      onWindowStageDestroy(): void {
        // Main window is destroyed, release UI related resources
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
      }
    
      onForeground(): void {
        // Ability has brought to foreground
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
      }
    
      onBackground(): void {
        // Ability has back to background
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
      }
    }
    

    效果图

 

 使用call事件拉起指定UIAbility到后台

许多应用希望借助卡片的能力,实现和应用在前台时相同的功能。例如音乐卡片,卡片上提供播放、暂停等按钮,点击不同按钮将触发音乐应用的不同功能,进而提高用户的体验。在卡片中使用postCardAction接口的call能力,能够将卡片提供方应用的指定的UIAbility拉到后台。同时,call能力提供了调用应用指定方法、传递数据的功能,使应用在后台运行时可以通过卡片上的按钮执行不同的功能。

约束限制

提供方应用需要具备后台运行权限(ohos.permission.KEEP_BACKGROUND_RUNNING)。

权限在 module.json5 里面配置 与page同级

"requestPermissions": [
  {"name": 'ohos.permission.KEEP_BACKGROUND_RUNNING'}
]
开发步骤

通常使用按钮控件来触发call事件,示例代码如下:

  • 在卡片页面中布局两个按钮,点击其中一个按钮时调用postCardAction向指定UIAbility发送call事件,并在事件内定义需要调用的方法和传递的数据。需要注意的是,method参数为必选参数,且类型需要为string类型,用于触发UIAbility中对应的方法。
@Entry
@Component
struct WidgetEventCallCard {
  @LocalStorageProp('formId') formId: string = '12400633174999288';

  build() {
    Column() {
      //...
      Row() {
        Column() {
          Button() {
          //...
          }
          //...
          .onClick(() => {
            postCardAction(this, {
              action: 'call',
              abilityName: 'WidgetEventCallEntryAbility', // 只能跳转到当前应用下的UIAbility,与module.json5中定义保持
              params: {
                formId: this.formId,
                method: 'funA' // 在EntryAbility中调用的方法名
              }
            });
          })

          Button() {
          //...
          }
          //...
          .onClick(() => {
            postCardAction(this, {
              action: 'call',
              abilityName: 'WidgetEventCallEntryAbility', // 只能跳转到当前应用下的UIAbility,与module.json5中定义保持
              params: {
                formId: this.formId,
                method: 'funB', // 在EntryAbility中调用的方法名
                num: 1 // 需要传递的其他参数
              }
            });
          })
        }
      }.width('100%').height('80%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
  }
}
  • 在UIAbility中接收call事件并获取参数,根据传递的method不同,执行不同的方法。其余数据可以通过readString方法获取。需要注意的是,UIAbility需要onCreate生命周期中监听所需的方法。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
  
const TAG: string = 'WidgetEventCallEntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;
const CONST_NUMBER_1: number = 1;
const CONST_NUMBER_2: number = 2;
  
class MyParcelable implements rpc.Parcelable {
  num: number;
  str: string;
  
  constructor(num: number, str: string) {
    this.num = num;
    this.str = str;
  }
  
  marshalling(messageSequence: rpc.MessageSequence): boolean {
    messageSequence.writeInt(this.num);
    messageSequence.writeString(this.str);
    return true;
  }
  
  unmarshalling(messageSequence: rpc.MessageSequence): boolean {
    this.num = messageSequence.readInt();
    this.str = messageSequence.readString();
      return true;
  }
}
  
export default class WidgetEventCallEntryAbility extends UIAbility {
  // 如果UIAbility第一次启动,在收到call事件后会触发onCreate生命周期回调
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      // 监听call事件所需的方法
      this.callee.on('funA', (data: rpc.MessageSequence) => {
        // 获取call事件中传递的所有参数
        hilog.info(DOMAIN_NUMBER, TAG, `FunACall param:  ${JSON.stringify(data.readString())}`);
        promptAction.showToast({
          message: 'FunACall param:' + JSON.stringify(data.readString())
        });
        return new MyParcelable(CONST_NUMBER_1, 'aaa');
      });
      this.callee.on('funB', (data: rpc.MessageSequence) => {
        // 获取call事件中传递的所有参数
        hilog.info(DOMAIN_NUMBER, TAG, `FunBCall param:  ${JSON.stringify(data.readString())}`);
        promptAction.showToast({
          message: 'FunBCall param:' + JSON.stringify(data.readString())
        });
        return new MyParcelable(CONST_NUMBER_2, 'bbb');
      });
    } catch (err) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee on. Cause: ${JSON.stringify(err as BusinessError)}`);
    }
  }
  
  // 进程退出时,解除监听
  onDestroy(): void | Promise<void> {
    try {
      this.callee.off('funA');
      this.callee.off('funB');
    } catch (err) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee off. Cause: ${JSON.stringify(err as BusinessError)}`);
    }
  }
}

通过message事件刷新卡片内容

在卡片页面中可以通过postCardAction接口触发message事件拉起FormExtensionAbility,然后由FormExtensionAbility刷新卡片内容,下面是这种刷新方式的简单示例。

  • 在卡片页面通过注册Button的onClick点击事件回调,并在回调中调用postCardAction接口触发message事件拉起FormExtensionAbility。卡片页面中使用LocalStorageProp装饰需要刷新的卡片数据。
let storageUpdateByMsg = new LocalStorage();

@Entry(storageUpdateByMsg)
@Component
struct UpdateByMessageCard {
  @LocalStorageProp('title') title: ResourceStr = $r('app.string.default_title');
  @LocalStorageProp('detail') detail: ResourceStr = $r('app.string.DescriptionDefault');

  build() {
    Column() {
      Column() {
        Text(this.title)
          .opacity(0.9)
          .fontSize(14)
          .margin({ top: '8%', left: '10%' })
        Text(this.detail)
          .opacity(0.6)
          .fontSize(12)
          .margin({ top: '5%', left: '10%' })
      }.width('100%').height('50%')
      .alignItems(HorizontalAlign.Start)

      Row() {
        Button() {
          Text($r('app.string.update'))
            .fontColor('#45A6F4')
            .fontSize(12)
        }
        .width(120)
        .height(32)
        .margin({ top: '30%', bottom: '10%' })
        .backgroundColor('#FFFFFF')
        .borderRadius(16)
        .onClick(() => {
          postCardAction(this, {
            action: 'message',
            params: { msgTest: 'messageEvent' }
          });
        })
      }.width('100%').height('40%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start)
    .backgroundImageSize(ImageSize.Cover)
  }
}
  •  在FormExtensionAbility的onFormEvent生命周期中调用updateForm接口刷新卡片。
import { formBindingData, FormExtensionAbility, formInfo, formProvider } from "@kit.FormKit";
import { Want, Configuration } from "@kit.AbilityKit";
import { BusinessError } from "@kit.BasicServicesKit";

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

interface CartCount {
  formId: string
}

// ArkTS规范中ets文件无法使用Object.keys和for..in...获取Object的key值,请使用自定义函数getObjKeys代替。
// 使用时请将此函数单独抽离至一个ts文件中并导出,在需要用到的ets文件中导入此函数后使用。
function getObjKeys(obj: Object): string[] {
  let keys = Object.keys(obj);
  return keys;
}

export default class EntryFormAbility extends FormExtensionAbility {
  /**
   * 卡片提供方接收卡片添加的通知接口。
   * @param want 卡片添加的能力。
   * want表示获取卡片添加的描述。描述包括Bundle名称、能力名称、模块名称、卡片名和卡片维度
   * @return 返回一个FormBindingData对象。
   * */
  onAddForm(want: Want) {
    let formId = want.parameters!["ohos.extra.param.key.form_identity"] as string
    // Called to return a FormBindingData object.
    let formData: CartCount = {
      formId: formId
    };
    // 这里不允许直接存储首选项 此首选项非应用的首选项
    return formBindingData.createFormBindingData(formData);
  }

  /**
   * 卡片提供方接收临时卡片转常态卡片的通知接口。
   * @param formId 请求转换为常态的卡片标识。
   * @return 无返回值。
   * */
  onCastToNormalForm(formId: string) {
    console.log(`${TAG} onCastToNormalForm, formId: ${formId}`);
  }

  /**
   *卡片提供方接收携带参数的更新卡片的通知接口。获取最新数据后调用formProvider的updateForm接口刷新卡片数据。
   * @param formId 卡片标识,用于更新卡片数据。
   * @param wantParams 携带的参数。
   * */
  onUpdateForm(formId: string, wantParams?: Record<string, Object>) {
    console.log(`${TAG} onUpdateForm, formId: ${formId},
        wantPara: ${wantParams?.['ohos.extra.param.key.host_bg_inverse_color']}`);
    let param: Record<string, string> = {
      'temperature': '22c',
      'time': '22:00'
    }
    let obj2: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
    formProvider.updateForm(formId, obj2).then(() => {
      console.log(`${TAG} context updateForm`);
    }).catch((error: BusinessError) => {
      console.error(`${TAG} context updateForm failed, data: ${error}`);
    });
  }

  /**
   * 卡片提供方接收修改可见性的通知接口。
   * 该接口仅对系统应用生效,且需要将formVisibleNotify配置为true。
   * @param newStatus 卡片可见性变化的状态。
   * */
  nChangeFormVisibility(newStatus: Record<string, number>) {
    console.log(`${TAG} onChangeFormVisibility, newStatus: ${newStatus}`);
    let param: Record<string, string> = {
      'temperature': '22c',
      'time': '22:00'
    }
    let obj2: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);

    let keys: string[] = getObjKeys(newStatus);

    for (let i: number = 0; i < keys.length; i++) {
      console.log(`${TAG} onChangeFormVisibility, key: ${keys[i]}, value= ${newStatus[keys[i]]}`);
      formProvider.updateForm(keys[i], obj2).then(() => {
        console.log(`${TAG} context updateForm`);
      }).catch((error: BusinessError) => {
        console.error(`Operation updateForm failed. Cause: ${JSON.stringify(error)}`);
      });
    }
  }

  /**
   * 卡片提供方接收处理卡片事件的通知接口。
   * @param formId 卡片标识。
   * @param message 卡片事件的消息内容。
   * */
  onFormEvent(formId: string, message: string): void {
    // Called when a specified message event defined by the form provider is triggered.
    console.log(TAG, `FormAbility onFormEvent, formId = ${formId}, message: ${JSON.stringify(message)}`);

    class FormDataClass {
      title: string = '我不好'; // 和卡片布局中对应
      detail: string = '呵呵呵呵呵'; // 和卡片布局中对应
    }

    let formData = new FormDataClass();
    let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData);
    formProvider.updateForm(formId, formInfo).then(() => {
      console.log(TAG, 'FormAbility updateForm success.');
    }).catch((error: BusinessError) => {
      console.log(TAG, JSON.stringify(FormDataClass));
      console.log(TAG, `Operation updateForm failed. Cause: ${JSON.stringify(error)}`);
    })
  }

  /**
   * 卡片提供方接收卡片被移除的通知接口。
   * @param formId 卡片标识。
   * */
  onRemoveForm(formId: string) {
    console.log(`${TAG} onRemoveForm, formId: ${formId}`);
  }

  /**
   * 卡片提供方接收配置更新的通知接口。
   * @param newConfig 新的配置。
   * */
  onConfigurationUpdate(newConfig: Configuration) {
    // 仅当前formExtensionAbility存活时更新配置才会触发此生命周期。
    // 需要注意:formExtensionAbility创建后10秒内无操作将会被清理。
    console.log(`${TAG} onConfigurationUpdate, config: ${JSON.stringify(newConfig)}`);
  }

  /**
   * 卡片提供方接收卡片状态查询的通知接口。
   * @param want 查询卡片状态的能力。
   * want表示获取卡片状态的描述。描述包括Bundle名称、能力名称、模块名称、卡片名和卡片维度
   * */
  onAcquireFormState(want: Want) {
    console.log(`${TAG} onAcquireFormState, want: ${want}`);
    return formInfo.FormState.UNKNOWN;
  }

  /**
   * 当卡片提供方的卡片进程退出时,触发该回调。
   * */
  onStop() {
    console.log(`${TAG} onStop`);
  }
};

 

通过router事件刷新卡片内容 

  • 在卡片页面通过注册Button的onClick点击事件回调,并在回调中调用postCardAction接口触发router事件拉起UIAbility。
let storageUpdateRouter = new LocalStorage();

@Entry(storageUpdateRouter)
@Component
struct WidgetUpdateRouterCard {
  @LocalStorageProp('routerDetail') routerDetail: ResourceStr = $r('app.string.init');

  build() {
    Column() {
      Column() {
        Text(this.routerDetail)
          .opacity(0.9)
          .fontSize(14)
          .margin({ top: '8%', left: '10%', right: '10%' })
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .maxLines(2)
      }.width('100%').height('50%')
      .alignItems(HorizontalAlign.Start)

      Row() {
        Button() {
          Text($r('app.string.JumpLabel'))
            .fontColor('#45A6F4')
            .fontSize(12)
        }
        .width(120)
        .height(32)
        .margin({ top: '30%', bottom: '10%' })
        .backgroundColor('#FFFFFF')
        .borderRadius(16)
        .onClick(() => {
          postCardAction(this, {
            action: 'router',
            abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
            params: {
              routerDetail: 'RouterFromCard',
            }
          });
        })
      }.width('100%').height('40%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start)
    .backgroundImageSize(ImageSize.Cover)
  }
}
  • 在UIAbility的onCreate或者onNewWant生命周期中可以通过入参want获取卡片的formID和传递过来的参数信息,然后调用updateForm接口刷新卡片。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, formInfo, formProvider } from '@kit.FormKit';


const TAG: string = 'EntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;


export default class EntryAbility extends UIAbility {
  handleFormRouterEvent(want: Want, source: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, `handleFormRouterEvent ${source}, Want: ${JSON.stringify(want)}`);
    if (want.parameters && want.parameters[formInfo.FormParam.IDENTITY_KEY] !== undefined) {
      let curFormId = want.parameters[formInfo.FormParam.IDENTITY_KEY].toString();
      // want.parameters.params 对应 postCardAction() 中 params 内容
      let message: string = (JSON.parse(want.parameters?.params as string))?.routerDetail;
      hilog.info(DOMAIN_NUMBER, TAG, `UpdateForm formId: ${curFormId}, message: ${message}`);
      let formData: Record<string, string> = {
        'routerDetail': "不能累,加油吧,明天是美好的"
      };
      let formMsg = formBindingData.createFormBindingData(formData);
      formProvider.updateForm(curFormId, formMsg).then((data) => {
        hilog.info(DOMAIN_NUMBER, TAG, 'updateForm success.', JSON.stringify(data));
      }).catch((error: BusinessError) => {
        hilog.info(DOMAIN_NUMBER, TAG, 'updateForm failed.', JSON.stringify(error));
      });
    }
  }

  // 如果UIAbility第一次启动,在收到call事件后会触发onCreate生命周期回调
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleFormRouterEvent(want, 'onCreate');
  }

  // 如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期回调
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN_NUMBER, TAG, 'onNewWant Want:', JSON.stringify(want));
    this.handleFormRouterEvent(want, 'onNewWant');
  }

  onDestroy(): void | Promise<void> {
    try {
      this.callee.off('PageA');
      this.callee.off('PageB');
    } catch (err) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee off. Cause: ${JSON.stringify(err as BusinessError)}`);
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    // Main window is created, set main page for this ability


    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

 

通过call事件刷新卡片内容 

  • 在使用postCardAction接口的call事件时,需要在FormExtensionAbility中的onAddForm生命周期回调中更新formId
import { Want } from '@kit.AbilityKit';
import { formBindingData, FormExtensionAbility } from '@kit.FormKit';

export default class WidgetCalleeFormAbility extends FormExtensionAbility {
  onAddForm(want: Want): formBindingData.FormBindingData {
    class DataObj1 {
      formId: string = '';
    }

    let dataObj1 = new DataObj1();
    if (want.parameters && want.parameters['ohos.extra.param.key.form_identity'] !== undefined) {
      let formId: string = want.parameters['ohos.extra.param.key.form_identity'].toString();
      dataObj1.formId = formId;
    }
    let obj1 = formBindingData.createFormBindingData(dataObj1);
    return obj1;
  }
  // ...
}

 在卡片页面通过注册Button的onClick点击事件回调,并在回调中调用postCardAction接口触发call事件拉起UIAbility。

let storageUpdateCall = new LocalStorage();

@Entry(storageUpdateCall)
@Component
struct WidgetUpdateCallCard {
  @LocalStorageProp('formId') formId: string = '12400633174999288';
  @LocalStorageProp('calleeDetail') calleeDetail: ResourceStr = $r('app.string.init');

  build() {
    Column() {
      Column() {
        Text(this.calleeDetail)
          .opacity(0.9)
          .fontSize(14)
          .margin({ top: '8%', left: '10%' })
      }.width('100%').height('50%')
      .alignItems(HorizontalAlign.Start)

      Row() {
        Button() {
          Text($r('app.string.JumpLabel'))
            .fontColor('#45A6F4')
            .fontSize(12)
        }
        .width(120)
        .height(32)
        .margin({ top: '30%', bottom: '10%' })
        .backgroundColor('#FFFFFF')
        .borderRadius(16)
        .onClick(() => {
          postCardAction(this, {
            action: 'call',
            abilityName: 'EntryAbility', // 只能拉起当前应用下的UIAbility
            params: {
              method: 'funA',
              formId: this.formId,
              calleeDetail: 'CallFrom'
            }
          });
        })
      }.width('100%').height('40%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start)
    .backgroundImageSize(ImageSize.Cover)
  }
}
  •  在UIAbility的onCreate生命周期中监听call事件所需的方法,然后在对应方法中调用updateForm接口刷新卡片。

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, formProvider } from '@kit.FormKit';
import { rpc } from '@kit.IPCKit';


const TAG: string = 'EntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;
const MSG_SEND_METHOD: string = 'funA';
const CONST_NUMBER_1: number = 1;

class MyParcelable implements rpc.Parcelable {
  num: number;
  str: string;

  constructor(num: number, str: string) {
    this.num = num;
    this.str = str;
  };

  marshalling(messageSequence: rpc.MessageSequence): boolean {
    messageSequence.writeInt(this.num);
    messageSequence.writeString(this.str);
    return true;
  };

  unmarshalling(messageSequence: rpc.MessageSequence): boolean {
    this.num = messageSequence.readInt();
    this.str = messageSequence.readString();
    return true;
  };
}

// 在收到call事件后会触发callee监听的方法
let funACall = (data: rpc.MessageSequence): MyParcelable => {
  // 获取call事件中传递的所有参数
  let params: Record<string, string> = JSON.parse(data.readString());
  if (params.formId !== undefined) {
    let curFormId: string = params.formId;
    let message: string = params.calleeDetail;
    hilog.info(DOMAIN_NUMBER, TAG, `UpdateForm formId: ${curFormId}, message: ${message}`);
    let formData: Record<string, string> = {
      'calleeDetail': '卧槽没数据啊'
    };
    let formMsg: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData);
    formProvider.updateForm(curFormId, formMsg).then((data) => {
      hilog.info(DOMAIN_NUMBER, TAG, `updateForm success. ${JSON.stringify(data)}`);
    }).catch((error: BusinessError) => {
      hilog.error(DOMAIN_NUMBER, TAG, `updateForm failed: ${JSON.stringify(error)}`);
    });
  }
  return new MyParcelable(CONST_NUMBER_1, 'aaa');
};

export default class EntryAbility extends UIAbility {
  // 如果UIAbility第一次启动,在收到call事件后会触发onCreate生命周期回调
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      // 监听call事件所需的方法
      this.callee.on(MSG_SEND_METHOD, funACall);
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, `${MSG_SEND_METHOD} register failed with error ${JSON.stringify(error)}`);
    }
  }

  // 如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期回调
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN_NUMBER, TAG, 'onNewWant Want:', JSON.stringify(want));
    try {
      // 监听call事件所需的方法
      this.callee.on(MSG_SEND_METHOD, funACall);
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, `${MSG_SEND_METHOD} register failed with error ${JSON.stringify(error)}`);
    }
  }

  onDestroy(): void | Promise<void> {
    try {
      this.callee.off('PageA');
      this.callee.off('PageB');
    } catch (err) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee off. Cause: ${JSON.stringify(err as BusinessError)}`);
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    // Main window is created, set main page for this ability


    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

 

卡片数据交互 

卡片数据交互说明

ArkTS卡片框架提供了updateForm接口和requestForm接口主动触发卡片的页面刷新,通过LocalStorageProp确认需要刷新的卡片数据。

接口是否系统能力约束
updateForm

1. 提供方调用。

2. 提供方仅允许刷新自己的卡片,其他提供方的卡片无法刷新。

requestForm

1. 使用方调用。

2. 仅允许刷新添加到当前使用方的卡片,添加到其他使用方的卡片无法刷新。

 卡片定时刷新和定点刷新

当前卡片框架提供了如下几种按时间刷新卡片的方式:

  • 定时刷新:表示在一定时间间隔内调用onUpdateForm的生命周期回调函数自动刷新卡片内容。可以在form_config.json配置文件的updateDuration字段中进行设置。例如,可以将刷新时间设置为每小时一次。

    说明

    1. 在使用定时和定点刷新功能之前,需要在form_config.json配置文件中设置updateEnabled字段为true,以启用周期性刷新功能。

    当配置了updateDuration(定时刷新)后,该设置会优先于scheduledUpdateTime(定点刷新)生效,即使同时配置了两者,定点刷新也会被忽略。

    2. 为减少卡片被动周期刷新进程启动次数,降低卡片刷新功耗,应用市场在安装应用时可以为该应用配置刷新周期,

    也可以为已经安装的应用动态配置刷新周期,用来限制卡片刷新周期的时长,以达到降低周期刷新进程启动次数的目的。

    ● 当配置了updateDuration(定时刷新)后,若应用市场动态配置了该应用的刷新周期,

    卡片框架会将form_config.json文件中配置的刷新周期与应用市场配置的刷新周期进行比较,取较长的刷新周期做为该卡片的定时刷新周期。

    ● 若应用市场未动态配置该应用的刷新周期,则以form_config.json文件中配置的刷新周期为准。

    ● 若该卡片取消定时刷新功能,该规则将无效。

    ● 卡片定时刷新的更新周期单位为30分钟。应用市场配置的刷新周期范围是1~336,即最短为半小时(1 * 30min)刷新一次,最长为一周(336 * 30min)刷新一次。

    ● 该规则从API11开始生效。若小于API11,则以form_config.json文件中配置的刷新周期为准。

{
  "forms": [
    {
      "name": "UpdateDuration",
      "description": "$string:widget_updateduration_desc",
      "src": "./ets/updateduration/pages/UpdateDurationCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 2,
      "defaultDimension": "2*2",
      "supportDimensions": [
        "2*2"
      ]
    }
  ]
}
  • 定点刷新:表示在每天的某个特定时间点自动刷新卡片内容。可以在form_config.json配置文件中的scheduledUpdateTime字段中进行设置。例如,可以将刷新时间设置为每天的上午10点30分。

说明

当同时配置了定时刷新updateDuration和定点刷新scheduledUpdateTime时,定时刷新的优先级更高。如果想要配置定点刷新,则需要将updateDuration配置为0。

{
  "forms": [
      {
      "name": "ScheduledUpdateTime",
      "description": "$string:widget_scheupdatetime_desc",
      "src": "./ets/scheduledupdatetime/pages/ScheduledUpdateTimeCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 0,
      "defaultDimension": "2*2",
      "supportDimensions": [
        "2*2"
      ]
    }
  ]
}
  •  下次刷新:表示指定卡片的下一次刷新时间。可以通过调用setFormNextRefreshTime接口来实现。最短刷新时间为5分钟。例如,可以在接口调用后的5分钟内刷新卡片内容。
import { FormExtensionAbility, formProvider } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG: string = 'UpdateByTimeFormAbility';
const FIVE_MINUTE: number = 5;
const DOMAIN_NUMBER: number = 0xFF00;

export default class UpdateByTimeFormAbility extends FormExtensionAbility {
  onFormEvent(formId: string, message: string): void {
    // Called when a specified message event defined by the form provider is triggered.
    hilog.info(DOMAIN_NUMBER, TAG, `FormAbility onFormEvent, formId = ${formId}, message: ${JSON.stringify(message)}`);
    try {
      // 设置过5分钟后更新卡片内容
      formProvider.setFormNextRefreshTime(formId, FIVE_MINUTE, (err: BusinessError) => {
        if (err) {
          hilog.info(DOMAIN_NUMBER, TAG, `Failed to setFormNextRefreshTime. Code: ${err.code}, message: ${err.message}`);
          return;
        } else {
          hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in setFormNextRefreshTiming.');
        }
      });
    } catch (err) {
      hilog.info(DOMAIN_NUMBER, TAG, `Failed to setFormNextRefreshTime. Code: ${(err as BusinessError).code}, message: ${(err as BusinessError).message}`);
    }
  }
  // ... 
}

在触发定时、定点或下次刷新后,系统会调用FormExtensionAbility的onUpdateForm生命周期回调,在回调中,可以使用updateForm进行提供方刷新卡片。onUpdateForm生命周期回调的使用请参见通过FormExtensionAbility刷新卡片内容。 

说明

  1. 定时刷新有配额限制,每张卡片每天最多通过定时方式触发刷新50次,定时刷新包含卡片配置项updateDuration和调用setFormNextRefreshTime方法两种方式,当达到50次配额后,无法通过定时方式再次触发刷新,刷新次数会在每天的0点重置。

  2. 当前定时刷新使用同一个计时器进行计时,因此卡片定时刷新的第一次刷新会有最多30分钟的偏差。比如第一张卡片A(每隔半小时刷新一次)在3点20分添加成功,定时器启动并每隔半小时触发一次事件,第二张卡片B(每隔半小时刷新一次)在3点40分添加成功,在3点50分定时器事件触发时,卡片A触发定时刷新,卡片B会在下次事件(4点20分)中才会触发。

  3. 定时刷新和定点刷新仅在屏幕亮屏情况下才会触发,在灭屏场景下仅会记录刷新动作,待亮屏时统一进行刷新。

  4. 如果使能了卡片代理刷新,定时刷新和下次刷新不生效。

 刷新本地图片和网络图片

在卡片上通常需要展示本地图片或从网络上下载的图片,获取本地图片和网络图片需要通过FormExtensionAbility来实现,如下示例代码介绍了如何在卡片上显示本地图片和网络图片。

1. 下载网络图片需要使用到网络能力,需要申请ohos.permission.INTERNET权限,配置方式请参见声明权限。

2. 在EntryFormAbility中的onAddForm生命周期回调中实现本地文件的刷新。

import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { formBindingData, FormExtensionAbility } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'WgtImgUpdateEntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;

export default class WgtImgUpdateEntryFormAbility extends FormExtensionAbility {
  // 在添加卡片时,打开一个本地图片并将图片内容传递给卡片页面显示
  onAddForm(want: Want): formBindingData.FormBindingData {
    // 假设在当前卡片应用的tmp目录下有一个本地图片:head.PNG
    let tempDir = this.context.getApplicationContext().tempDir;
    hilog.info(DOMAIN_NUMBER, TAG, `tempDir: ${tempDir}`);
    let imgMap: Record<string, number> = {};
    try {
      // 打开本地图片并获取其打开后的fd
      let file = fileIo.openSync(tempDir + '/' + 'head.PNG');
      imgMap['imgBear'] = file.fd;
    } catch (e) {
      hilog.error(DOMAIN_NUMBER, TAG, `openSync failed: ${JSON.stringify(e as BusinessError)}`);
    }

    class FormDataClass {
      text: string = 'Image: Bear';
      loaded: boolean = true;
      // 卡片需要显示图片场景, 必须和下列字段formImages 中的key 'imgBear' 相同。
      imgName: string = 'imgBear';
      // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), 'imgBear' 对应 fd
      formImages: Record<string, number> = imgMap;
    }

    let formData = new FormDataClass();
    // 将fd封装在formData中并返回至卡片页面
    return formBindingData.createFormBindingData(formData);
  }
  //...
}

3. 在EntryFormAbility中的onFormEvent生命周期回调中实现网络文件的刷新。 

import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { http } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'WgtImgUpdateEntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;

export default class WgtImgUpdateEntryFormAbility extends FormExtensionAbility {
  async onFormEvent(formId: string, message: string): Promise<void> {
    let param: Record<string, string> = {
      'text': '刷新中...'
    };
    let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
    formProvider.updateForm(formId, formInfo);

    // 注意:FormExtensionAbility在触发生命周期回调时被拉起,仅能在后台存在5秒
    // 建议下载能快速下载完成的小文件,如在5秒内未下载完成,则此次网络图片无法刷新至卡片页面上
    let netFile = 'https://cn-assets.gitee.com/assets/mini_app-e5eee5a21c552b69ae6bf2cf87406b59.jpg'; // 需要在此处使用真实的网络图片下载链接
    let tempDir = this.context.getApplicationContext().tempDir;
    let fileName = 'file' + Date.now();
    let tmpFile = tempDir + '/' + fileName;
    let imgMap: Record<string, number> = {};

    class FormDataClass {
      text: string = 'Image: Bear' + fileName;
      loaded: boolean = true;
      // 卡片需要显示图片场景, 必须和下列字段formImages 中的key fileName 相同。
      imgName: string = fileName;
      // 卡片需要显示图片场景, 必填字段(formImages 不可缺省或改名), fileName 对应 fd
      formImages: Record<string, number> = imgMap;
    }

    let httpRequest = http.createHttp()
    let data = await httpRequest.request(netFile);
    if (data?.responseCode == http.ResponseCode.OK) {
      try {
        let imgFile = fileIo.openSync(tmpFile, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
        imgMap[fileName] = imgFile.fd;
        try{
          let writeLen: number = await fileIo.write(imgFile.fd, data.result as ArrayBuffer);
          hilog.info(DOMAIN_NUMBER, TAG, "write data to file succeed and size is:" + writeLen);
          hilog.info(DOMAIN_NUMBER, TAG, 'ArkTSCard download complete: %{public}s', tmpFile);
          try {
            let formData = new FormDataClass();
            let formInfo = formBindingData.createFormBindingData(formData);
            await formProvider.updateForm(formId, formInfo);
            hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'FormAbility updateForm success.');
          } catch (error) {
            hilog.error(DOMAIN_NUMBER, TAG, `FormAbility updateForm failed: ${JSON.stringify(error)}`);
          }
        } catch (err) {
          hilog.error(DOMAIN_NUMBER, TAG, "write data to file failed with error message: " + err.message + ", error code: " + err.code);
        } finally {
          fileIo.closeSync(imgFile);
        };
      } catch (e) {
        hilog.error(DOMAIN_NUMBER, TAG, `openSync failed: ${JSON.stringify(e as BusinessError)}`);
      }

    } else {
      hilog.error(DOMAIN_NUMBER, TAG, `ArkTSCard download task failed`);
      let param: Record<string, string> = {
        'text': '刷新失败'
      };
      let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
      formProvider.updateForm(formId, formInfo);
    }
    httpRequest.destroy();
  }
}

4. 在卡片页面通过backgroundImage属性展示EntryFormAbility传递过来的卡片内容。

let storageWidgetImageUpdate = new LocalStorage();

@Entry(storageWidgetImageUpdate)
@Component
struct WidgetImageUpdateCard {
  @LocalStorageProp('text') text: ResourceStr = $r('app.string.loading');
  @LocalStorageProp('loaded') loaded: boolean = false;
  @LocalStorageProp('imgName') imgName: ResourceStr = $r('app.string.imgName');

  build() {
    Column() {
      Column() {
        Text(this.text)
          .fontColor('#FFFFFF')
          .opacity(0.9)
          .fontSize(12)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .maxLines(1)
          .margin({ top: '8%', left: '10%' })
      }.width('100%').height('50%')
      .alignItems(HorizontalAlign.Start)

      Row() {
        Button() {
          Text($r('app.string.update'))
            .fontColor('#45A6F4')
            .fontSize(12)
        }
        .width(120)
        .height(32)
        .margin({ top: '30%', bottom: '10%' })
        .backgroundColor('#FFFFFF')
        .borderRadius(16)
        .onClick(() => {
          postCardAction(this, {
            action: 'message',
            params: {
              info: 'refreshImage'
            }
          });
        })
      }.width('100%').height('40%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%').height('100%')
    .backgroundImage(this.loaded ? 'memory://' + this.imgName : $r('app.media.ImageDisp'))
    .backgroundImageSize(ImageSize.Cover)
  }
}

说明

  • Image组件通过入参(memory://fileName)中的(memory://)标识来进行远端内存图片显示,其中fileName需要和EntryFormAbility传递对象('formImages': {key: fd})中的key相对应。

  • Image组件通过传入的参数是否有变化来决定是否刷新图片,因此EntryFormAbility每次传递过来的imgName都需要不同,连续传递两个相同的imgName时,图片不会刷新。

  • 在卡片上展示的图片,大小需要控制在2MB以内。

 根据卡片状态刷新不同内容

相同的卡片可以添加到桌面上实现不同的功能,比如添加两张桌面的卡片,一张显示杭州的天气,一张显示北京的天气,设置每天早上7点触发定时刷新,卡片需要感知当前的配置是杭州还是北京,然后将对应城市的天气信息刷新到卡片上,以下示例介绍了如何根据卡片的状态动态选择需要刷新的内容。

卡片配置文件:配置每30分钟自动刷新。

{
  "forms": [
    {
      "name": "WidgetUpdateByStatus",
      "description": "$string:UpdateByStatusFormAbility_desc",
      "src": "./ets/widgetupdatebystatus/pages/WidgetUpdateByStatusCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "2*2",
      "supportDimensions": [
        "2*2"
      ]
    }
  ]
}

卡片页面:卡片具备不同的状态选择,在不同的状态下需要刷新不同的内容,因此在状态发生变化时通过postCardAction通知EntryFormAbility。

let storageUpdateByStatus = new LocalStorage();

@Entry(storageUpdateByStatus)
@Component
struct WidgetUpdateByStatusCard {
  @LocalStorageProp('textA') textA: Resource = $r('app.string.to_be_refreshed');
  @LocalStorageProp('textB') textB: Resource = $r('app.string.to_be_refreshed');
  @State selectA: boolean = false;
  @State selectB: boolean = false;

  build() {
    Column() {
      Column() {
        Row() {
          Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
            .padding(0)
            .select(false)
            .margin({ left: 26 })
            .onChange((value: boolean) => {
              this.selectA = value;
              postCardAction(this, {
                action: 'message',
                params: {
                  selectA: JSON.stringify(value)
                }
              });
            })
          Text($r('app.string.status_a'))
            .fontColor('#000000')
            .opacity(0.9)
            .fontSize(14)
            .margin({ left: 8 })
        }
        .width('100%')
        .padding(0)
        .justifyContent(FlexAlign.Start)

        Row() {
          Checkbox({ name: 'checkbox2', group: 'checkboxGroup' })
            .padding(0)
            .select(false)
            .margin({ left: 26 })
            .onChange((value: boolean) => {
              this.selectB = value;
              postCardAction(this, {
                action: 'message',
                params: {
                  selectB: JSON.stringify(value)
                }
              });
            })
          Text($r('app.string.status_b'))
            .fontColor('#000000')
            .opacity(0.9)
            .fontSize(14)
            .margin({ left: 8 })
        }
        .width('100%')
        .position({ y: 32 })
        .padding(0)
        .justifyContent(FlexAlign.Start)
      }
      .position({ y: 12 })

      Column() {
        Row() { // 选中状态A才会进行刷新的内容
          Text($r('app.string.status_a'))
            .fontColor('#000000')
            .opacity(0.4)
            .fontSize(12)

          Text(this.textA)
            .fontColor('#000000')
            .opacity(0.4)
            .fontSize(12)
        }
        .margin({ top: '12px', left: 26, right: '26px' })

        Row() { // 选中状态B才会进行刷新的内容
          Text($r('app.string.status_b'))
            .fontColor('#000000')
            .opacity(0.4)
            .fontSize(12)
          Text(this.textB)
            .fontColor('#000000')
            .opacity(0.4)
            .fontSize(12)
        }.margin({ top: '12px', bottom: '21px', left: 26, right: '26px' })
      }
      .margin({ top: 80 })
      .width('100%')
      .alignItems(HorizontalAlign.Start)
    }.width('100%').height('100%')
    .backgroundImage($r('app.media.CardUpdateByStatus'))
    .backgroundImageSize(ImageSize.Cover)
  }
}
  • EntryFormAbility:将卡片的状态存储在本地数据库中,在刷新事件回调触发时,通过formId获取当前卡片的状态,然后根据卡片的状态选择不同的刷新内容。
    import { Want } from '@kit.AbilityKit';
    import { preferences } from '@kit.ArkData';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    
    const TAG: string = 'UpdateByStatusFormAbility';
    const DOMAIN_NUMBER: number = 0xFF00;
    
    export default class UpdateByStatusFormAbility extends FormExtensionAbility {
      onAddForm(want: Want): formBindingData.FormBindingData {
        let formId: string = '';
        let isTempCard: boolean;
        if (want.parameters) {
          formId = want.parameters[formInfo.FormParam.IDENTITY_KEY].toString();
          isTempCard = want.parameters[formInfo.FormParam.TEMPORARY_KEY] as boolean;
          if (isTempCard === false) { // 如果为常态卡片,直接进行信息持久化
            hilog.info(DOMAIN_NUMBER, TAG, 'Not temp card, init db for:' + formId);
            let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
            promise.then(async (storeDB: preferences.Preferences) => {
              hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
              await storeDB.put('A' + formId, 'false');
              await storeDB.put('B' + formId, 'false');
              await storeDB.flush();
            }).catch((err: BusinessError) => {
              hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
            });
          }
      }
        let formData: Record<string, Object | string> = {};
        return formBindingData.createFormBindingData(formData);
      }
    
      onRemoveForm(formId: string): void {
        hilog.info(DOMAIN_NUMBER, TAG, 'onRemoveForm, formId:' + formId);
        let promise = preferences.getPreferences(this.context, 'myStore');
        promise.then(async (storeDB) => {
          hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
          await storeDB.delete('A' + formId);
          await storeDB.delete('B' + formId);
        }).catch((err: BusinessError) => {
        hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
        });
      }
    
      // 如果在添加时为临时卡片,则建议转为常态卡片时进行信息持久化
      onCastToNormalForm(formId: string): void {
        hilog.info(DOMAIN_NUMBER, TAG, 'onCastToNormalForm, formId:' + formId);
        let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
        promise.then(async (storeDB: preferences.Preferences) => {
          hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
          await storeDB.put('A' + formId, 'false');
          await storeDB.put('B' + formId, 'false');
          await storeDB.flush();
        }).catch((err: BusinessError) => {
        hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
        });
      }
    
      onUpdateForm(formId: string): void {
        let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
        promise.then(async (storeDB: preferences.Preferences) => {
          hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences from onUpdateForm.');
          let stateA = await storeDB.get('A' + formId, 'false');
          let stateB = await storeDB.get('B' + formId, 'false');
          // A状态选中则更新textA
          if (stateA === 'true') {
            let param: Record<string, string> = {
              'textA': 'AAA'
            };
            let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
            await formProvider.updateForm(formId, formInfo);
          }
          // B状态选中则更新textB
          if (stateB === 'true') {
            let param: Record<string, string> = {
              'textB': 'BBB'
            };
            let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
          await formProvider.updateForm(formId, formInfo);
          }
          hilog.info(DOMAIN_NUMBER, TAG, `Update form success stateA:${stateA} stateB:${stateB}.`);
        }).catch((err: BusinessError) => {
          hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
        });
      }
    
      onFormEvent(formId: string, message: string): void {
        // 存放卡片状态
        hilog.info(DOMAIN_NUMBER, TAG, 'onFormEvent formId:' + formId + 'msg:' + message);
        let promise: Promise<preferences.Preferences> = preferences.getPreferences(this.context, 'myStore');
        promise.then(async (storeDB: preferences.Preferences) => {
          hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded to get preferences.');
          let msg: Record<string, string> = JSON.parse(message);
          if (msg.selectA !== undefined) {
            hilog.info(DOMAIN_NUMBER, TAG, 'onFormEvent selectA info:' + msg.selectA);
            await storeDB.put('A' + formId, msg.selectA);
          }
          if (msg.selectB !== undefined) {
            hilog.info(DOMAIN_NUMBER, TAG, 'onFormEvent selectB info:' + msg.selectB);
            await storeDB.put('B' + formId, msg.selectB);
          }
          await storeDB.flush();
        }).catch((err: BusinessError) => {
          hilog.info(DOMAIN_NUMBER, TAG, `Failed to get preferences. ${JSON.stringify(err)}`);
        });
      }
    }

    说明

    通过本地数据库进行卡片信息的持久化时,建议先在onAddForm生命周期中通过TEMPORARY_KEY判断当前添加的卡片是否为常态卡片:如果是常态卡片,则直接进行卡片信息持久化;如果为临时卡片,则可以在卡片转为常态卡片(onCastToNormalForm)时进行持久化;同时需要在卡片销毁(onRemoveForm)时删除当前卡片存储的持久化信息,避免反复添加删除卡片导致数据库文件持续变大。

卡片生命周期拓展--@ohos.app.form.FormExtensionAbility (FormExtensionAbility)

FormExtensionAbility为卡片扩展模块,提供卡片创建、销毁、刷新等生命周期回调。

说明

本模块首批接口从API version 9开始支持。后续版本的新增接口,采用上角标单独标记接口的起始版本。

如下模块不支持在FormExtensionAbility引用,可能会导致程序异常退出。

  • @ohos.ability.particleAbility (ParticleAbility模块)
  • @ohos.multimedia.audio (音频管理)
  • @ohos.multimedia.camera (相机管理)
  • @ohos.multimedia.media (媒体服务)
  • @ohos.resourceschedule.backgroundTaskManager (后台任务管理)

属性

模型约束: 此接口仅可在Stage模型下使用。

系统能力: SystemCapability.Ability.Form

名称类型可读可写说明
contextFormExtensionContext

FormExtensionAbility的上下文环境,继承自ExtensionContext。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

onAddForm

onAddForm(want: Want): formBindingData.FormBindingData

卡片提供方接收创建卡片的通知接口。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
wantWant当前卡片相关的Want类型信息,包括卡片ID、卡片名称、卡片样式等。这些卡片信息必须作为持久数据进行管理,以便后续更新和删除卡片。

返回值:

类型说明
formBindingData.FormBindingDataformBindingData.FormBindingData对象,卡片要显示的数据。

示例:

import { formBindingData, FormExtensionAbility } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
  /**
   * 卡片提供方接收创建卡片的通知接口。
   * @param want 请求创建卡片的能力。
   * want表示获取卡片状态的描述。描述包括Bundle名称、能力名称、模块名称、卡片名和卡片维度
   * @return formBindingData.FormBindingData对象,卡片要显示的数据。
   * */
  onAddForm(want: Want) {
    console.log(`${TAG} onAddForm, want: ${want.abilityName}`);
    let dataObj1: Record<string, string> = {
      'temperature': '11c',
      'time': '11:00'
    };
    let obj1: formBindingData.FormBindingData = formBindingData.createFormBindingData(dataObj1);
    return obj1;
  }
}

onCastToNormalForm

onCastToNormalForm(formId: string): void

卡片提供方接收临时卡片转常态卡片的通知接口。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
formIdstring请求转换为常态的卡片标识。

示例:

import { FormExtensionAbility } from '@kit.FormKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
/**
 * 卡片提供方接收临时卡片转常态卡片的通知接口。
 * @param formId 请求转换为常态的卡片标识。
 * @return 无返回值。
 * */
onCastToNormalForm(formId: string) {
  console.log(`${TAG} onCastToNormalForm, formId: ${formId}`);
}
};

onUpdateForm

onUpdateForm(formId: string, wantParams?: Record<string, Object>): void

卡片提供方接收携带参数的更新卡片的通知接口。获取最新数据后调用formProvider的updateForm接口刷新卡片数据。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
formIdstring请求更新的卡片ID。
wantParams12+Record<string, Object>更新参数。

示例:

import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
/**
   *卡片提供方接收携带参数的更新卡片的通知接口。获取最新数据后调用formProvider的updateForm接口刷新卡片数据。
   * @param formId 卡片标识,用于更新卡片数据。
   * @param wantParams 携带的参数。
   * */
  onUpdateForm(formId: string, wantParams?: Record<string, Object>) {
    console.log(`${TAG} onUpdateForm, formId: ${formId},
        wantPara: ${wantParams?.['ohos.extra.param.key.host_bg_inverse_color']}`);
    let param: Record<string, string> = {
      'temperature': '22c',
      'time': '22:00'
    }
    let obj2: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);
    formProvider.updateForm(formId, obj2).then(() => {
      console.log(`${TAG} context updateForm`);
    }).catch((error: BusinessError) => {
      console.error(`${TAG} context updateForm failed, data: ${error}`);
    });
  }
};

onChangeFormVisibility

onChangeFormVisibility(newStatus: Record<string, number>): void

卡片提供方接收修改可见性的通知接口。

该接口仅对系统应用生效,且需要将formVisibleNotify配置为true。

模型约束: 此接口仅可在Stage模型下使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
newStatusRecord<string, number>请求修改的卡片标识和可见状态。

示例:

import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

// ArkTS规范中ets文件无法使用Object.keys和for..in...获取Object的key值,请使用自定义函数getObjKeys代替。
// 使用时请将此函数单独抽离至一个ts文件中并导出,在需要用到的ets文件中导入此函数后使用。
function getObjKeys(obj: Object): string[] {
  let keys = Object.keys(obj);
  return keys;
}

export default class MyFormExtensionAbility extends FormExtensionAbility {
/**
   * 卡片提供方接收修改可见性的通知接口。
   * 该接口仅对系统应用生效,且需要将formVisibleNotify配置为true。
   * @param newStatus 卡片可见性变化的状态。
   * */
  nChangeFormVisibility(newStatus: Record<string, number>) {
    console.log(`${TAG} onChangeFormVisibility, newStatus: ${newStatus}`);
    let param: Record<string, string> = {
      'temperature': '22c',
      'time': '22:00'
    }
    let obj2: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);

    let keys: string[] = getObjKeys(newStatus);

    for (let i: number = 0; i < keys.length; i++) {
      console.log(`${TAG} onChangeFormVisibility, key: ${keys[i]}, value= ${newStatus[keys[i]]}`);
      formProvider.updateForm(keys[i], obj2).then(() => {
        console.log(`${TAG} context updateForm`);
      }).catch((error: BusinessError) => {
        console.error(`Operation updateForm failed. Cause: ${JSON.stringify(error)}`);
      });
    }
  }
};

onFormEvent

onFormEvent(formId: string, message: string): void

卡片提供方接收处理卡片事件的通知接口。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
formIdstring请求触发事件的卡片标识。
messagestring事件消息。

示例:

import { FormExtensionAbility } from '@kit.FormKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
/**
   * 卡片提供方接收处理卡片事件的通知接口。
   * @param formId 卡片标识。
   * @param message 卡片事件的消息内容。
   * */
  onFormEvent(formId: string, message: string) {
    console.log(`${TAG} onFormEvent, formId: ${formId}, message: ${message}`);
  }
};

onRemoveForm

onRemoveForm(formId: string): void

卡片提供方接收销毁卡片的通知接口。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
formIdstring请求销毁的卡片标识。

示例:

import { FormExtensionAbility } from '@kit.FormKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
 /**
   * 卡片提供方接收卡片被移除的通知接口。
   * @param formId 卡片标识。
   * */
  onRemoveForm(formId: string) {
    console.log(`${TAG} onRemoveForm, formId: ${formId}`);
  }
};

onConfigurationUpdate

onConfigurationUpdate(newConfig: Configuration): void

当系统配置更新时调用。

仅当前formExtensionAbility存活时更新配置才会触发此生命周期。需要注意:formExtensionAbility创建后10秒内无操作将会被清理。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
newConfigConfiguration表示需要更新的配置信息。

示例:

import { FormExtensionAbility } from '@kit.FormKit';
import { Configuration } from '@kit.AbilityKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
  /**
   * 卡片提供方接收配置更新的通知接口。
   * @param newConfig 新的配置。
   * */
  onConfigurationUpdate(newConfig: Configuration) {
    // 仅当前formExtensionAbility存活时更新配置才会触发此生命周期。
    // 需要注意:formExtensionAbility创建后10秒内无操作将会被清理。
    console.log(`${TAG} onConfigurationUpdate, config: ${JSON.stringify(newConfig)}`);
  }
};

onAcquireFormState

onAcquireFormState?(want: Want): formInfo.FormState

卡片提供方接收查询卡片状态通知接口,默认返回卡片初始状态(该方法可以选择性重写)。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

参数:

参数名类型必填说明
wantWantwant表示获取卡片状态的描述。描述包括Bundle名称、能力名称、模块名称、卡片名和卡片维度。

示例:

import { FormExtensionAbility, formInfo } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
  /**
   * 卡片提供方接收卡片状态查询的通知接口。
   * @param want 查询卡片状态的能力。
   * want表示获取卡片状态的描述。描述包括Bundle名称、能力名称、模块名称、卡片名和卡片维度
   * */
  onAcquireFormState(want: Want) {
    console.log(`${TAG} onAcquireFormState, want: ${want}`);
    return formInfo.FormState.UNKNOWN;
  }
};

onStop

onStop?(): void

当卡片提供方的卡片进程退出时,触发该回调。

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API version 12开始,该接口支持在元服务中使用。

系统能力: SystemCapability.Ability.Form

示例:

import { FormExtensionAbility } from '@kit.FormKit';

// 日志标签。方便查看日志。
const TAG: string = 'EntryFormAbility';

export default class MyFormExtensionAbility extends FormExtensionAbility {
  /**
   * 当卡片提供方的卡片进程退出时,触发该回调。
   * */
  onStop() {
    console.log(`${TAG} onStop`);
  }
}

绘制卡片页面拓展--使用 DevEco CodeGenie 绘制卡片样式

 

这里预览失败了,可能是我电脑卡住了,但是没事,这里有代码 ,可以点击</>或者保存到工程

代码报错了不着急,因为那些图片我们没有,只能自己准备图片了 

如果它能把图片一起打包过来就完美了

效果图,说实话,随便找的图片所以看着有点丑,但是把确实能用 

静态卡片能力拓展 

供静态卡片交互组件,用于静态卡片内部和提供方应用间的交互,当前支持router、message和call三种类型的事件。

说明

  • 该组件从API Version 10开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。

  • 该组件仅可以在静态卡片中使用。

权限

子组件

支持单个子组件

接口

FormLink(options: FormLinkOptions)

卡片能力: 从API version 10开始,该接口支持在ArkTS卡片中使用。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.ArkUI.ArkUI.Full

参数:

参数名类型必填说明
optionsFormLinkOptions定义卡片信息

FormLinkOptions对象说明

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.ArkUI.ArkUI.Full

名称类型必填说明
actionstring

action的类型,支持三种预定义的类型:

- router:跳转到提供方应用的指定UIAbility。

- message:自定义消息,触发后会调用提供方FormExtensionAbility的onFormEvent()生命周期回调。

- call:后台启动提供方应用。触发后会拉起提供方应用的指定UIAbility(仅支持launchType为singleton的UIAbility,即启动模式为单实例的UIAbility),但不会调度到前台。提供方应用需要具备后台运行权限(ohos.permission.KEEP_BACKGROUND_RUNNING)。

说明:

不推荐使用router事件刷新卡片UI。

卡片能力: 从API version 10开始,该接口支持在ArkTS卡片中使用。

moduleNamestring

action为router / call 类型时跳转的模块名。

卡片能力: 从API version 10开始,该接口支持在ArkTS卡片中使用。

bundleNamestring

action为router / call 类型时跳转的包名。

卡片能力: 从API version 10开始,该接口支持在ArkTS卡片中使用。

abilityNamestring

action为router / call 类型时跳转的UIAbility名。

卡片能力: 从API version 10开始,该接口支持在ArkTS卡片中使用。

uri11+string

action为router 类型时跳转的UIAbility的统一资源标识符。uri和abilityName同时存在时,abilityName优先。

卡片能力: 从API version 11开始,该接口支持在ArkTS卡片中使用。

paramsObject

当前action携带的额外参数,内容使用JSON格式的键值对形式。call 类型时需填入参数'method',且类型需要为string类型,用于触发UIAbility中对应的方法。

说明:

不推荐通过params传递卡片内部的状态变量。

卡片能力: 从API version 10开始,该接口支持在ArkTS卡片中使用。

属性

支持通用属性

事件

不支持通用事件

示例 

@Entry
@Component
struct FormLinkDemo {
  build() {
    Column() {
      Text("这是一个静态卡片").fontSize(20).margin(10)

      // router事件用于静态卡片跳转到对应的UIAbility
      FormLink({
        action: "router",
        abilityName: "EntryAbility",
        params: {
          'message': 'testForRouter' // 自定义要发送的message
        }
      }) {
        Button("router event").width(120)
      }.margin(10)



      // message事件触发FormExtensionAbility的onFormEvent生命周期
      FormLink({
        action: "message",
        abilityName: "EntryAbility",
        params: {
          'message': 'messageEvent' // 自定义要发送的message
        }
      }) {
        Button("message event").width(120)
      }.margin(10)



      // call事件用于触发UIAbility中对应的方法
      FormLink({
        action: "call",
        abilityName: "EntryAbility",
        params: {
          'method': 'funA', // 在EntryAbility中调用的方法名
          'num': 1 // 需要传递的其他参数
        }
      }) {
        Button("call event").width(120)
      }.margin(10)

      // router事件用于静态卡片deeplink跳转到对应的UIAbility
      FormLink({
        action: "router",
        uri: 'example://uri.ohos.com/link_page',
        params: {
          message: 'router msg for static uri deeplink' // 自定义要发送的message
        }
      }) {
        Button("deeplink event").width(120)
      }.margin(10)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%').height('100%')
  }
}

 

待跳转应用 module.json5 uris 配置示例: 

"abilities": [
  {
    "skills": [
      {
        "uris": [
          {
            "scheme": "example",
            "host": "uri.ohos.com",
            "path": "link_page"
          },
        ]
      }
    ],
  }
]


http://www.kler.cn/a/398894.html

相关文章:

  • 常用命令之LinuxOracleHivePython
  • 微服务即时通讯系统的实现(客户端)----(3)
  • python 多进程,程序运行越来越慢踩坑
  • 问:说说SpringDAO及ORM的用法?
  • 消息队列原理面试题及参考答案
  • Vue3 模板语法
  • uni-app快速入门(八)--常用内置组件(上)
  • 人机界面中的数据、信息、知识、算法分层
  • UE5遇到问题记录—在sequence制作时如何让角色隐藏/显示?
  • 数据结构_图的遍历
  • springboot整合elasticsearch,并使用docker desktop运行elasticsearch镜像容器遇到的问题。
  • 游戏引擎学习第14天
  • B-树介绍
  • 深入Linux基础:文件系统与进程管理详解
  • OpenSSL 自签名
  • 3D数据格式转换工具HOOPS Exchange如何在读取CAD文件时处理镶嵌数据?
  • java数据类型之间的转换|超详解
  • 腾讯云轻量应用服务器部署私有笔记,立省365元
  • spring boot接收参数
  • 大数据挖掘
  • Javamail发送Excel附件具体实现
  • 【c++笔试强训】(第十一篇)
  • 在CentOS中,通过nginx访问php
  • Win10/11 安装使用 Neo4j Community Edition
  • Linux从入门到精通
  • vue el-table 超出隐藏移入弹窗显示