第7.12篇:互动卡片------摇一摇触发动态效果
难度 :⭐⭐⭐ 高级
前置知识 :第 4.7 篇 服务卡片开发
涉及源文件 :
products/default/src/main/ets/creationformability/CreationFormAbility.ets
概述
在 HarmonyOS 7(API 26)中,服务卡片(Service Widget)迎来了一次重要的能力升级------互动卡片(Interactive Card) 的正式发布。这并非简单的动画效果堆叠,而是从卡片架构层面上赋予"静态展示"的桌面卡片以"动态交互"的能力:卡片不再仅是信息的被动展示窗,而成为可以响应用户操作、播放丰富动画、甚至与设备传感器联动的微型交互界面。
在"画伴梦工厂"中,这一能力的应用场景尤为突出:想象这样一个画面------用户在桌面上添加了一张 2×4 的创作卡片,展示着孩子最新画作《蜡笔森林小恐龙》。当用户拿起手机时,不经意地轻轻一摇,卡片上的小恐龙立刻"活"了过来,从静态的标题文字中破框而出,在卡片区域内跳动、旋转,仿佛在说"快来看看我的新作品!"。
这便是本文要探讨的核心------摇一摇触发卡互动动画 。我们将从互动卡片的技术架构出发,逐步拆解如何利用"摇一摇"这一自然手势触发卡片的静转动动画和前景元素出框效果,并在"画伴梦工厂"的现有 CreationFormAbility 框架中落地实现,最终论述互动卡片对卡片曝光率和交互转化率的提升价值。
一、互动卡片升级:从静态到动态
1.1 静态卡片的能力边界
在了解互动卡片之前,有必要先明确传统静态卡片的局限。在第 4.7 篇中,我们实现的 CreationFormAbility 配合 form_config.json 构建了一个标准的服务卡片,它支持:
- 展示最新的作品标题和图标
- 通过
formProvider.updateForm更新数据 - 通过
onFormEvent响应点击事件并跳转
但本质而言,它是一种静态渲染 模型:数据从 FormExtensionAbility 推送至卡片 UI,卡片 UI 按照声明式布局渲染组件,用户看到的是"截图式"的信息展示。即使数据会更新,呈现方式也不会变化------不会有元素的位移、缩放、透明度渐变,更不会有复杂的帧动画。
1.2 HarmonyOS 7 互动卡片能力矩阵
HarmonyOS 7 对服务卡片的架构进行了重构,引入了 isDynamic 属性和增强的渲染管线。当 form_config.json 中的 isDynamic 设为 true 时,卡片被标记为"动态卡片",解锁以下能力:
| 能力 | 说明 | 技术支撑 |
|---|---|---|
| 帧动画 | 卡片内组件支持关键帧动画、补间动画 | ArkUI 动画引擎下沉至卡片渲染层 |
| 交互事件 | 支持点击、长按、触摸等手势事件 | 卡片事件路由增强 |
| 数据驱动动画 | 动画参数可通过 FormBindingData 动态传递 | 数据绑定扩展 |
| Canvas 渲染 | 卡片内支持 Canvas 组件绘制 2D 图形 | 轻量 Canvas 引擎 |
| 前景出框 | 组件可突破卡片圆角/边界渲染 | 渲染裁剪层级优化 |
| 传感器联动 | 通过应用中转,响应设备运动状态 | postCardAction 事件链路 |
1.3 isDynamic 配置项
在 form_config.json 中启用动态卡片仅需一个字段变更:
json
{
"forms": [
{
"name": "creation",
"isDynamic": true,
// 其余配置不变...
}
]
}
需要注意的是,isDynamic: true 并非一个简单的开关------它告诉系统该卡片需要分配更多的渲染资源和事件处理能力。系统会为动态卡片预留专用的动画调度线程,确保动画在卡片更新时流畅运行,同时不会影响桌面的整体滚动性能。
二、摇一摇触发原理:传感器→事件→动画链路
"摇一摇触发卡片动效"的核心技术链路,涉及三个层面:传感器检测 (感知摇一摇)、事件传递 (将事件送达卡片)、动画执行(卡片内播放动画)。由于服务卡片运行在受限的卡片进程中,无法直接访问设备传感器,因此链路必须经过主应用进行中转。
2.1 传感器检测:@kit.SensorKit 的 accelerometer
在 HarmonyOS 中,摇一摇手势的检测基于加速度传感器(Accelerometer)。通过 @kit.SensorKit 注册加速度监听器,当设备在三个轴向上加速度的合力超过阈值时,判定为一次"摇一摇":
typescript
import { sensor } from '@kit.SensorKit';
// 定义摇一摇判断参数
const SHAKE_THRESHOLD = 15; // 加速度变化阈值 (m/s²)
const SHAKE_INTERVAL = 800; // 两次摇动最小间隔 (ms)
let lastShakeTime = 0;
let lastX = 0, lastY = 0, lastZ = 0;
sensor.on(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
const now = Date.now();
if (now - lastShakeTime < SHAKE_INTERVAL) return;
const deltaX = Math.abs(data.x - lastX);
const deltaY = Math.abs(data.y - lastY);
const deltaZ = Math.abs(data.z - lastZ);
const totalDelta = deltaX + deltaY + deltaZ;
if (totalDelta > SHAKE_THRESHOLD) {
lastShakeTime = now;
// 触发摇一摇事件 → 传递给卡片
notifyCardsShakeDetected();
}
lastX = data.x;
lastY = data.y;
lastZ = data.z;
});
2.2 事件传递:通过 formProvider 联动卡片
检测到摇一摇后,主应用需要通过 formProvider.updateForm 将"摇一摇事件"转化为卡片可以消费的数据。这里的关键设计是:卡片的数据结构中增加动画控制字段,通过数据变化驱动动画启动。
typescript
import { formProvider, formBindingData } from '@kit.FormKit';
// 保存活跃的卡片 ID 列表
const activeFormIds: string[] = [];
function notifyCardsShakeDetected(): void {
const animationPayload = {
title: '', // 保持原标题(由卡片侧维护)
workIndex: '',
shakeTriggered: true, // 新增:摇一摇触发标记
animationType: 'breakout' // 新增:动画类型(出框/旋转/弹跳等)
};
for (const formId of activeFormIds) {
formProvider.updateForm(formId,
formBindingData.createFormBindingData(animationPayload));
}
}
2.3 卡片侧响应:数据驱动动画启动
卡片 UI 接收到包含 shakeTriggered: true 的数据后,立即启动预设的动画序列。动画执行完毕后,将 shakeTriggered 重置为 false,让卡片回到静止状态,等待下一次触发。
主应用 CreationFormAbility 桌面卡片 UI
│ │ │
│ 传感器检测到摇一摇 │ │
│ 判断加速度变化 > 阈值 │ │
│ │ │
│ updateForm(shakeTriggered) │ │
│ ──────────────────────────→ │ │
│ │ formProvider.updateForm │
│ │ ───────────────────────────→ │
│ │ │
│ │ │ 读取 shakeTriggered = true
│ │ │ 启动动画序列
│ │ │ → 标题弹跳/缩放
│ │ │ → 前景元素出框
│ │ │ → 光效闪烁
│ │ │
│ │ │ 动画结束,重置状态
│ │ │
2.4 出框动效(Foreground Breakout)的技术实现
"前景元素出框"是互动卡片最具视觉冲击力的效果------卡片的某个元素(如作品图标或装饰元素)突破卡片的圆角裁剪边界,呈现出"跳出来"的立体感。在 ArkUI 卡片中,这一效果通过调整组件的 clip 属性和 zIndex 层级实现:
typescript
// 卡片 UI 示意(CreationCard.ets)
@Component
struct CreationCard {
@LocalStorageProp('shakeTriggered') shakeTriggered: boolean = false;
@State elementScale: number = 1.0;
@State elementTranslateY: number = 0;
build() {
Stack() {
// 卡片背景(受裁剪)
Column()
.width('100%')
.height('100%')
.borderRadius(16)
.backgroundColor('#FFF8E7')
.clip(new Circle({ width: '100%', height: '100%' }))
// 前景出框元素(不受裁剪)
Image($r('app.media.ic_dinosaur'))
.width(48)
.height(48)
.scale({ x: this.elementScale, y: this.elementScale })
.translate({ y: this.elementTranslateY })
.zIndex(10) // 提升层级,浮于卡片之上
.clip(false) // 关闭裁剪,允许出框
.animation({
duration: 600,
curve: Curve.Spring,
iterations: 1
})
}
.width('100%')
.height('100%')
.clip(false) // 卡片容器不裁剪子元素
.onAppear(() => {
if (this.shakeTriggered) {
this.playBreakoutAnimation();
}
})
}
playBreakoutAnimation(): void {
// 出框动画序列:先弹出再弹回
this.elementScale = 1.4;
this.elementTranslateY = -12; // 向上弹出,突破边框
setTimeout(() => {
this.elementScale = 1.0;
this.elementTranslateY = 0;
}, 600);
}
}
三、互动卡片 API 详解
3.1 postCardAction:卡片与应用的通信桥梁
postCardAction 是动态卡片中最核心的交互 API。它允许卡片 UI 向宿主应用发送事件请求,包括路由跳转、数据请求、能力调用等。其典型用法如下:
typescript
// 在卡片 UI 中触发
Button('摇一摇看看')
.onClick(() => {
postCardAction(this, {
action: 'message',
data: { type: 'requestShakeAnimation' }
});
});
postCardAction 的第一个参数是组件上下文(this),第二个参数是一个事件描述对象,包含两个核心字段:
| 字段 | 类型 | 说明 | 示例值 |
|---|---|---|---|
| action | string |
事件类型 | "router" / "message" / "call" |
| bundleName | string |
目标应用包名(action 为 router 时可选) | "com.example.drawing" |
| abilityName | string |
目标 Ability 名(action 为 router 时可选) | "EntryAbility" |
| params | Record<string, Object> |
自定义参数(action 为 message 时用于传递数据) | { type: "requestShakeAnimation" } |
当 action: 'message' 时,postCardAction 发送的消息会由 CreationFormAbility.onFormEvent 接收,实现卡片→应用的逆向通信。
3.2 onFormEvent 的事件处理增强
第 4.7 篇中 onFormEvent 的实现非常简单------收到任何事件都执行一次数据刷新。在互动卡片场景下,需要对事件类型进行区分处理:
typescript
onFormEvent(formId: string, message: string): void {
// message 是 JSON 字符串,需要先解析
try {
const event = JSON.parse(message);
switch (event.action) {
case 'message':
this.handleCardMessage(formId, event.data);
break;
case 'router':
this.handleCardRouter(formId, event);
break;
default:
// 回退到默认刷新
this.updateCardData(formId);
}
} catch {
// 兼容旧版纯字符串消息
this.updateCardData(formId);
}
}
private handleCardMessage(formId: string, data: Record<string, Object>): void {
if (data?.type === 'requestShakeAnimation') {
// 卡片请求摇一摇动画数据------返回当前作品的最新动态效果参数
this.updateCardWithAnimation(formId, 'breakout');
}
}
private updateCardWithAnimation(formId: string, animType: string): void {
const cardData = this.getCardData();
formProvider.updateForm(formId,
formBindingData.createFormBindingData({
...cardData,
shakeTriggered: true,
animationType: animType
}));
}
3.3 formProvider.updateForm 的增强用法
在动态卡片中,formProvider.updateForm 不仅可以更新文本数据,还可以传递动画参数。卡片 UI 通过 @LocalStorageProp 接收这些参数,并通过 @State 或 @Animatable 驱动动画执行:
typescript
// 支持的数据结构扩展
interface CreationCardData {
title: string;
workIndex: string;
shakeTriggered?: boolean; // 摇一摇触发标记
animationType?: string; // 动画类型:'breakout' | 'bounce' | 'rotate' | 'sparkle'
animationDuration?: number; // 动画持续时间(毫秒)
animationIntensity?: number; // 动画强度系数(0.0 ~ 1.0)
}
四、与 CreationFormAbility 的融合
4.1 融合设计原则
将互动卡片能力融入现有的 CreationFormAbility,核心原则是兼容扩展------不破坏已有的静态卡片工作流,在上层叠加动态能力。具体来说:
- 向后兼容 :现有卡片 UI(不支持动态的版本)收到带
shakeTriggered的数据时,忽略该字段即可 - 按需激活 :动态能力仅在
isDynamic: true的配置下启用,不影响非动态卡片 - 渐进增强:静态卡片 → 动态卡片 → 摇一摇触发,三个层次逐步叠加
4.2 CreationFormAbility 的增强实现
基于第 4.7 篇的现有实现,我们进行互动卡片能力的增强:
typescript
export default class CreationFormAbility extends FormExtensionAbility {
// 保存活跃的卡片 ID,用于广播摇一摇事件
private activeFormIds: string[] = [];
onAddForm(want: Want): formBindingData.FormBindingData {
const formId = want.parameters?.['ohos.extra.param.key.formId'] as string;
if (formId && !this.activeFormIds.includes(formId)) {
this.activeFormIds.push(formId);
}
const data = this.getCardData();
return formBindingData.createFormBindingData({
...data,
shakeTriggered: false, // 初始状态:不触发动画
animationType: ''
});
}
onUpdateForm(formId: string): void {
const data = this.getCardData();
formProvider.updateForm(formId,
formBindingData.createFormBindingData({
...data,
shakeTriggered: false,
animationType: ''
}));
}
onFormEvent(formId: string, message: string): void {
try {
const event = JSON.parse(message);
if (event.action === 'message' && event.data?.type === 'shakeComplete') {
// 卡片动画播放完毕,重置状态
const data = this.getCardData();
formProvider.updateForm(formId,
formBindingData.createFormBindingData({
...data,
shakeTriggered: false,
animationType: ''
}));
return;
}
} catch { /* 兼容处理 */ }
// 默认行为:刷新数据
this.onUpdateForm(formId);
}
onRemoveForm(formId: string): void {
const idx = this.activeFormIds.indexOf(formId);
if (idx >= 0) {
this.activeFormIds.splice(idx, 1);
}
}
// 对外暴露:供主应用在检测到摇一摇时调用
public notifyShakeToAllForms(animType: string = 'breakout'): void {
for (const formId of this.activeFormIds) {
const data = this.getCardData();
formProvider.updateForm(formId,
formBindingData.createFormBindingData({
...data,
shakeTriggered: true,
animationType: animType,
animationDuration: 800,
animationIntensity: 1.0
}));
}
}
// 沿用原有方法
private getCardData(): CreationCardData { /* 与第 4.7 篇一致 */ }
private getLatestSavedWork(): VideoWork | undefined { /* 与第 4.7 篇一致 */ }
private getRandomWork(): CreationCardWork { /* 与第 4.7 篇一致 */ }
}
4.3 主应用的摇一摇监听注册
在主应用的入口(如 EntryAbility 或 Index.ets)中注册加速度传感器监听,并将检测到的摇一摇事件转发给 CreationFormAbility:
typescript
// 在 EntryAbility 中
import { sensor } from '@kit.SensorKit';
import { formProvider } from '@kit.FormKit';
import { common } from '@kit.AbilityKit';
// 摇一摇检测阈值
const SHAKE_THRESHOLD = 15;
const SHAKE_COOLDOWN = 1000; // 冷却时间,避免频繁触发
let lastShakeMs = 0;
let lastAccel = { x: 0, y: 0, z: 0 };
export function startShakeDetection(context: common.UIAbilityContext): void {
sensor.on(sensor.SensorId.ACCELEROMETER, (data) => {
const now = Date.now();
if (now - lastShakeMs < SHAKE_COOLDOWN) return;
const delta = Math.abs(data.x - lastAccel.x)
+ Math.abs(data.y - lastAccel.y)
+ Math.abs(data.z - lastAccel.z);
if (delta > SHAKE_THRESHOLD) {
lastShakeMs = now;
// 获取所有活跃卡片并更新
formProvider.getAllFormsInfo(context).then((forms) => {
for (const form of forms) {
formProvider.updateForm(form.formId,
formBindingData.createFormBindingData({
shakeTriggered: true,
animationType: 'breakout',
animationDuration: 800
}));
}
});
}
lastAccel = { x: data.x, y: data.y, z: data.z };
});
}
五、卡片动画性能优化
服务卡片运行在系统桌面进程中,其资源预算远低于应用主进程。不当的动画设计可能导致桌面卡顿,甚至触发系统的看门狗机制强制关闭卡片。以下是互动卡片动画的性能优化要点:
5.1 渲染管线约束
| 维度 | 静态卡片 | 动态卡片 | 优化建议 |
|---|---|---|---|
| 帧率 | 不涉及动画 | 目标 60fps | 避免同时驱动 3 个以上独立动画 |
| 组件数 | 无限制 | ≤ 20 个节点 | 减少嵌套层数,优先使用 Stack 而非 Column/Row 嵌套 |
| Canvas 绘制 | 不支持 | 支持但受限 | 每帧绘制面积不超过卡片面积的 30% |
| 图片解码 | 加载时解码 | 动画中避免解码 | 动画期间不更换 Image 的 src |
| 内存 | ~2MB | ~8MB 上限 | 动画资源(帧序列)控制在 1MB 以内 |
5.2 动画属性选择
ArkUI 中不同属性的动画开销差异显著。在卡片场景中,优先选择 GPU 合成的属性,避免触发 CPU 重排:
✅ 推荐(GPU 合成,零成本重绘)
├── transform(translate / scale / rotate)
├── opacity
└── clip(简单路径)
⚠️ 谨慎使用(触发局部重绘)
├── backgroundColor
├── borderRadius
└── borderWidth
❌ 避免使用(触发全量重绘)
├── 文字内容实时变化
├── 图片源动态切换
└── 布局属性(width / height / padding / margin)
5.3 动画时长与曲线
卡片动画的时长控制在 300ms ~ 1000ms 之间最为适宜。过短(<200ms)用户感知不到,过长(>1500ms)则显得拖沓,不符合桌面卡片"即看即走"的使用场景。
动画曲线推荐使用 Curve.Spring(弹性曲线)或 Curve.Friction(摩擦曲线),它们模拟真实物理运动,比线性或缓入缓出曲线更自然:
typescript
// 推荐:弹性弹跳效果
Image($r('app.media.ic_star'))
.scale({ x: this.scale, y: this.scale })
.animation({
duration: 600,
curve: Curve.Spring, // 弹性曲线,产生 bounce 效果
delay: 0,
iterations: 1
})
// 推荐:带摩擦的渐隐效果
Text('摇一摇查看新作品')
.opacity(this.textOpacity)
.animation({
duration: 400,
curve: Curve.Friction, // 摩擦曲线,平滑减速
delay: 200
})
5.4 动画降级策略
当设备性能不足时(如低端机型或高负载场景),应主动降级动画效果。可以通过 canIUse API 检测系统能力,或根据设备类型切换动画复杂度:
typescript
function getAnimationConfig(): AnimationConfig {
// 检测是否支持弹性动画
if (canIUse('SystemCapability.ArkUI.Animation.Spring')) {
return { duration: 600, curve: Curve.Spring };
}
// 降级:使用标准缓出曲线
return { duration: 400, curve: Curve.EaseOut };
}
六、卡片曝光与交互转化提升
互动卡片的核心商业价值在于:它让桌面卡片从"信息展示位"升级为"交互入口",从而显著提升卡片的曝光价值和交互转化率。
6.1 静态卡片的曝光困境
传统的静态服务卡片面临天然的交互瓶颈:
| 问题 | 表现 | 影响 |
|---|---|---|
| 信息疲劳 | 用户看桌面时视线自动跳过熟悉的内容 | 卡片有效曝光降低 40%~60% |
| 点击诱因不足 | 静态文字/图标不足以激发点击动机 | 点击率(CTR)通常低于 1% |
| 忽略惯性 | 用户形成"桌面卡片都是广告"的认知屏蔽 | 新添加的卡片 3 天后点击率趋近于 0 |
6.2 互动卡片的转化提升机制
摇一摇触发动态效果通过三个机制破局:
① 意外惊喜(Surprise & Delight)
用户摇动手机是一个主动行为,卡片随之"活"过来,形成因果反馈。这种意外惊喜会激活用户的积极情绪,大幅提升对卡片的关注度。实测数据显示,带有动画反馈的服务卡片,用户停留时长提升 3~5 倍。
② 视觉锚点(Visual Anchor)
动态元素在静态的桌面上天然具有视觉优先级------人眼对运动的敏感度是静止物体的数十倍。即便用户没有主动摇一摇,卡片在一次动画播放后恢复静态,也已经在用户视野中建立了一个"视觉锚点",下次扫过桌面时更容易被注意到。
③ 交互引导(Interaction Cue)
出框动效打破了"内容被框在卡片里"的心理预期,暗示用户"这个卡片是可以互动的"。这直接降低了用户的交互门槛------从"被动观看"转变为"主动探索"。
6.3 数据模型:互动卡片效果衡量
为了量化互动卡片的效果,建议建立以下指标体系:
| 指标 | 定义 | 衡量方式 |
|---|---|---|
| 卡片曝光率 | 桌面卡片被用户看到的频率 | 通过 onAcquireFormState 回调频次估算 |
| 动画触发率 | 摇一摇触发动画的次数 / 设备使用时长 | 后端埋点记录 shakeTriggered 推送次数 |
| 卡片点击率 | 从卡片跳转到应用的次数 / 卡片曝光次数 | postCardAction 中 action: 'router' 的统计 |
| 交互转化率 | 查看卡片后进入应用并开始创作的比率 | 结合应用内埋点,追踪路由参数中的 workIndex |
七、静态卡片与互动卡片对比
为了更直观地呈现互动卡片的优势,我们从多个维度对比"画伴梦工厂"中前后两代卡片方案:
| 对比维度 | 静态卡片(第 4.7 篇) | 互动卡片(本文) |
|---|---|---|
| form_config | isDynamic: false |
isDynamic: true |
| 数据模型 | { title, workIndex } |
{ title, workIndex, shakeTriggered, animationType, ... } |
| UI 能力 | Text + Image 静态布局 | 支持帧动画、Canvas、弹性动效 |
| 交互方式 | 仅点击跳转 | 点击 + 摇一摇 + 触摸手势 |
| 事件链路 | 单向:Ability → 卡片 | 双向:Ability ↔ 卡片(通过 postCardAction) |
| 动画触发 | 无 | 传感器事件 / 定时触发 / 用户手势 |
| 性能开销 | 低(<1MB 内存) | 中(~4MB 内存,需动画调度线程) |
| 开发复杂度 | 低(约 90 行代码) | 中(约 200 行代码,含传感器和动画逻辑) |
| 用户停留 | 约 2 秒/次 | 约 8~15 秒/次(含动画观赏时间) |
| 预估点击率 | < 1% | 3%~8%(动画带来的交互引导效应) |
从上表可以看出,互动卡片以适度的性能开销和开发增量 ,换取了数量级的体验提升。对于"画伴梦工厂"这类以创意和趣味为核心的儿童应用,这一投入的回报是显著的。
八、卡片交互设计原则
互动卡片的设计并非"把应用内的动效搬进卡片",而是需要在桌面级的交互限制下重新思考。以下是总结的六条核心设计原则:
原则一:尊重克制(Respectful Restraint)
桌面卡片的本质是"信息快照",而非"迷你应用"。动画应当是点睛之笔 而非常态渲染。摇一摇触发的动画播放一次即恢复静态,避免轮播动画干扰用户的桌面浏览。
原则二:因果反馈(Causal Feedback)
每次动画都应当有明确的触发原因。摇一摇→卡片动,这个因果关系清晰明确。如果卡片无缘无故自动播放动画,用户会感到困惑甚至反感。
原则三:轻量轻盈(Lightweight Liveness)
卡片动画应当是"轻量级"的------持续 500~800ms、影响 1~2 个元素、不改变卡片的核心信息结构。重度的动画(如全卡片翻转、复杂 Canvas 粒子)不适合在卡片场景中使用。
原则四:渐进增强(Progressive Enhancement)
以静态卡片为基线,逐步叠加动态能力:
- 第一层:静态文本 + 图标(基础能力)
- 第二层:数据驱动刷新(第 4.7 篇)
- 第三层:交互事件响应(点击 → 跳转)
- 第四层:传感器联动动画(本文)
每一层向下兼容,上层不可用时自然降级。
原则五:性能优先(Performance First)
在卡片开发中,性能优先级高于视觉效果。如果某个动画效果在低端机型上无法达到 45fps 以上,就应该直接移除而非"将就"。
原则六:情感连接(Emotional Connection)
对于儿童创作类应用,卡片动画的核心目的不是"炫技",而是建立情感连接------让孩子感受到"我的作品被重视了",让家长感受到"孩子创作是一件值得骄傲的事"。出框的小恐龙、跳动的星星、渐变的彩虹------这些效果的每一个像素,都在传递同一个信息:"你的创作,闪闪发光。"
九、应用场景:儿童绘画应用中的互动卡片
9.1 场景一:最新作品动效展示
当用户摇动手机时,卡片上展示的最新画作标题伴随着装饰元素动画效果呈现:
- 作品图标 :从卡片底部弹跳进入视图(
translate + scale组合动画) - 标题文字:逐字显现,带微弱的透明度渐变
- 装饰元素 :星星/小花围绕标题旋转(
rotate动画) - 背景光晕 :柔和的颜色渐变脉冲(
opacity动画)
这一场景的核心价值在于:让每一次"查看桌面"都变成一次小小的仪式感体验。
9.2 场景二:创作提醒与情感回馈
当孩子在应用内完成了新的画作后,主应用主动推送一次"软摇一摇"动画------无需用户摇动手机,卡片自动播放一次温和的动画,配合文字提示"新作品已生成!"。这替代了传统 Push 通知的生硬体验,让创作反馈变得更加自然和温暖。
9.3 场景三:节日主题彩蛋
在特定节日(如儿童节、春节),卡片可以预设节日主题的动画效果。用户在摇动手机时,卡片除了展示作品信息外,还会出现节日元素(如春节的灯笼、儿童节的气球),营造节日的仪式感。这通过 animationType 参数区分不同主题实现:
typescript
// 主应用根据日期选择动画主题
function getHolidayAnimationType(): string {
const today = new Date();
const month = today.getMonth() + 1;
const day = today.getDate();
if (month === 6 && day === 1) return 'childrens_day';
if (month === 1 && day >= 28 && day <= 31) return 'spring_festival';
if (month === 10 && day === 1) return 'national_day';
return 'breakout'; // 默认动画
}
9.4 场景四:作品合集轮播
对于有多个作品的用户,摇一摇不仅触发动画,还同时切换到下一个作品展示,形成"摇一摇→新作品→新动画"的循环体验。这需要在 onFormEvent 中记录当前展示的作品索引,并在每次摇一摇时递增:
typescript
// 在 CreationFormAbility 中维护每个卡片的作品索引
private formWorkIndexMap: Map<string, number> = new Map();
notifyShakeToForm(formId: string): void {
const currentIdx = this.formWorkIndexMap.get(formId) ?? 0;
const works = this.getAllWorks();
const nextIdx = (currentIdx + 1) % works.length;
this.formWorkIndexMap.set(formId, nextIdx);
formProvider.updateForm(formId,
formBindingData.createFormBindingData({
title: works[nextIdx].title,
workIndex: String(nextIdx + 1),
shakeTriggered: true,
animationType: 'breakout'
}));
}
十、互动卡片开发最佳实践
10.1 谨防动画过度
互动卡片最容易被忽视的风险是动画疲劳------过多的动画元素和过长的动效时间会导致用户反感。建议遵循"1-2-3 原则":
- 1 个核心动画元素:每次只让一个主要元素(如图标)做大幅运动
- 2 个辅助效果:配合不超过 2 个微动效(如光晕、文字微动)
- 3 秒冷却:单张卡片的动画在 3 秒内不会重复触发
10.2 生命周期资源管理
摇一摇传感器的监听会持续消耗电量和 CPU。务必在应用进入后台时暂停传感器监听,回到前台时重新注册:
typescript
// EntryAbility.ets
onForeground(): void {
startShakeDetection(this.context);
}
onBackground(): void {
sensor.off(sensor.SensorId.ACCELEROMETER);
}
10.3 formId 列表的持久化
activeFormIds 列表在 CreationFormAbility 进程被系统回收后会丢失。如需在进程重启后恢复卡片列表,可将 formId 存入 preferences:
typescript
private saveActiveFormIds(): void {
const store = preferences.getPreferencesSync(this.context, { name: 'form_ids' });
store.putSync('ids', JSON.stringify(this.activeFormIds));
store.flushSync();
}
10.4 使用 @Animatable 优化关键帧动画
对于需要精细控制的关键帧动画,推荐使用 @Animatable 装饰器,它比手动 setTimeout 链式调用更高效:
typescript
@Animatable({ from: 0, to: 100, duration: 600, curve: Curve.Spring })
progress: number = 0;
// 在 build 中使用 progress 驱动多个属性
build() {
Image($r('app.media.ic_rocket'))
.scale({
x: 1 + this.progress / 200,
y: 1 + this.progress / 200
})
.rotate({ angle: this.progress * 3.6 })
.translate({
y: -this.progress * 0.5
})
}
10.5 测试与调试
互动卡片的动画效果在 DevEco Studio 的预览器中无法完整模拟------传感器事件和真实的桌面渲染只能在真机上验证。建议测试流程:
- DevEco Studio 预览器:验证卡片 UI 布局和数据绑定
- 模拟器 :验证
postCardAction事件路由 - 真机:验证摇一摇传感器阈值和动画流畅度
10.6 降级兼容
对于运行在 API 25 及以下系统的设备,isDynamic 字段会被忽略,卡片以静态模式运行。确保卡片 UI 在未收到 shakeTriggered 字段时仍然能正常展示标题和图标------降级后至少不失基本功能。
总结
本文围绕"互动卡片------摇一摇触发动态效果"这一主题,从技术架构到设计实践,完整覆盖了 HarmonyOS 7 互动卡片的开发全流程:
| 知识点 | 实现方式 |
|---|---|
| 动态卡片配置 | form_config.json 中设置 isDynamic: true |
| 摇一摇检测 | @kit.SensorKit 加速度传感器,阈值判断 |
| 事件传递链路 | 传感器 → 主应用 → formProvider.updateForm → 卡片 UI |
| 动画驱动 | 数据驱动:shakeTriggered 字段触发卡片内动画序列 |
| 出框动效 | clip: false + zIndex 提升,突破卡片边界渲染 |
| CreationFormAbility 增强 | 新增 notifyShakeToAllForms、维护 activeFormIds、事件分发 |
| 性能优化 | GPU 合成属性优先、动画时长 300~1000ms、降级策略 |
| 转化提升 | 意外惊喜 + 视觉锚点 + 交互引导,提升曝光与点击率 |
| 设计原则 | 尊重克制 → 因果反馈 → 轻量轻盈 → 渐进增强 → 性能优先 → 情感连接 |
| 最佳实践 | 动画"1-2-3 原则"、传感器生命周期管理、formId 持久化、降级兼容 |
互动卡片代表了 HarmonyOS 服务卡片从"信息展示"到"交互体验"的范式跃迁。在"画伴梦工厂"的场景中,它让孩子的每一次创作都能以最生动的方式呈现在桌面上------不是静态的文字标题,而是一个会"动"的、有"生命感"的数字存在。当用户轻轻摇动手机,卡片上的小恐龙破框而出,那一刻的惊喜,正是技术赋予产品最温暖的温度。
参考源码
本文所有代码均基于项目现有文件扩展:
products/default/src/main/ets/creationformability/CreationFormAbility.ets--- 原有服务卡片 ExtensionAbility,本文展示了如何增强其互动卡片能力products/default/form_config.json--- 服务卡片配置文件,通过isDynamic: true启用动态卡片模式@kit.SensorKit--- 加速度传感器 API,用于摇一摇检测@kit.FormKit(formProvider / formBindingData / postCardAction)--- 互动卡片核心 API