HarmonyOS开发:静态卡片开发——BasicCard实现
HarmonyOS开发:静态卡片开发——BasicCard实现
📌 核心要点:静态卡片是元服务最常用的形态,通过form_config.json配置 + ArkTS布局 + FormBindingData数据驱动,实现轻量级信息展示。
背景与动机
上一篇聊了元服务的概念和FormAbility的基本结构。现在该动手了——从零写一张静态卡片。
为什么先学静态卡片?因为它是元服务开发的基本功。你做10个元服务项目,8个用静态卡片就够了。天气、日程、待办、快递、步数……这些信息展示类场景,静态卡片完全hold住。
动态卡片虽然交互更丰富,但开发复杂度高、性能开销大、调试麻烦。先把静态卡片吃透,后面做动态卡片才能有的放矢。
那静态卡片开发到底分几步?
- 写form_config.json配置文件
- 写卡片布局(ArkTS)
- 写FormAbility提供数据
- 在module.json5中声明
四步走,缺一不可。但每一步都有坑,下面一个一个来。
核心原理
静态卡片的工作机制
静态卡片的核心机制就一句话:布局你写,渲染系统做,数据FormAbility供。
flowchart TB
subgraph 开发阶段
A[form_config.json<br/>卡片声明配置] --> B[WidgetCard.ets<br/>卡片布局文件]
B --> C[FormAbility.ets<br/>数据提供方]
C --> D[module.json5<br/>注册声明]
end
subgraph 运行时
E[用户添加卡片] --> F[系统读取form_config]
F --> G[触发FormAbility.onAddForm]
G --> H[获取FormBindingData]
H --> I[系统渲染引擎<br/>合并布局+数据]
I --> J[卡片显示在桌面]
K[定时/手动更新] --> L[触发onUpdateForm]
L --> M[获取新FormBindingData]
M --> I
end
D --> E
classDef devPhase fill:#E8EAF6,stroke:#283593,color:#1A237E
classDef runtime fill:#E0F2F1,stroke:#00695C,color:#004D40
classDef core fill:#FFF8E1,stroke:#F57F17,color:#E65100
class A,B,C,D devPhase
class E,F,G,H,I,J,K,L,M runtime
注意那个"系统渲染引擎"——这是静态卡片和动态卡片的本质区别。静态卡片的UI不是你的进程渲染的,是系统渲染引擎拿着你的布局文件和数据,在系统侧完成渲染。这意味着:
- 卡片渲染不占你应用的内存
- 即使你的应用进程被杀,卡片照样显示
- 但你没法控制渲染时机,只能"喂数据"
form_config.json详解
这个配置文件是卡片的"身份证",告诉系统你提供什么卡片、支持什么尺寸、多久更新一次。
{
"forms": [
{
// 卡片名称,FormAbility通过这个区分不同卡片
"name": "todoCard",
// 用户在卡片选择器看到的名称
"displayName": "待办事项",
// 卡片描述
"description": "显示今日待办事项列表",
// 卡片布局文件路径(相对于src/main/ets/)
"src": "./ets/widget/pages/TodoCard.ets",
// 语法类型,静态卡片用arkts
"uiSyntax": "arkts",
// 窗口设计宽度,影响布局计算
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
// 颜色模式:auto跟随系统,dark暗色,light亮色
"colorMode": "auto",
// 是否为默认卡片(长按桌面时优先展示)
"isDefault": true,
// 是否启用定时更新
"updateEnabled": true,
// 定时更新时间(24小时制,如"10:30"表示10:30触发更新)
"scheduledUpdateTime": "08:00",
// 更新持续时间(0表示只在scheduledUpdateTime更新一次,
// 1表示从scheduledUpdateTime开始每小时更新一次)
"updateDuration": 1,
// 默认卡片尺寸
"defaultDimension": "2*2",
// 支持的卡片尺寸列表
"supportDimensions": ["2*2", "2*4", "4*4"],
// 卡片是否可见(开发调试时可设为false隐藏)
"formVisibleNotify": true,
// 元数据
"metadata": [],
// 深度链接,点击卡片时跳转的Ability
"deepLink": "myapp://todo/today"
}
]
}
每个字段都有讲究,但最容易搞错的是这几个:
- updateDuration:不是"更新持续多久",是"从scheduledUpdateTime开始,每隔多久更新一次"。0=只更新一次,1=每小时更新一次。最大值是16,即每16小时更新一次。
- supportDimensions:必须包含defaultDimension,否则编译报错。
- designWidth:720是推荐值,别乱改。改了你的布局可能全乱。
代码实战
基础用法:最简待办卡片
先来个最简单的——一张2x2的待办事项卡片,显示今日待办数量和最近一条待办。
第一步:form_config.json
{
"forms": [{
"name": "todoCard",
"displayName": "待办事项",
"description": "今日待办一览",
"src": "./ets/widget/pages/TodoCard.ets",
"uiSyntax": "arkts",
"window": { "designWidth": 720, "autoDesignWidth": true },
"colorMode": "auto",
"isDefault": true,
"updateEnabled": true,
"scheduledUpdateTime": "07:00",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": ["2*2"]
}]
}
第二步:卡片布局 TodoCard.ets
@Entry
@Component
struct TodoCard {
// 这些变量名必须和FormBindingData的key一一对应
@State todoCount: string = '0'
@State latestTodo: string = '暂无待办'
@State dateStr: string = ''
build() {
Column() {
// 顶部:日期
Row() {
Text(this.dateStr)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.justifyContent(FlexAlign.Start)
// 中部:待办数量
Row() {
Text(this.todoCount)
.fontSize(32)
.fontColor('#FF6B35')
.fontWeight(FontWeight.Bold)
Text('项待办')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 4, top: 14 })
}
.margin({ top: 12 })
.alignItems(VerticalAlign.Bottom)
// 底部:最近一条
Text(this.latestTodo)
.fontSize(12)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
第三步:FormAbility
import { formBindingData, FormExtensionAbility } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
export default class TodoFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
const now = new Date()
const dateStr = `${now.getMonth() + 1}月${now.getDate()}日`
// 模拟数据——实际项目从数据库或DataShare读取
const todos = this.getTodayTodos()
return formBindingData.createFormBindingData({
dateStr: dateStr,
todoCount: todos.length.toString(),
latestTodo: todos.length > 0 ? todos[0] : '暂无待办'
})
}
onUpdateForm(formId: string): formBindingData.FormBindingData {
// 更新逻辑和创建一样,重新获取最新数据
return this.onAddForm(null)
}
onRemoveForm(formId: string): void {
hilog.info(0x0000, 'TodoForm', `卡片删除: ${formId}`)
}
private getTodayTodos(): string[] {
return ['完成周报', '修复登录Bug', '评审设计稿']
}
}
第四步:module.json5声明
{
"extensionAbilities": [{
"name": "TodoFormAbility",
"srcEntry": "./ets/formability/TodoFormAbility.ets",
"type": "form",
"label": "$string:form_label",
"description": "$string:form_desc",
"metadata": [{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}]
}]
}
四步走完,编译运行,长按桌面空白处,就能看到你的待办卡片了。
进阶用法:多尺寸卡片适配
实际产品中,用户可能选择不同尺寸的卡片。2x2显示概要,2x4显示列表,4x4显示详细信息。怎么用一套代码适配多种尺寸?
关键在于:form_config.json里声明多个尺寸,但布局文件只需要一个。你通过判断卡片宽度来切换布局。
{
"forms": [{
"name": "todoCard",
"displayName": "待办事项",
"src": "./ets/widget/pages/TodoCard.ets",
"defaultDimension": "2*2",
"supportDimensions": ["2*2", "2*4", "4*4"]
}]
}
@Entry
@Component
struct TodoCard {
@State todoCount: string = '0'
@State latestTodo: string = '暂无待办'
@State dateStr: string = ''
@State todoList: string = '' // 2x4和4x4用,逗号分隔
@State detailInfo: string = '' // 4x4专用
build() {
Column() {
// 通用顶部
Row() {
Text(this.dateStr)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
// 根据宽度判断尺寸——2x2约150vp,2x4约300vp,4x4约300vp
// 使用响应式布局
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween }) {
// 待办数量(所有尺寸都显示)
Row() {
Text(this.todoCount)
.fontSize(32)
.fontColor('#FF6B35')
.fontWeight(FontWeight.Bold)
Text('项待办')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 4, top: 14 })
}
// 最近待办(2x2显示)
Text(this.latestTodo)
.fontSize(12)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
但说实话,这种"一个布局适配所有尺寸"的方式效果一般。更好的做法是:在form_config.json里为不同尺寸声明不同的布局文件。
{
"forms": [
{
"name": "todoCardSmall",
"displayName": "待办概要",
"src": "./ets/widget/pages/TodoCardSmall.ets",
"defaultDimension": "2*2",
"supportDimensions": ["2*2"]
},
{
"name": "todoCardMedium",
"displayName": "待办列表",
"src": "./ets/widget/pages/TodoCardMedium.ets",
"defaultDimension": "2*4",
"supportDimensions": ["2*4"]
},
{
"name": "todoCardLarge",
"displayName": "待办详情",
"src": "./ets/widget/pages/TodoCardLarge.ets",
"defaultDimension": "4*4",
"supportDimensions": ["4*4"]
}
]
}
这样每种尺寸有独立的布局文件,互不干扰,维护起来也清晰。代价是FormAbility里要根据formName返回不同数据。
完整示例:天气卡片(含图片资源)
静态卡片支持Image组件,你可以用图片让卡片更好看。但图片资源的使用有讲究——只能用本地资源,不能加载网络图片。
// WeatherCard.ets —— 带图标的天气卡片
@Entry
@Component
struct WeatherCard {
@State city: string = ''
@State temperature: string = ''
@State weatherDesc: string = ''
@State weatherType: string = 'sunny' // sunny/cloudy/rainy/snowy
@State updateTime: string = ''
@State aqi: string = '' // 空气质量
@State humidity: string = '' // 湿度
build() {
Column() {
// 顶栏:城市 + 更新时间
Row() {
Text(this.city)
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
Blank()
Text(this.updateTime)
.fontSize(10)
.fontColor('#AAAAAA')
}
.width('100%')
// 主信息区:温度 + 天气图标
Row() {
Column() {
Text(this.temperature)
.fontSize(40)
.fontColor('#FF6B35')
.fontWeight(FontWeight.Bold)
Text(this.weatherDesc)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
// 天气图标——只能用本地资源
Image(this.getWeatherIcon(this.weatherType))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
}
.width('100%')
.layoutWeight(1)
.alignItems(VerticalAlign.Center)
// 底部信息栏:湿度 + 空气质量
Row() {
Row() {
Text('💧')
.fontSize(12)
Text(this.humidity)
.fontSize(11)
.fontColor('#666666')
.margin({ left: 2 })
}
Blank()
Row() {
Text('AQI ')
.fontSize(11)
.fontColor('#999999')
Text(this.aqi)
.fontSize(11)
.fontColor(this.getAqiColor(this.aqi))
.fontWeight(FontWeight.Bold)
}
}
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#F8F9FA')
.borderRadius(16)
}
// 根据天气类型返回图标资源
private getWeatherIcon(type: string): Resource {
const iconMap: Record<string, Resource> = {
'sunny': $r('app.media.weather_sunny'),
'cloudy': $r('app.media.weather_cloudy'),
'rainy': $r('app.media.weather_rainy'),
'snowy': $r('app.media.weather_snowy')
}
return iconMap[type] || $r('app.media.weather_sunny')
}
// 根据AQI返回颜色
private getAqiColor(aqi: string): string {
const val = parseInt(aqi)
if (val <= 50) return '#4CAF50' // 优
if (val <= 100) return '#FFC107' // 良
if (val <= 150) return '#FF9800' // 轻度污染
return '#F44336' // 中度及以上
}
}
对应的FormAbility:
export default class WeatherFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
const weatherData = this.fetchWeatherData()
return formBindingData.createFormBindingData({
city: weatherData.city,
temperature: weatherData.temperature,
weatherDesc: weatherData.weatherDesc,
weatherType: weatherData.weatherType,
updateTime: weatherData.updateTime,
aqi: weatherData.aqi,
humidity: weatherData.humidity
})
}
onUpdateForm(formId: string): formBindingData.FormBindingData {
return this.onAddForm(null)
}
onRemoveForm(formId: string): void {
// 清理
}
private fetchWeatherData(): WeatherData {
// 实际项目从缓存或DataShare读取
return {
city: '北京',
temperature: '26°',
weatherDesc: '晴',
weatherType: 'sunny',
updateTime: this.getCurrentTime(),
aqi: '42',
humidity: '45%'
}
}
private getCurrentTime(): string {
const now = new Date()
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
}
}
图片资源放在 src/main/resources/base/media/ 目录下,文件名对应 $r('app.media.xxx') 中的xxx。比如 weather_sunny.png、weather_cloudy.png 等。
踩坑与注意事项
坑1:@State变量名必须和数据key完全匹配
这是新手踩得最多的坑。你在FormAbility里返回的数据key叫todoCount,那卡片里的@State变量也必须叫todoCount,一个字母都不能差。
// ❌ 不匹配,卡片显示空白
// FormAbility返回: { count: '3' }
// 卡片里: @State todoCount: string = '0'
// ✅ 匹配,正常显示
// FormAbility返回: { todoCount: '3' }
// 卡片里: @State todoCount: string = '0'
而且类型也要匹配。FormAbility返回的是字符串,@State就得声明string。返回数字,@State就得声明number。类型不匹配,数据注入失败,卡片显示默认值。
坑2:卡片布局不支持所有ArkTS能力
静态卡片的布局能力是受限的。以下能力在静态卡片中不支持:
@Builder装饰器@Extend装饰器@Styles装饰器- 自定义组件(
@Component) animateTo动画Canvas绘制Web组件LazyForEach@Watch、@Link、@Provide、@Consume
这意味着什么?你的卡片布局必须是一个扁平的结构,所有UI都写在一个build()方法里。不能抽取公共组件,不能用Builder简化代码。
// ❌ 静态卡片不能用自定义组件
@Component
struct WeatherIcon { // 编译报错!
build() { ... }
}
// ✅ 只能在build()里内联写
build() {
Image($r('app.media.weather_sunny'))
.width(48)
.height(48)
}
坑3:图片资源只能用本地资源
静态卡片不支持网络图片。Image('https://xxx.com/icon.png') 在卡片里是显示不出来的。
你有两种选择:
- 用本地资源:把图片放在resources/base/media/下,用
$r('app.media.xxx')引用 - 用系统图标:用
$r('sys.media.xxx')引用系统资源
但注意,卡片图片资源的大小也有限制。单张图片建议不超过50KB,否则可能加载失败或导致卡片白屏。
坑4:卡片布局调试困难
静态卡片的布局调试是个老大难问题。你在DevEco Studio里预览的效果,和实际在桌面上的效果可能不一样。原因是:
- 预览器用的是720设计宽度,桌面可能用不同的渲染参数
- 桌面有圆角裁剪,预览器没有
- 不同设备的DPI不同,字体和间距可能偏差
怎么办?实机调试,没有捷径。多跑几台设备,特别是不同DPI的设备,看看效果是否一致。
坑5:FormBindingData数据量控制
前面提过1KB限制。这里给个实际的参考——1KB大概能装多少数据?
// 这个数据大约800字节,接近1KB上限
const data = {
title: '今日待办事项', // ~20字节
count: '5', // ~8字节
item1: '完成周报编写和提交', // ~25字节
item2: '修复用户反馈的登录问题', // ~28字节
item3: '参加下午3点的设计评审', // ~26字节
item4: '更新项目文档', // ~16字节
item5: '准备明天的演示材料', // ~22字节
date: '2026年6月27日', // ~18字节
updateTime: '14:30' // ~14字节
}
// 总计约177字节(UTF-8编码中文3字节/字)
中文比英文更费空间,一个汉字3字节。所以1KB大概能装300个汉字左右。够用了,但别浪费。
HarmonyOS 6适配说明
-
静态卡片布局引擎升级:6.0的静态卡片渲染引擎支持了更多布局能力,包括
Flex的wrap属性和Grid的自动布局。5.0里必须手算列数的Grid布局,6.0可以自动适配了。 -
图片资源管理变化:6.0对卡片图片资源的管理更加严格,图片必须放在
resources/base/media/目录下,不支持从rawfile目录引用。5.0里能用$rawfile('xxx.png')的写法,6.0不行了。 -
卡片预览增强:6.0的DevEco Studio支持在预览器中直接预览卡片在不同尺寸下的效果,不用再每次都跑真机调试了。这是最大的开发体验提升。
-
深色模式适配:6.0对卡片的深色模式支持更完善。之前
colorMode: "auto"经常出现深色模式下文字看不见的问题(黑底黑字),6.0修复了系统级的颜色适配逻辑,但你仍然需要在布局中用语义化颜色而非硬编码颜色值。
// ❌ 硬编码颜色,深色模式下可能看不见
.fontColor('#333333')
.backgroundColor('#FFFFFF')
// ✅ 使用系统资源颜色,自动适配深色模式
.fontColor($r('sys.color.ohos_id_color_text_primary'))
.backgroundColor($r('sys.color.ohos_id_color_background'))
总结
静态卡片开发的核心就是"配置+布局+数据"三件套。form_config.json管声明,ArkTS管展示,FormBindingData管数据。三者缺一不可,任何一环出问题,卡片都跑不起来。
关键要点回顾:
- form_config.json是卡片的身份证,每个字段都有讲究
- 卡片布局能力受限,不能用自定义组件、Builder、动画等
- @State变量名必须和FormBindingData的key完全匹配
- 图片只能用本地资源,不支持网络图片
- 数据量不超过1KB,中文要特别注意空间
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐ 上手简单,但限制多,需要反复调试 |
| 使用频率 | ⭐⭐⭐⭐⭐ 静态卡片是元服务最常用的形态 |
| 重要程度 | ⭐⭐⭐⭐⭐ 不做静态卡片,元服务等于没做 |
下一篇我们聊动态卡片——当你需要实时交互和更丰富的UI时,动态卡片是怎么玩的。
- 点赞
- 收藏
- 关注作者
评论(0)