服务卡片可将元服务/应用的重要信息以卡片的形式展示在桌面,用户可通过快捷手势使用卡片,通过轻量交互行为实现服务直达、减少层级跳转的目的。
使用约束
- 每个module最多可以配置16张服务卡片。
- 卡片不支持调试。
- 元服务不支持创建JS卡片。
创建服务卡片
- 创建服务卡片包括如下两种方式:
- 选择模块(如entry模块)下的任意文件,单击菜单栏File > New > Service Widget创建服务卡片。
- 选择模块(如entry模块)下的任意文件,单击右键 > New > Service Widget创建服务卡片。
- 在Choose a Template for Your Service Widget 界面中,选择卡片模板,单击Next。
- 在Configure Your Service Widget界面中,配置卡片的基本信息,包括:
- Service widget name:卡片的名称,在同一个应用/服务中,卡片名称不能重复,且只能包含大小写字母、数字和下划线。
- Display name:卡片预览面板上显示的卡片名称。仅API 11 Stage工程支持配置该字段。
- Description:卡片的描述信息。
- Language: 界面开发语言,可选择创建ArkTS/JS卡片。
- Support dimension:选择卡片的规格。部分卡片支持同时设置多种规格。首次创建服务卡片时,将默认生成一个EntryCard目录,用于存放卡片快照。
- Default dimension:在下拉框中可选择默认的卡片。
- Ability name: 选择一个挂靠服务卡片的Form Ability,或者创建一个新的Form Ability。
- Module name: 卡片所属的模块。
4. 单击Finish完成卡片的创建。创建完成后,工具会自动创建出服务卡片的布局文件,并在form_config.json文件中写入服务卡片的属性字段
创建动态/静态卡片
动态卡片支持自定义交互、动效、滑动等功能,功能丰富但内存占用较大;静态卡片内存占用较小,有助实现整机内存优化,可实现静态信息展示、刷新和点击跳转。
创建服务卡片后,在form_config.json 文件中,可修改isDynamic 参数配置。isDynamic置空或为"true",则该卡片为动态卡片;若赋值为"false",则该卡片为静态卡片。
预览服务卡片
在开发服务卡片过程中,支持对卡片进行实时预览。服务卡片通过ArkTS、JS文件进行布局设计,在开发过程中,可以对布局文件进行实时预览,只要在布局文件中保存了修改的源代码,在预览器中就可以实时查看布局效果。在Phone和Tablet服务卡片的预览效果中,每个尺寸的服务卡片提供3种场景的预览效果,分别为极窄(Minimum)、默认(Default)、极宽(Maximum),开发者应确保三种尺寸的显示效果均正常,以便适应不同屏幕尺寸的设备。
服务卡片架构
卡片的基本概念:
-
卡片使用方:如上图中的桌面,显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
- 应用图标:应用入口图标,点击后可拉起应用进程,图标内容不支持交互
- 卡片:具备不同规格大小的界面展示,卡片的内容可以进行交互,如实现按钮进行界面的刷新、应用的跳转等。
-
卡片提供方:包含卡片的应用,提供卡片的显示内容、控件布局以及控件点击处理逻辑。
- FormExtensionAbility:卡片业务逻辑模块,提供卡片创建、销毁、刷新等生命周期回调。
- 卡片页面:卡片UI模块,包含页面控件、布局、事件等显示和交互信息。
卡片常见使用步骤
- 长按"桌面图标",弹出操作菜单。
- 点击"服务卡片"选项,进入卡片预览界面。
- 点击"添加到桌面"按钮,即可在桌面上看到新添加的卡片。
亮点/特征
- 服务直达:将元服务/应用的重要信息以卡片形式展示在桌面,用户可以通过快捷手势使用卡片,通过轻量交互行为实现服务直达、减少层级跳转的目的。
- 永久在线:提供定时、代理等多种卡片刷新机制,实现卡片永久在线。
- 受限管控:卡片支持的组件、事件、动效、数据管理、状态管理和API能力均进行了一定限制,保障性能、功耗及安全可靠。
约束限制
针对ArkTS卡片,主要存在如下使用限制:
- 当导入模块时,仅支持导入标识"支持在ArkTS卡片中使用"的模块。
- 仅支持声明式范式的部分组件、事件、动效、数据管理、状态管理和API能力。
- 卡片的事件处理和使用方的事件处理是独立的,建议在使用方支持左右滑动的场景下卡片内容不要使用左右滑动功能的组件,以防手势冲突影响交互体验。
- 暂不支持导入共享包及使用native语言开发。
- 暂不支持极速预览、断点调试能力、热重载及设置超时任务(setTimeOut)等能力。
实现原理
- 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置,当前仅系统应用可以作为卡片使用方。
- 卡片提供方:提供卡片显示内容的应用,控制卡片的显示内容、控件布局以及控件点击事件。
- 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,提供formProvider的接口能力,同时提供卡片对象的管理与使用以及卡片周期性刷新等能力。
- 卡片渲染服务:用于管理卡片渲染实例,渲染实例与卡片使用方上的卡片组件一一绑定。卡片渲染服务运行卡片页面代码widgets.abc进行渲染,并将渲染后的数据发送至卡片使用方对应的卡片组件。
ArkTS卡片渲染服务运行原理
与动态卡片相比,静态卡片整体的运行框架和渲染流程是一致的,主要区别在于,卡片渲染服务将卡片内容渲染完毕后,卡片使用方会使用最后一帧渲染的数据作为静态图片显示,其次卡片渲染服务中的卡片实例会释放该卡片的所有运行资源以节省内存。因此频繁的刷新会导致静态卡片运行时资源不断的创建和销毁,增加卡片功耗。
与JS卡片相比,ArkTS卡片支持在卡片中运行逻辑代码,为确保ArkTS卡片发生问题后不影响卡片使用方应用的使用,ArkTS卡片新增了卡片渲染服务用于运行卡片页面代码widgets.abc,卡片渲染服务由卡片管理服务管理。卡片使用方的每个卡片组件都对应了卡片渲染服务里的一个渲染实例,同一应用提供方的渲染实例运行在同一个ArkTS虚拟机运行环境中,不同应用提供方的渲染实例运行在不同的ArkTS虚拟机运行环境中,通过ArkTS虚拟机运行环境隔离不同应用提供方卡片之间的资源与状态。开发过程中需要注意的是globalThis对象的使用,相同应用提供方的卡片globalThis对象是同一个,不同应用提供方的卡片globalThis对象是不同的。
ArkTS卡片的优势
卡片作为应用的一个快捷入口,ArkTS卡片相较于JS卡片具备如下几点优势:
-
统一开发范式,提升开发体验和开发效率。
提供ArkTS卡片能力后,统一了卡片和页面的开发范式,页面的布局可以直接复用到卡片布局中,提升开发体验和开发效率。
-
增强了卡片的能力,使卡片更加万能。
- 新增了动效的能力:ArkTS卡片开放了属性动画和显式动画的能力,使卡片的交互更加友好。
- 新增了自定义绘制的能力:ArkTS卡片开放了Canvas画布组件的能力,卡片可以使用自定义绘制的能力构建更多样的显示和交互效果。
- 允许卡片中运行逻辑代码:开放逻辑代码运行后很多业务逻辑可以在卡片内部自闭环,拓宽了卡片的业务适用场景。
ArkTS卡片的约束
ArkTS卡片相较于JS卡片具备了更加丰富的能力,但也增加了使用卡片进行恶意行为的风险。由于ArkTS卡片显示在使用方应用中,使用方应用一般为桌面应用,为确保桌面的使用体验以及功耗相关考虑,对ArkTS卡片的能力做了以下约束:
- 当导入模块时,仅支持导入标识"支持在ArkTS卡片中使用"的模块。
- 不支持导入共享包。
- 不支持使用native语言开发。
- 仅支持声明式范式的部分组件、事件、动效、数据管理、状态管理和API能力。
- 卡片的事件处理和使用方的事件处理是独立的,建议在使用方支持左右滑动的场景下卡片内容不要使用左右滑动功能的组件,以防手势冲突影响交互体验。
除此之外,当前ArkTS卡片还存在如下约束:
- 暂不支持极速预览。
- 暂不支持断点调试能力。
- 暂不支持Hot Reload热重载。
- 暂不支持setTimeOut。
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)相关信息。
服务卡片的路由跳转
用于卡片内部和提供方应用间的交互
- component:当前自定义组件的实例,通常传入this。
- action:action的类型
- abilityName:action为router / call 类型时跳转的UIAbility名。
- params:当前action携带的额外参数
- targetPage:选择拉起的页面
php
// WidgetCard.ets
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { targetPage: 'pages/RecommendPage' }
})
判断应用进入页面
onCreate函数冷启动判断是否是通过targetPage方式启动页面
csharp
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want.parameters) {
const params: Record<string, string> = JSON.parse(JSON.stringify(want.parameters))
this.selectPage = params.targetPage
}
}
javascript
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent(this.selectPage || 'pages/Index', (err) => {
this.selectPage = ''
}
}
onNewWant函数热启动判断是否是通过targetPage方式启动页面
csharp
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want.parameters) {
const params: Record<string, string> = JSON.parse(JSON.stringify(want.parameters))
this.selectPage = params.targetPage
}
}
javascript
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent(this.selectPage || 'pages/Index', (err) => {
this.selectPage = ''
}
}
创建LoadImageForFormData类工具
- 初始化类数据
ini
initialFormData = new FormData([], {})
ini
constructor(imageUrls: string[], finishCb: (formInfo: formBindingData.FormBindingData) => void,
ability: FormExtensionAbility) {
this.imageUrls = imageUrls
this.finishCb = finishCb
this.ability = ability
this.tempDir = ability.context.getApplicationContext()
.tempDir
}
-
图片下载方法
-
request.downloadFile
downloadFile(context: BaseContext, config: DownloadConfig): Promise<DownloadTask>
context:基于应用程序的上下文。 config:下载的配置信息。DownloadConfig:
-
url 资源地址
-
filePath 设置下载路径
-
enableMetered 设置是否允许在按流量计费的连接下下载
-
enableRoaming 设置是否允许在漫游网络中下载
返回Promise<DownloadTask>,获取DownloadTask对象,进行状态监听
-
-
kotlin
startLoad() {
if (this.imageUrls.length === 0) {
console.error('please provide download imglist')
return
}
let netFile = this.imageUrls[this.curIndex]
let fileName = 'file' + Date.now()
let tmpFile = this.tempDir + '/' + fileName
// tmpFile: /data/storage/el2/base/temp/file1708331593898
request.downloadFile(
this.ability.context,
{
url: netFile,
filePath: tmpFile,
enableMetered: true,
enableRoaming: true
}
)
.then((task) => {
// 监听下载任务完成
task.on('complete', () => {
let file: fileFs.File
try {
// 打开沙箱文件
file = fs.openSync(tmpFile)
fd描述符保存到内存图片对象
this.formImages[fileName] = file.fd
} catch (e) {
console.error(`openSync failed: ${JSON.stringify(e)}`)
}
// 保存文件名
this.fileNameList.push(fileName)
// 更新下载索引
this.curIndex++
if (this.curIndex < this.imageUrls.length) {
// 如果还没下载完毕,继续下载
this.startLoad()
} else {
// 全部下载完毕更新数据
this.initialFormData.fileNameList = this.fileNameList
this.initialFormData.formImages = this.formImages
// 创建一个FormBindingData对象。参数:js卡片要展示的数据。
let formInfo = formBindingData.createFormBindingData(this.initialFormData)
// 传入回调函数
this.finishCb(formInfo)
}
})
})
.catch(() => {
})
}
图片调用
formProvider.updateForm
updateForm(formId: string, formBindingData: formBindingData.FormBindingData): Promise<void>
更新指定的卡片,使用Promise异步回调。
javascript
// FormAbility.ets
// 卡片提供方接收创建卡片的通知接口
onAddForm(want: Want) {
const lf = new LoadImageForFormData(
['http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-05/6fdcac19-dd44-442c-9212-f7ec3cf3ed18.jpg',
'https://yanxuan-item.nosdn.127.net/31a81e6c7e4c173d1cf19d5abeb97550.png'], // 下载的图片路径
(formInfo: formBindingData.FormBindingData) => {
// 获取 formId (卡片 id),用于针对性更新某个卡片
const formId = want.parameters?.['ohos.extra.param.key.form_identity'].toString()
Log.info(formId, '卡片:当前卡片的id ')
Log.info(formInfo.data, '卡片:图片下载成功')
// 传递数据给卡片(卡片id, 卡片传数据时要求的格式)
formProvider.updateForm(formId, formInfo)
},
this// FormExtensionAbility
)
lf.startLoad() // 调用 startLoad 后,才开始下载图片
let formData = '';
return formBindingData.createFormBindingData(formData);
}
卡片显示
卡片组件中通过 @localStorage获取下发的数据,然后使用Image组件通过入参memory://fileName来进行远端内存图片显示
scss
// WidgetCard.ets
const storageWidgetImageUpdate = new LocalStorage()
@Entry(storageWidgetImageUpdate)
@Component
struct WidgetCard {
@LocalStorageProp('fileNameList') fileNameList: string[] = []
Image(`memory://${url}`)
}
更新图片
scss
// WidgetCard.ets
Image($r('app.media.ic_public_refresh'))
.fillColor(Color.White)
.width(30)
.onClick(() => {
postCardAction(this, { action: 'message' });
})
typescript
// FormAbility.ets
function getRandomImages(array: string[]) {
if (array.length < 2) {
return array;
}
let index1 = Math.floor(Math.random() * array.length);
let index2 = Math.floor(Math.random() * array.length);
while (index1 === index2) {
index2 = Math.floor(Math.random() * array.length);
}
return [array[index1], array[index2]];
}
// 卡片提供方接收处理卡片事件的通知接口
onFormEvent(formId: string, message: string) {
// 调取数组获取新图片数据
getGoodsGuessLikeAPI().then((res) => {
const imageList = res.data.result.items.map(item => item.picture as string)
const randomImages = getRandomImages(imageList)
Log.info(formId, '卡片:当前卡片的id')
const loadImage = new LoadImageForFormData(
randomImages,
(formBindingData: formBindingData.FormBindingData) => {
formProvider.updateForm(formId, formBindingData)
},
this
)
loadImage.startLoad()
})
}