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

【鸿蒙开发】第四十章 Form Kit(卡片开发服务)

目录

1 概述

1.1 卡片使用场景

1.2 服务卡片架构

1.3 亮点/特征

1.4 开发模式

1.5 与相关Kit的关系

1.6 约束限制

2 ArkTS卡片运行机制

2.1 实现原理

2.2 ArkTS卡片的优势

2.3 ArkTS卡片的约束

3 ArkTS卡片相关模块 

4 ArkTS卡片开发指导

4.1 创建一个ArkTS卡片

 4.2 配置卡片的配置文件

isDynamic标签

window标签

4.3 卡片生命周期管理

4.4 开发卡片页面

4.4.1 卡片页面能力说明

4.4.2 卡片使用动效能力

4.4.3 卡片使用自定义绘制能力

4.5 开发卡片事件 

4.5.1 卡片事件能力说明

动态卡片事件能力说明

 静态卡片事件能力说明

 4.5.2 拉起卡片提供方的UIAbility(router事件)

开发步骤

 4.5.3 拉起卡片提供方的UIAbility到后台(call事件)

开发步骤

4.5.4 通过message事件刷新卡片内容

4.5.5 通过router或call事件刷新卡片内容 

通过router事件刷新卡片内容

 通过call事件刷新卡片内容

4.6 卡片数据交互

4.6.1 卡片内容更新

4.6.2 卡片定时刷新

4.6.3 卡片定点刷新 

4.6.4 刷新本地图片和网络图片 

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

 


1 概述

Form Kit(卡片开发框架)提供了一种在桌面、锁屏等系统入口嵌入显示应用信息的开发框架和API,可以将应用内用户关注的重要信息或常用操作抽取到服务卡片(以下简称“卡片”)上,通过将卡片添加到桌面上,以达到信息展示、服务直达的便捷体验效果。

1.1 卡片使用场景

  • 支持设备类型:卡片可以在手机、平板等设备上使用。
  • 支持开发卡片应用类型:应用和元服务内均支持开发卡片。
  • 支持卡片使用位置:用户可以在桌面、锁屏等系统应用上添加使用,暂不支持在普通应用内嵌入显示卡片。

1.2 服务卡片架构

图1 服务卡片架构

卡片场景中涉及到的基本概念

  • 卡片使用方:如上图中的桌面,作为显示卡片内容的宿主应用,用于与用户直接进行交互,完成卡片添加、删除、显示功能,并能控制卡片在宿主中具体展示的位置。

  • 卡片提供方:提供卡片的应用或元服务,是卡片功能的具体实现者,需要设计实现卡片UI、数据更新、以及点击交互处理功能。

  • 卡片管理服务:操作系统内管理整机卡片信息的系统服务,作为卡片提供方和使用方的桥梁,向使用方提供卡片信息查询、添加、删除等能力,同时向提供方提供卡片被添加、被删除、刷新、点击事件等通知能力。

卡片的常见使用步骤如下:

图2 卡片常见使用步骤

  1. 长按“桌面图标”,弹出操作菜单。
  2. 点击“卡片”选项,进入卡片预览界面。
  3. 点击“添加到桌面”按钮,即可在桌面上看到新添加的卡片。

1.3 亮点/特征

  • 信息呈现:将应用/元服务的重要信息以卡片形式展示在桌面,同时支持信息定时更新能力,用户可以随时查看关注的信息。

  • 服务直达:通过点击卡片内按钮,就可以实现功能快捷操作,也支持点击后跳转到应用/元服务对应功能页,实现功能服务一步直达的效果。

1.4 开发模式

应用运行模式选择

当前系统中应用开发模型支持Stage和FA两种方式,所以Form Kit也同时支持开发者使用Stage模型和FA模型来开发卡片应用,但更推荐使用Stage模型。

UI开发范式选择

  • Stage模型支持两种卡片UI开发方式,可以基于声明式范式ArkTS语言开发卡片(简称ArkTS卡片)、也可以基于类Web范式JS语言开发卡片(简称JS卡片)。
  • FA模型仅支持基于类Web范式JS语言开发JS卡片。

ArkTS卡片与JS卡片具备不同的实现原理及特征,在场景能力上的差异如下表所示:

类别JS卡片ArkTS卡片
开发范式类Web范式声明式范式
组件能力支持支持
布局能力支持支持
事件能力支持支持
自定义动效不支持支持
自定义绘制不支持支持
逻辑代码执行不支持支持

1.5 与相关Kit的关系

  • Ability Kit: Form Kit内部实现依赖Ability Kit提供的Extension基础能力,与Ability Kit存在生命周期调度交互。
  • ArkUI: Form Kit卡片提供方在卡片页面中可以使用ArkUI提供的部分组件、事件、动效、状态管理等能力。

1.6 约束限制

UI能力约束

卡片尺寸大小有限,适合承载简洁明了的信息和交互操作,所以卡片开发中支持使用的UI控件范围、动效能力会有一定限制。

运动能力约束

卡片能为应用和元服务提供在桌面等入口常驻的信息显示和更新的能力,系统对于更新频次和卡片后台运行能力范围会有一定的限制。

2 ArkTS卡片运行机制

2.1 实现原理

图1 ArkTS卡片实现原理

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

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

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

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

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

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

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

2.2 ArkTS卡片的优势

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

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

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

    图3 卡片工程结构对比

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

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

2.3 ArkTS卡片的约束

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

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

  • 不支持导入共享包。

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

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

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

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

  • 暂不支持极速预览。

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

  • 暂不支持Hot Reload热重载。

  • 暂不支持setTimeOut。

3 ArkTS卡片相关模块 

图1 ArkTS卡片相关模块

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

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

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

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

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

  • 页面布局(WidgetCard.ets):基于ArkUI提供卡片UI开发能力。

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

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

4 ArkTS卡片开发指导

4.1 创建一个ArkTS卡片

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

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

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

  1. 右键新建卡片。

    说明

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

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

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

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

 4.2 配置卡片的配置文件

卡片相关的配置文件主要包含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. 

  1. 卡片的具体配置信息。在上述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。
    fontScaleFollowSystem

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

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

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

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

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

    - rect:表示方形卡片。

    - circle:表示圆形卡片。

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

isDynamic标签

此标签标识卡片是否为动态卡片(仅针对ArkTS卡片生效)。

卡片类型支持的能力适用场景优缺点
静态卡片仅支持UI组件和布局能力。主要用于展示静态信息(UI相对固定),仅可以通过FormLink组件跳转到指定的UIAbility。功能简单但可以有效控制内存开销。
动态卡片除了支持UI组件和布局能力,还支持通用事件能力和自定义动效能力。用于有复杂业务逻辑和交互的场景。例如:卡片页面图片的刷新、卡片内容的刷新等。功能丰富但内存开销较大。

window标签

此标签标识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","isDefault": true,"updateEnabled": true,"scheduledUpdateTime": "10:30","updateDuration": 1,"defaultDimension": "2*2","supportDimensions": ["2*2"],"formConfigAbility": "ability://EntryAbility","dataProxyEnabled": false,"isDynamic": true,"transparencyEnabled": false,"metadata": []}]
}

4.3 卡片生命周期管理

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

1. 在EntryFormAbility.ets中,导入相关模块。

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

2. 在EntryFormAbility.ets中,实现FormExtensionAbility生命周期接口,其中在onAddForm的入参want中可以通过FormParam取出卡片的相关信息。

const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;export default class EntryFormAbility extends FormExtensionAbility {onAddForm(want: Want): formBindingData.FormBindingData {hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onAddForm');hilog.info(DOMAIN_NUMBER, TAG, want.parameters?.[formInfo.FormParam.NAME_KEY] as string);// ...// 卡片使用方创建卡片时触发,提供方需要返回卡片数据绑定类let obj: Record<string, string> = {'title': 'titleOnAddForm','detail': 'detailOnAddForm'};let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);return formData;}onCastToNormalForm(formId: string): void {// 卡片使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理。// 1、临时卡、常态卡是卡片使用方的概念。// 2、临时卡是短期存在的,在特定事件或用户行为后显示,完成后自动消失。// 3、常态卡是持久存在的,在用户未进行清除或更改的情况下,会一直存在,平时开发的功能卡片属于常态卡。// 4、目前手机上没有地方会使用临时卡。hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onCastToNormalForm');}onUpdateForm(formId: string): void {// 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onUpdateForm');let obj: Record<string, string> = {'title': 'titleOnUpdateForm','detail': 'detailOnUpdateForm'};let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);formProvider.updateForm(formId, formData).catch((error: BusinessError) => {hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] updateForm, error:' + JSON.stringify(error));});}onChangeFormVisibility(newStatus: Record<string, number>): void {// 卡片使用方发起可见或者不可见通知触发,提供方需要做相应的处理,仅系统应用生效hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onChangeFormVisibility');}onFormEvent(formId: string, message: string): void {// 若卡片支持触发事件,则需要重写该方法并实现对事件的触发hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onFormEvent');// ...}onRemoveForm(formId: string): void {// 删除卡片实例数据hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onRemoveForm');// 删除之前持久化的卡片实例数据// 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例}onConfigurationUpdate(config: Configuration) {// 当前formExtensionAbility存活时更新系统配置信息时触发的回调。// 需注意:formExtensionAbility创建后10秒内无操作将会被清理。hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onConfigurationUpdate:' + JSON.stringify(config));}onAcquireFormState(want: Want) {// 卡片提供方接收查询卡片状态通知接口,默认返回卡片初始状态。return formInfo.FormState.READY;}
}

说明

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

4.4 开发卡片页面

4.4.1 卡片页面能力说明

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

对于支持在ArkTS卡片中使用的接口,会添加“卡片能力”的标记:从API version x开始,该接口支持在ArkTS卡片中使用。同时请留意卡片场景下的能力差异说明。

例如:以下说明表示CircleShape可在ArkTS卡片中使用。

4.4.2 卡片使用动效能力

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

表1 动效参数限制

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

说明

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

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

@Entry
@Component
struct AnimationCard {@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)}
}

4.4.3 卡片使用自定义绘制能力

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

@Entry
@Component
struct CustomCanvasDrawingCard {private canvasWidth: number = 0;private canvasHeight: number = 0;// 初始化CanvasRenderingContext2D和RenderingContextSettingsprivate 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 = '#5A5FFF';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.strokeStyle = '#FFFFFF';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.strokeStyle = '#FFFFFF';this.context.lineWidth = 15;this.context.stroke();// 绘制笑脸的鼻子let startX = circleX;let startY = circleY - 20;this.context.beginPath();this.context.moveTo(startX, startY);this.context.lineTo(startX - 8, startY + 40);this.context.lineTo(startX + 8, startY + 40);this.context.strokeStyle = '#FFFFFF';this.context.lineWidth = 15;this.context.lineCap = 'round';this.context.lineJoin = 'round';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 = '#FFFFFF';this.context.lineWidth = 15;this.context.stroke();this.context.closePath();})}}.height('100%').width('100%')}
}

运行效果如下图所示。

4.5 开发卡片事件 

4.5.1 卡片事件能力说明

动态卡片事件能力说明

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

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

请参见FormLink

 4.5.2 拉起卡片提供方的UIAbility(router事件)

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

开发步骤
  1. 创建动态卡片

    在工程的 entry 模块中,新建名为WidgetEventRouterCard的ArkTs卡片。

  2. 构建ArkTs卡片页面代码布局

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

//src/main/ets/widgeteventroutercard/pages/WidgetEventRouterCard.ets
@Entry
@Component
struct WidgetEventRouterCard {build() {Column() {Text($r('app.string.JumpLabel')).fontColor('#FFFFFF').opacity(0.9).fontSize(14).margin({ top: '8%', left: '10%' })Row() {Column() {Button() {Text($r('app.string.ButtonA_label')).fontColor('#45A6F4').fontSize(12)}.width(120).height(32).margin({ top: '20%' }).backgroundColor('#FFFFFF').borderRadius(16).onClick(() => {postCardAction(this, {action: 'router',abilityName: 'EntryAbility',params: { targetPage: 'funA' }});})Button() {Text($r('app.string.ButtonB_label')).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: 'funB' }});})}}.width('100%').height('80%').justifyContent(FlexAlign.Center)}.width('100%').height('100%').alignItems(HorizontalAlign.Start).backgroundImage($r('app.media.CardEvent')).backgroundImageSize(ImageSize.Cover)}
}

 3. 处理router事件

在UIAbility中接收router事件并获取参数,根据传递的params不同,选择拉起不同的页面。

//src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';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 {// 获取router事件中传递的targetPage参数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);}}onWindowStageCreate(windowStage: window.WindowStage): void {// Main window is created, set main page for this abilitylet targetPage: string;// 根据传递的targetPage不同,选择拉起不同的页面switch (this.selectPage) {case 'funA':targetPage = 'pages/FunA'; //与实际的UIAbility页面路径保持一致break;case 'funB':targetPage = 'pages/FunB'; //与实际的UIAbility页面路径保持一致break;default:targetPage = 'pages/Index'; //与实际的UIAbility页面路径保持一致}if (this.currentWindowStage === null) {this.currentWindowStage = windowStage;}windowStage.loadContent(targetPage, (err, data) => {if (err.code) {hilog.error(DOMAIN_NUMBER, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');return;}hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');});}
}

4. 创建跳转后的UIAbility页面

在pages文件夹下新建FunA.ets和FunB.ets,构建页面布局。

//src/main/ets/pages/FunA.ets
@Entry
@Component
struct FunA {@State message: string = 'Hello World';build() {RelativeContainer() {Text(this.message).id('HelloWorld').fontSize(50).fontWeight(FontWeight.Bold).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },middle: { anchor: '__container__', align: HorizontalAlign.Center }})}.height('100%').width('100%')}
}

 5. 注册UIAbility页面

打开main_pages.json,将新建的FunA.ets和FunB.ets正确注册在src数组中。

//src/main/resources/base/profile/main_pages.json
{"src": ["pages/Index","pages/FunA","pages/FunB"]
}

 4.5.3 拉起卡片提供方的UIAbility到后台(call事件)

开发步骤
  1. 创建动态卡片

    新建一个名为WidgetEventCallCardArkTs动态卡片。

  2. 页面布局代码实现

    在卡片页面中布局两个按钮,点击其中一个按钮时调用postCardAction向指定UIAbility发送call事件,并在事件内定义需要调用的方法和传递的数据。需要注意的是,method参数为必选参数,且类型需要为string类型,用于触发UIAbility中对应的方法。

 //src/main/ets/widgeteventcallcard/pages/WidgetEventCallCardCard.ets@Entry@Componentstruct 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)}}

3. 创建指定的UIAbility

在UIAbility中接收call事件并获取参数,根据传递的method不同,执行不同的方法。其余数据可以通过readString方法获取。需要注意的是,UIAbility需要onCreate生命周期中监听所需的方法。

//src/main/ets/widgeteventcallcard/WidgetEventCallEntryAbility/WidgetEventCallEntryAbility.ets
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)}`);}}
}

 4. 配置后台运行权限

call事件含有约束限制:提供方应用需要在module.json5顶层对象module下添加后台运行权限(ohos.permission.KEEP_BACKGROUND_RUNNING)。

//src/main/module.json5
"requestPermissions":[{"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"}]

 5. 配置指定的UIAbility

在module.json5顶层对象module的abilities数组内添加WidgetEventCallEntryAbility对应的配置信息。

//src/main/module.json5
"abilities": [{"name": 'WidgetEventCallEntryAbility',"srcEntry": './ets/widgeteventcallcard/WidgetEventCallEntryAbility/WidgetEventCallEntryAbility.ets',"description": '$string:WidgetEventCallCard_desc',"icon": "$media:app_icon","label": "$string:WidgetEventCallCard_label","startWindowIcon": "$media:app_icon","startWindowBackground": "$color:start_window_background"}
]

4.5.4 通过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).fontColor('#FFFFFF').opacity(0.9).fontSize(14).margin({ top: '8%', left: '10%' })Text(this.detail).fontColor('#FFFFFF').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).backgroundImage($r('app.media.CardEvent')).backgroundImageSize(ImageSize.Cover)}
}
  •  在FormExtensionAbility的onFormEvent生命周期中调用updateForm接口刷新卡片。

import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;export default class EntryFormAbility 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)}`);class FormDataClass {title: string = 'Title Update.'; // 和卡片布局中对应detail: string = 'Description update success.'; // 和卡片布局中对应}let formData = new FormDataClass();let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(formData);formProvider.updateForm(formId, formInfo).then(() => {hilog.info(DOMAIN_NUMBER, TAG, 'FormAbility updateForm success.');}).catch((error: BusinessError) => {hilog.info(DOMAIN_NUMBER, TAG, `Operation updateForm failed. Cause: ${JSON.stringify(error)}`);})}//...
}

运行效果如下图所示。

初始状态点击刷新

4.5.5 通过router或call事件刷新卡片内容 

使用router事件,点击卡片可拉起对应应用的UIAbility至前台,并刷新卡片。使用call事件,点击卡片可拉起对应应用的UIAbility至后台,并刷新卡片。在卡片页面中可以通过postCardAction接口触发router事件或者call事件拉起UIAbility,然后由UIAbility刷新卡片内容。

通过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).fontColor('#FFFFFF').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: 'WidgetEventRouterEntryAbility', // 只能跳转到当前应用下的UIAbilityparams: {routerDetail: 'RouterFromCard',}});})}.width('100%').height('40%').justifyContent(FlexAlign.Center)}.width('100%').height('100%').alignItems(HorizontalAlign.Start).backgroundImage($r('app.media.CardEvent')).backgroundImageSize(ImageSize.Cover)}
}
  •  在UIAbility的onCreate或者onNewWant生命周期中可以通过入参want获取卡片的formID和传递过来的参数信息,然后调用updateForm接口刷新卡片。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, formInfo, formProvider } from '@kit.FormKit';
import { hilog } from '@kit.PerformanceAnalysisKit';const TAG: string = 'WidgetEventRouterEntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;export default class WidgetEventRouterEntryAbility extends UIAbility {onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {this.handleFormRouterEvent(want, 'onCreate');}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': message + ' ' + source + ' UIAbility', // 和卡片布局中对应};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已在后台运行,在收到Router事件后会触发onNewWant生命周期回调onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {hilog.info(DOMAIN_NUMBER, TAG, 'onNewWant Want:', JSON.stringify(want));this.handleFormRouterEvent(want, 'onNewWant');}onWindowStageCreate(windowStage: window.WindowStage): void {hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onWindowStageCreate');windowStage.loadContent('pages/Index', (err, data) => {if (err.code) {hilog.error(DOMAIN_NUMBER, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');return;}hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');});}// ...
}
 通过call事件刷新卡片内容
  • 在卡片页面代码文件中,通过注册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).fontColor('#FFFFFF').opacity(0.9).fontSize(14).margin({ top: '8%', left: '10%' })}.width('100%').height('50%').alignItems(HorizontalAlign.Start)Row() {Button() {Text($r('app.string.CalleeJumpLabel')).fontColor('#45A6F4').fontSize(12)}.width(120).height(32).margin({ top: '30%', bottom: '10%' }).backgroundColor('#FFFFFF').borderRadius(16).onClick(() => {postCardAction(this, {action: 'call',abilityName: 'WidgetCalleeEntryAbility', // 只能拉起当前应用下的UIAbilityparams: {method: 'funA',formId: this.formId,calleeDetail: 'CallFrom'}});})}.width('100%').height('40%').justifyContent(FlexAlign.Center)}.width('100%').height('100%').alignItems(HorizontalAlign.Start).backgroundImage($r('app.media.CardEvent')).backgroundImageSize(ImageSize.Cover)}
}
  •  在UIAbility的onCreate生命周期中监听call事件所需的方法,然后在对应方法中调用updateForm接口刷新卡片。

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { formBindingData, formProvider } from '@kit.FormKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';const TAG: string = 'WidgetCalleeEntryAbility';
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': message};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 WidgetCalleeEntryAbility extends UIAbility {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)}`);}}onWindowStageCreate(windowStage: window.WindowStage): void {// Main window is created, set main page for this abilityhilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onWindowStageCreate');windowStage.loadContent('pages/Index', (err, data) => {if (err.code) {hilog.error(DOMAIN_NUMBER, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');return;}hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');});}
}

要拉起UIAbility至后台,需要在module.json5配置文件中,配置ohos.permission.KEEP_BACKGROUND_RUNNING权限。

  "requestPermissions":[{"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"}]

4.6 卡片数据交互

4.6.1 卡片内容更新

ArkTS卡片框架为提供方提供了updateForm接口、为使用方提供了requestForm接口来实现主动触发卡片的页面刷新能力;另外卡片框架还会通过开发者声明的定时信息按需通知提供方进行卡片刷新。

卡片UI代码内通过LocalStorageProp可以获得提供方推送的需要刷新的卡片数据。

接口是否系统能力约束
updateForm

1. 提供方调用。

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

requestForm

1. 使用方调用。

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

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

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

2. 使用方主动请求更新卡片流程示意:

卡片使用方在运行过程中,如果检测到系统语言、深浅色有变化时,可以主动通过formHost提供的requestForm接口请求更新卡片,卡片管理服务会进而通知提供方完成卡片更新。

3. 卡片框架通知提供方定时更新卡片流程示意:

根据卡片提供方开发者提前配置声明的定时刷新信息,卡片管理服务会根据定时信息、卡片可见状态、刷新次数等因素综合判断是否需要通知提供方更新卡片。

4.6.2 卡片定时刷新

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

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

{"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"]}]
}

说明

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

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

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

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

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

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

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

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

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

  • 下次刷新:表示指定卡片的下一次刷新时间。可以通过调用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}`);}}// ... 
}

约束限制:

  1. 定时刷新有配额限制,每张卡片每天最多通过定时方式触发刷新50次,定时刷新次数包含卡片配置项updateDuration和调用setFormNextRefreshTime方法两种方式,当达到50次配额后,无法通过定时方式再次触发刷新,刷新次数会在每天的0点重置。
  2. 当前定时刷新使用同一个计时器进行计时,因此卡片定时刷新的第一次刷新会有最多30分钟的偏差。比如第一张卡片A(每隔半小时刷新一次)在3点20分添加成功,定时器启动并每隔半小时触发一次事件,第二张卡片B(每隔半小时刷新一次)在3点40分添加成功,在3点50分定时器事件触发时,卡片A触发定时刷新,卡片B会在下次事件(4点20分)中才会触发。
  3. 定时刷新在卡片可见情况下才会触发,在卡片不可见时仅会记录刷新动作和刷新数据,待可见时统一刷新布局。
  4. 如果使能了卡片代理刷新,定时刷新和下次刷新不生效。

4.6.3 卡片定点刷新 

  • 定点刷新:表示在每天的某个特定时间点自动刷新卡片内容。可以在form_config.json配置文件中的scheduledUpdateTime字段中进行设置。例如,可以将刷新时间设置为每天的上午10点30分。
{"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"]}]
}

约束限制:

  1. 定点刷新在卡片可见情况下才会触发,在卡片不可见时仅会记录刷新动作和刷新数据,待可见时统一刷新布局。

4.6.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.PNGlet tempDir = this.context.getApplicationContext().tempDir;hilog.info(DOMAIN_NUMBER, TAG, `tempDir: ${tempDir}`);let imgMap: Record<string, number> = {};try {// 打开本地图片并获取其打开后的fdlet 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' 对应 fdformImages: 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 对应 fdformImages: 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执行之前,确保formProvider.updateForm已执行完毕。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以内。

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

相同的卡片可以添加到桌面上实现不同的功能,比如添加两张桌面的卡片,一张显示杭州的天气,一张显示北京的天气,设置每天早上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状态选中则更新textAif (stateA === 'true') {let param: Record<string, string> = {'textA': 'AAA'};let formInfo: formBindingData.FormBindingData = formBindingData.createFormBindingData(param);await formProvider.updateForm(formId, formInfo);}// B状态选中则更新textBif (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)时删除当前卡片存储的持久化信息,避免反复添加删除卡片导致数据库文件持续变大。

相关文章:

【鸿蒙开发】第四十章 Form Kit(卡片开发服务)

目录 1 概述 1.1 卡片使用场景 1.2 服务卡片架构 1.3 亮点/特征 1.4 开发模式 1.5 与相关Kit的关系 1.6 约束限制 2 ArkTS卡片运行机制 2.1 实现原理 2.2 ArkTS卡片的优势 2.3 ArkTS卡片的约束 3 ArkTS卡片相关模块 4 ArkTS卡片开发指导 4.1 创建一个ArkTS卡片 …...

汽车自动驾驶辅助L2++是什么?

自动驾驶辅助级别有哪些&#xff1f; 依照SAE&#xff08;SAE International&#xff0c;Society of Automotive Engineers国际自动机工程师学会&#xff09;的标准&#xff0c;大致划分为6级&#xff08;L0-L5&#xff09;&#xff1a; L0人工驾驶&#xff1a;即没有驾驶辅助…...

微信小程序消息推送解密

package com.test.main.b2b;import org.apache.commons.codec.binary.Base64;import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Arrays;/*** author * version 1.0* description: 解谜微信小…...

java项目之城市公园信息管理系统的设计与实现(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的城市公园信息管理系统的设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 城市公园信息…...

EasyRTC:基于WebRTC与P2P技术,开启智能硬件音视频交互的全新时代

在数字化浪潮的席卷下&#xff0c;智能硬件已成为我们日常生活的重要组成部分&#xff0c;从智能家居到智能穿戴&#xff0c;从工业物联网到远程协作&#xff0c;设备间的互联互通已成为不可或缺的趋势。然而&#xff0c;高效、低延迟且稳定的音视频交互一直是智能硬件领域亟待…...

【前端框架】vue2和vue3的区别详细介绍

Vue 3 作为 Vue 2 的迭代版本&#xff0c;在性能、语法、架构设计等多个维度均有显著的变革与优化。以下详细剖析二者的区别&#xff1a; 响应式系统 Vue 2 实现原理&#xff1a;基于 Object.defineProperty() 方法实现响应式。当一个 Vue 实例创建时&#xff0c;Vue 会遍历…...

23.1 WebBrowser控件

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 WebBrowser控件类似于IE浏览器的文档界面&#xff08;事实上IE也是使用的这个控件&#xff09;&#xff0c;它提供了显示网页及支持…...

从0-1搭建mac环境最新版

从0-1搭建mac环境 先查看自己的芯片信息 bash uname -mbash-3.2$ uname -m arm64这里是自己的型号安装brew xcode-select --install xcode-select -p /bin/zsh -c “$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)” source /Users/lanren/.…...

Docker-技术架构演进之路

目录 一、概述 常见概念 二、架构演进 1.单机架构 2.应用数据分离架构 3.应用服务集群架构 4.读写分离 / 主从分离架构 5.引入缓存 —— 冷热分离架构 6.垂直分库 7.业务拆分 —— 微服务 8.容器化引入——容器编排架构 三、尾声 一、概述 在进行技术学习过程中&am…...

堆、优先队列、堆排序

堆&#xff1a; 定义&#xff1a; 必须是一个完全二叉树&#xff08;完全二叉树&#xff1a;完全二叉树只允许最后一行不为满&#xff0c;且最后一行必须从左往右排序&#xff0c;最后一行元素之间不可以有间隔&#xff09; 堆序性&#xff1a; 大根堆&#xff1a;每个父节点…...

C语言之宏定义

目录 前言 一、宏定义前操作 二、引用自定义.h文件 三、宏定义#define 四、对比typedef的差异 五、替换一个函数或表达式 六、嵌套宏替换 七、用宏和typedef创建一个“布尔型数据 八、定义有参数的宏 总结 前言 C语言中的宏定义是一种预处理指令&#xff0c;用来定义常量、函数…...

大语言模型基础

简介 AI大模型是“人工智能预训练大模型”的简称,包含了“预训练”和“大模型”两层含义,二者结合产生了一种新的人工智能模式,即模型在大规模数据集上完成了预训练后无需微调,或仅需要少量数据的微调,就能直接支撑各类应用。AI大模型主要分为三类:大语言模型、CV大模型…...

vxe-table 如何实现跟 Excel 一样的数值或金额的负数自动显示红色字体

vxe-table 如何实现跟 Excel 一样的数值或金额的负数自动显示红色字体&#xff0c;当输入的值为负数时&#xff0c;会自动显示红色字体&#xff0c;对于数值或者金额输入时该功能就非常有用了。 查看官网&#xff1a;https://vxetable.cn gitbub&#xff1a;https://github.co…...

Web 自动化测试提速利器:Aqua 的 Web Inspector (检查器)使用详解

Web 自动化测试提速利器&#xff1a;Aqua 的 Web Inspector &#xff08;检查器&#xff09;使用详解 前言简介一、安装二、Web Inspector 的使用2.1 获取元素定位器&#xff08;Locators&#xff09;2.2 将定位器添加到代码2.3 验证定位器2.4 处理 Frames (框架) 总结 前言 Je…...

23种设计模式 - 空对象模式

模式定义 空对象模式&#xff08;Null Object Pattern&#xff09;是一种行为型设计模式&#xff0c;通过用无操作的空对象替代null值&#xff0c;消除客户端对空值的检查&#xff0c;避免空指针异常。其核心是让空对象与真实对象实现相同接口&#xff0c;但空对象不执行实际逻…...

【mysql80 安装】mysql8.0.31 安装修改3306端口

在离线安装 MySQL 时&#xff0c;可以通过修改 MySQL 的配置文件来更改默认的 3306 端口。以下是具体步骤&#xff1a; 1、vim /etc/my.cnf 打开配置文件后&#xff0c;找到 [mysqld] 部分&#xff0c;这是 MySQL 服务的配置区域。在该部分中&#xff0c;找到或添加以下内容&a…...

基于eBPF的全栈可观测性系统:重新定义云原生环境诊断范式

引言&#xff1a;突破传统APM的性能桎梏 某头部电商平台采用eBPF重构可观测体系后&#xff0c;生产环境指标采集性能提升327倍&#xff1a;百万QPS场景下传统代理模式CPU占用达63%&#xff0c;而eBPF直采方案仅消耗0.9%内核资源。核心业务的全链路追踪时延从900μs降至18μs&a…...

C语言基础学习指南:从零入门到实战应用——适合零基础学习者与进阶巩固

目录 一、C语言概述与开发环境搭建 二、核心语法与数据类型 三、控制结构与运算符 四、函数与模块化编程 五、指针与内存管理 六、实践建议与资源推荐 结语 一、C语言概述与开发环境搭建 C语言是一种高效、灵活的通用编程语言&#xff0c;广泛应用于系统开发、嵌入式系…...

软件架构设计:架构风格

一、架构风格概述 定义 架构风格是对软件系统整体结构和组织方式的抽象描述&#xff0c;提供了一套通用的设计原则和模式。 作用 提高系统的可维护性、可扩展性和可复用性。帮助开发团队在设计和实现过程中保持一致性和规范性。 常见架构风格 分层架构、MVC架构、微服务架构、…...

为啥vue3设计不直接用toRefs,而是reactive+toRefs

Vue 3 设计中将 reactive 和 toRefs 结合使用而非直接使用 toRefs&#xff0c;主要基于以下设计考量&#xff1a; 1. 响应式粒度的不同需求 reactive 适用于对象整体响应式 reactive 会为整个对象创建响应式代理&#xff0c;自动追踪对象内部所有属性的变化。这种设计适用于需要…...

go 网络编程 websocket gorilla/websocket

在 Go 语言中&#xff0c;你可以使用标准库中的 net/http 包和第三方库 gorilla/websocket 来实现一个 WebSocket 服务器。gorilla/websocket 库提供了对 WebSocket 协议的高级抽象&#xff0c;使得处理 WebSocket 连接变得相对简单。 package mainimport ("fmt"&qu…...

【微服务】springboot远程docker进行debug调试使用详解

目录 一、前言 二、线上问题常用解决方案 2.1 微服务线上运行中常见的问题 2.2 微服务线上问题解决方案 2.3 远程debug概述 2.3.1 远程debug原理 2.3.2 远程debug优势 三、实验环境准备 3.1 搭建springboot工程 3.1.1 工程结构 3.1.2 引入基础依赖 3.1.3 添加配置文…...

CORS跨域问题常见解决办法

1.引言 在现代前端开发中&#xff0c;跨域资源共享&#xff08;Cross-Origin Resource Sharing, CORS&#xff09;是一种通过设置 HTTP 头来允许或阻止不同源之间的资源访问的机制。浏览器出于安全考虑&#xff0c;默认情况下会阻止跨域请求。本文将详细介绍 CORS 的工作原理、…...

并查集算法篇上期:并查集原理及实现

引入 那么我们在介绍我们并查集的原理之前&#xff0c;我们先来看一下并查集所应用的一个场景&#xff1a;那么现在我们有一个长度为n的数组&#xff0c;他们分别属于不同的集合&#xff0c;那么现在我们要查询数组当中某个元素和其他元素是否处于同一集合当中&#xff0c;或者…...

树莓派4基于Debian GNU/Linux 12 (Bookworm)添加多个静态ipv4网络

假设之前已经配置了 在eth0接口配置了192.168.0.100&#xff0c;现在要在同一接口&#xff08;例如 eth0&#xff09;上添加 192.168.1.100&#xff1a; 直接编辑 /etc/NetworkManager/system-connections/ 中相应的连接文件&#xff08;该文件的文件名通常与连接名称相同&…...

「正版软件」PDF Reader - 专业 PDF 编辑阅读工具软件

PDF Reader 轻松查看、编辑、批注、转换、数字签名和管理 PDF 文件&#xff0c;以提高工作效率并充分利用 PDF 文档。 像专业人士一样编辑 PDF 编辑 PDF 文本 轻松添加、删除或修改 PDF 文档中的原始文本以更正错误。自定义文本属性&#xff0c;如颜色、字体大小、样式和粗细。…...

Python连接MySQL数据库图文教程,Python连接数据库MySQL入门教程

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1. 环境准备1.1安装 Python1.2选择开发环境1.3安装 MySQL 数据库1.4 安装 pymysql 库 2. 连接数据库3. 数据库基本操作3.1 创建数据库3.2 创建表3.3 插入数据3.…...

Websocket——心跳检测

1. 前言&#xff1a;为什么需要心跳机制&#xff1f; 在现代的实时网络应用中&#xff0c;保持客户端和服务端的连接稳定性是非常重要的。尤其是在长时间的网络连接中&#xff0c;存在一些异常情况&#xff0c;导致服务端无法及时感知到客户端的断开&#xff0c;可能造成不必要…...

量子计算驱动的金融衍生品定价革命:突破传统蒙特卡洛模拟的性能边界

引言&#xff1a;金融计算的算力困局 某国际投行采用128量子位处理器对亚洲期权组合定价时&#xff0c;其量子振幅估计算法在2.7秒内完成传统GPU集群需要68小时的计算任务。在蒙特卡洛路径模拟实验中&#xff0c;量子随机游走算法将10,000维衍生品的价格收敛速度提升4个数量级…...

文心智能体平台已全面接入DeepSeek模型,全部免费!

文心智能体平台已全面接入DeepSeek模型&#xff01;即日起&#xff0c;您可以在创建智能体时&#xff0c;自由选择所需要的模型&#xff0c;欢迎大家体验。 ✅ ‌零成本体验‌&#xff1a;当前阶段所有用户可免费使用‌DeepSeek模型。‌ ✅ ‌‌智能适配&#xff1a;4款DeepSe…...

DuodooBMS源码解读之 odoo_phoenix_alarm模块

Odoo18 扩展模块声光报警器用户使用手册 一、模块概述 本扩展模块是基于 Odoo18 原生系统进行开发的&#xff0c;主要用于实现与上位声光报警设备的通讯功能。通过该模块&#xff0c;用户可以方便地向设备发送指令&#xff0c;控制设备的声音、灯光等操作。本手册将详细介绍该…...

docker从容器中cp到本地、cp本地到容器

在 Docker 中&#xff0c;你可以使用 docker cp 命令从容器中复制文件到本地主机。以下是具体步骤&#xff1a; 1. 查找容器 ID 或名称 首先&#xff0c;你需要知道容器的 ID 或名称。你可以使用以下命令列出所有正在运行的容器&#xff1a; docker ps 这将显示所有正在运行…...

网络工程师 (49)UDP协议

前言 UDP协议&#xff0c;即用户数据报协议&#xff08;User Datagram Protocol&#xff09;&#xff0c;是一种无连接的、不可靠的、面向报文的传输层通信协议。 一、基本特点 无连接性&#xff1a;UDP在发送数据之前不需要与目标设备建立连接&#xff0c;也无需在数据发送结束…...

1.20作业

1 mfw(git泄露) ./git&#xff0c;原本以为点了链接下了index文件&#xff0c;就可以打开看源码&#xff0c;结果解析不了 老老实实用了githacker githacker --url --output 1 assert() 断言(assert)的用法 | 菜鸟教程 命令注入: /?page).system(cat ./templates/fl…...

HTML/CSS中交集选择器

1.作用:选中同时符合多个条件的元素 交集就是或的意思 2.语法:选择器1选择器2选择器3......选择器n{} 3.举例: /* 选中:类名为beauty的p元素,此种写法用的非常的多 */p.beauty{color: red;}/* 选中:类名包含rich和beauty的元素 */.rich.beauty{color: blue;} 4.注意: 1.有标签…...

迅为RK3568开发板篇Openharmony配置HDF控制UART-实操-HDF驱动配置UART-修改HCS配置

对于不同的平台&#xff0c;需要在对应的平台目录修改对应的 hcs 文件&#xff0c;接下来示例为在 rk3568下新增 uart4 uart9 uart7 的修改方法。 修改 vendor/hihope/rk3568/hdf_config/khdf/device_info/device_info.hcs 文件&#xff0c;device_info.hcs 中添加以下内容&…...

实时股票行情接口与WebSocket行情接口的应用

实时股票行情接口与WebSocket行情接口的应用 实时股票行情接口是量化交易和投资决策的核心工具之一&#xff0c;行情接口的种类和功能也在不断扩展。介绍几种常见的行情接口&#xff0c;包括实时股票行情接口、Level2行情接口、WebSocket行情接口以及量化行情接口&#xff0c;…...

k8s故障处理经典案例(Classic Case of k8s Fault Handling)

k8s故障处理经典案例 问题描述 kubernetes版本&#xff1a;v1.22.5 部分Pod在新版本发布后一直处于ContainerCreating状态&#xff0c;经过kubectl delete命令删除后一直Terminating状态。 排查过程 遇到问题先查日志 首先进入宿主机&#xff0c;查看三个日志&#xff0c…...

关于uniApp的面试题及其答案解析

我的血液里流淌着战意&#xff01;力量与智慧指引着我&#xff01; 文章目录 1. 什么是uniApp&#xff1f;2. uniApp与原生小程序开发有什么区别&#xff1f;3. 如何使用uniApp实现条件编译&#xff1f;4. uniApp支持哪些平台&#xff0c;各有什么特点&#xff1f;5. 在uniApp中…...

给老系统做个安全检查——Burp SqlMap扫描注入漏洞

背景 在AI技术突飞猛进的今天&#xff0c;类似Cursor之类的工具已经能写出堪比大部分程序员水平的代码了。然而&#xff0c;在我们的代码世界里&#xff0c;仍然有不少"老骥伏枥"的系统在兢兢业业地发光发热。这些祖传系统的代码可能早已过时&#xff0c;架构可能岌…...

langchain系列 - FewShotPromptTemplate 少量示例

导读 环境&#xff1a;OpenEuler、Windows 11、WSL 2、Python 3.12.3 langchain 0.3 背景&#xff1a;前期忙碌的开发阶段结束&#xff0c;需要沉淀自己的应用知识&#xff0c;过一遍LangChain 时间&#xff1a;20250220 说明&#xff1a;技术梳理&#xff0c;针对FewShotP…...

【C语言】fgetpos函数用法介绍

目录 一、函数概述 二、核心参数与数据类型 三、典型应用场景 四、与 ftell() 的对比 五、错误处理与调试 六、进阶示例&#xff1a;多位置标记与恢复 七、注意事项 八、总结 fgetpos() 是C标准库中用于文件操作的关键函数之一&#xff0c;其核心功能是获取文件流的当前…...

《算法基础入门:最常用的算法详解与应用(持续更新实战与面试题)》

1. 排序算法 排序算法是将一组数据按特定的顺序排列起来的算法&#xff0c;常见的有&#xff1a; 冒泡排序&#xff08;Bubble Sort&#xff09;选择排序&#xff08;Selection Sort&#xff09;插入排序&#xff08;Insertion Sort&#xff09;归并排序&#xff08;Merge So…...

YOLOv11-ultralytics-8.3.67部分代码阅读笔记-split_dota.py

split_dota.py ultralytics\data\split_dota.py 目录 split_dota.py 1.所需的库和模块 2.def bbox_iof(polygon1, bbox2, eps1e-6): 3.def load_yolo_dota(data_root, split"train"): 4.def get_windows(im_size, crop_sizes(1024,), gaps(200,), im_rate_t…...

如何使用Python快速开发一个带管理系统界面的网站-解析方案

如果你想用 Python 开发一个 管理系统界面 的网站&#xff0c;并且希望界面美观&#xff0c;可以考虑以下几个框架和库&#xff1a; 1. Streamlit&#xff08;快速、简洁&#xff09; 适合&#xff1a;数据分析、仪表盘、内部管理系统特点&#xff1a; 写法简单&#xff0c;类…...

25年HVV关于0day的面试题

以下是对0day漏洞如何防&#xff0c;基本上是每次HVV中大家都会提到的&#xff0c;今天总结了100day防护手段。 《网安面试指南》https://mp.weixin.qq.com/s/RIVYDmxI9g_TgGrpbdDKtA?token1860256701&langzh_CN 5000篇网安资料库https://mp.weixin.qq.com/s?__bizMzkw…...

【C# 数据结构】队列 FIFO

目录 队列的概念FIFO (First-In, First-Out)Queue<T> 的工作原理&#xff1a;示例&#xff1a;解释&#xff1a; 小结&#xff1a; 环形队列1. **FIFO&#xff1f;**2. **环形缓冲队列如何实现FIFO&#xff1f;**关键概念&#xff1a; 3. **环形缓冲队列的工作过程**假设…...

git 克隆及拉取github项目到本地微信开发者工具,微信开发者工具通过git commit、git push上传代码到github仓库

git 克隆及拉取github项目到本地微信开发者工具&#xff0c;微信开发者工具通过git commit、git push上传代码到github仓库 git 克隆及拉取github项目到本地 先在自己的用户文件夹新建一个项目文件夹&#xff0c;取名为项目名 例如这样 C:\Users\HP\yzj-再打开一个终端页面&…...

【机器学习】多元线性回归算法和正规方程解求解

多元线性方差和正规方差解 一、摘要二、多元线性回归介绍三、正规方程解的求解及代码实现 一、摘要 本文围绕多元线性回归的正规方程解展开&#xff0c;为初学者系统介绍了相关基本概念、求解方法、实际应用以及算法封装要点。 首先&#xff0c;深入阐释了正规方程解这一多元…...

在Linux上创建一个Docker容器并在其中执行Python脚本

在Linux上创建一个Docker容器并在其中执行Python脚本的过程&#xff0c;涉及多个方面的内容&#xff0c;包括安装Docker、编写Dockerfile、构建镜像、运行容器等。 1. 安装Docker 在Linux上使用Docker之前&#xff0c;你需要确保系统已安装Docker。Docker支持的Linux发行版有…...