HarmonyOS开发:卡片交互——点击跳转与消息事件
HarmonyOS开发:卡片交互——点击跳转与消息事件
📌 核心要点:卡片交互有三种模式——router跳转、call调用、message消息,分别对应"打开页面"、“调用方法”、"发送通知"三种交互场景。
背景与动机
卡片光能看不行,还得能点。
用户看到天气卡片上显示"26° 晴",想看未来几天的预报——点一下,跳到天气App的预报页面。看到待办卡片上显示"3项待办",想看具体是哪3项——点一下,跳到待办列表。看到音乐卡片上显示"正在播放",想暂停——点一下,暂停播放。
这些交互怎么实现?鸿蒙提供了三种卡片交互模式:
- router:点击卡片跳转到UIAbility的指定页面
- call:点击卡片调用UIAbility的指定方法(后台执行,不跳转页面)
- message:点击卡片向FormAbility发送消息
三种模式适用不同场景。选错了,交互体验就很别扭——用户点"暂停"结果跳到了新页面,点"查看详情"结果什么都没发生。
核心原理
三种交互模式对比
flowchart TB
A[用户点击卡片] --> B{交互模式}
B -->|router| C[拉起UIAbility]
C --> D[携带Want参数]
D --> E[UIAbility.onNewWant接收]
E --> F[跳转到指定页面]
B -->|call| G[拉起UIAbility<br/>后台模式]
G --> H[携带method参数]
H --> I[UIAbility.onNewWant接收]
I --> J[执行指定方法]
B -->|message| K[发送消息到FormAbility]
K --> L[FormAbility.onFormEvent接收]
L --> M[处理消息逻辑]
M --> N[更新卡片数据]
classDef userAction fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
classDef router fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
classDef call fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef message fill:#FCE4EC,stroke:#C62828,color:#B71C1C
class A userAction
class C,D,E,F router
class G,H,I,J call
class K,L,M,N message
| 交互模式 | 是否跳转页面 | 接收方 | 适用场景 |
|---|---|---|---|
| router | ✅ | UIAbility | 查看详情、打开列表 |
| call | ❌ | UIAbility | 后台操作(刷新数据、同步) |
| message | ❌ | FormAbility | 卡片内操作(切换状态、标记已读) |
router模式详解
router是最常用的交互模式。用户点击卡片的某个区域,系统拉起你的UIAbility,并携带Want参数。你在UIAbility的onCreate或onNewWant里解析Want参数,跳转到对应页面。
// 卡片侧:设置router动作
Button('查看详情')
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
targetPage: 'detail', // 自定义参数:要跳转的页面
itemId: '12345' // 自定义参数:项目ID
}
})
})
call模式详解
call模式和router类似,也是拉起UIAbility,但不会显示界面。适合后台操作——比如"刷新数据"、“同步设置”。
// 卡片侧:设置call动作
Button('刷新')
.onClick(() => {
postCardAction(this, {
action: 'call',
abilityName: 'EntryAbility',
params: {
method: 'refreshData' // 要调用的方法名
}
})
})
message模式详解
message模式不拉起UIAbility,而是直接发送消息给FormAbility。适合卡片内的轻量操作——比如"标记已读"、“切换状态”。
// 卡片侧:设置message动作
Button('标记已读')
.onClick(() => {
postCardAction(this, {
action: 'message',
params: {
action: 'markRead',
messageId: '12345'
}
})
})
代码实战
基础用法:待办卡片的router跳转
最常见的交互:点击卡片跳转到App的详情页。
卡片布局:
// TodoCard.ets
@Entry
@Component
struct TodoCard {
@State todoCount: string = '0'
@State latestTodo: string = '暂无待办'
@State dateStr: string = ''
build() {
Column() {
// 日期
Text(this.dateStr)
.fontSize(12)
.fontColor('#999999')
// 待办数量
Row() {
Text(this.todoCount)
.fontSize(32)
.fontColor('#FF6B35')
.fontWeight(FontWeight.Bold)
Text('项待办')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 4, top: 14 })
}
.margin({ top: 8 })
// 最近一条待办
Text(this.latestTodo)
.fontSize(12)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
// 查看全部按钮
Button('查看全部')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#FF6B35')
.borderRadius(16)
.height(28)
.margin({ top: 12 })
.onClick(() => {
// router跳转——点击后打开App的待办列表页
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
targetPage: 'todoList',
date: this.dateStr
}
})
})
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
UIAbility侧接收跳转:
// EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { hilog } from '@kit.PerformanceAnalysisKit'
export default class EntryAbility extends UIAbility {
// 首次创建时调用
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'EntryAbility', 'onCreate')
this.handleRouterAction(want)
}
// 已有实例时调用(singleInstance模式)
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'EntryAbility', 'onNewWant')
this.handleRouterAction(want)
}
// 处理router跳转
private handleRouterAction(want: Want): void {
const params = want.parameters
if (!params) return
const targetPage = params.targetPage as string
switch (targetPage) {
case 'todoList':
// 跳转到待办列表页
this.navigateToPage('TodoListPage', {
date: params.date as string
})
break
case 'todoDetail':
// 跳转到待办详情页
this.navigateToPage('TodoDetailPage', {
todoId: params.todoId as string
})
break
default:
// 默认跳转首页
this.navigateToPage('Index', {})
}
}
private navigateToPage(pageName: string, params: Record<string, string>): void {
// 通过AppStorage或全局状态管理传递页面参数
AppStorage.setOrCreate('targetPage', pageName)
AppStorage.setOrCreate('pageParams', JSON.stringify(params))
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'EntryAbility', `加载失败: ${JSON.stringify(err)}`)
return
}
})
}
}
页面侧根据参数跳转:
// Index.ets
@Entry
@Component
struct Index {
@StorageProp('targetPage') targetPage: string = ''
@StorageProp('pageParams') pageParams: string = ''
// 页面路由栈
private navPathStack: NavPathStack = new NavPathStack()
build() {
Navigation(this.navPathStack) {
// 首页内容
Column() {
Text('待办应用首页')
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
.onAppear(() => {
// 检查是否需要跳转
this.checkAndNavigate()
})
}
private checkAndNavigate(): void {
if (this.targetPage === 'todoList') {
const params = JSON.parse(this.pageParams || '{}')
this.navPathStack.pushPath({
name: 'TodoListPage',
param: params
})
// 清除标记
AppStorage.setOrCreate('targetPage', '')
} else if (this.targetPage === 'todoDetail') {
const params = JSON.parse(this.pageParams || '{}')
this.navPathStack.pushPath({
name: 'TodoDetailPage',
param: params
})
AppStorage.setOrCreate('targetPage', '')
}
}
}
进阶用法:消息卡片的message交互
消息卡片需要"标记已读"功能——点击后不跳转页面,只更新卡片状态。
// MessageCard.ets
@Entry
@Component
struct MessageCard {
@State unreadCount: string = '0'
@State latestMessage: string = '暂无消息'
@State sender: string = ''
@State isRead: string = 'false'
build() {
Column() {
// 未读数量
Row() {
Text(this.unreadCount)
.fontSize(24)
.fontColor('#FF6B35')
.fontWeight(FontWeight.Bold)
Text('条未读')
.fontSize(12)
.fontColor('#666666')
.margin({ left: 4, top: 8 })
Blank()
// 查看全部——router跳转
Text('查看全部 >')
.fontSize(12)
.fontColor('#2196F3')
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { targetPage: 'messageList' }
})
})
}
.width('100%')
// 最新消息
Row() {
Column() {
Text(this.sender)
.fontSize(12)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
Text(this.latestMessage)
.fontSize(11)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 标记已读按钮——message模式
if (this.isRead === 'false') {
Button('已读')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#4CAF50')
.borderRadius(12)
.height(24)
.width(48)
.onClick(() => {
postCardAction(this, {
action: 'message',
params: {
action: 'markRead',
messageId: this.getMessageId()
}
})
})
}
}
.width('100%')
.margin({ top: 12 })
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
private getMessageId(): string {
// 从卡片数据中获取消息ID
return 'msg_001'
}
}
FormAbility侧处理message:
// MessageFormAbility.ets
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
export default class MessageFormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
return formBindingData.createFormBindingData({
unreadCount: '3',
latestMessage: '会议时间已调整到下午3点',
sender: '张三',
isRead: 'false'
})
}
onUpdateForm(formId: string): formBindingData.FormBindingData {
return this.onAddForm(null)
}
onRemoveForm(formId: string): void {
hilog.info(0x0000, 'MsgForm', `卡片删除: ${formId}`)
}
// 处理message事件
onFormEvent(formId: string, message: string): void {
hilog.info(0x0000, 'MsgForm', `收到消息: ${formId}, ${message}`)
try {
const msg = JSON.parse(message)
switch (msg.action) {
case 'markRead':
this.handleMarkRead(formId, msg.messageId)
break
case 'delete':
this.handleDelete(formId, msg.messageId)
break
default:
hilog.warn(0x0000, 'MsgForm', `未知操作: ${msg.action}`)
}
} catch (err) {
hilog.error(0x0000, 'MsgForm', `消息解析失败: ${err}`)
}
}
// 标记已读
private handleMarkRead(formId: string, messageId: string): void {
// 1. 更新数据库
this.markMessageReadInDB(messageId)
// 2. 获取新的未读数量
const newUnreadCount = this.getUnreadCount()
// 3. 获取新的最新消息
const latestMsg = this.getLatestUnreadMessage()
// 4. 更新卡片
const data = {
unreadCount: newUnreadCount.toString(),
latestMessage: latestMsg.content,
sender: latestMsg.sender,
isRead: 'true'
}
formProvider.updateForm(formId, formBindingData.createFormBindingData(data))
.then(() => {
hilog.info(0x0000, 'MsgForm', '卡片已更新')
})
.catch((err: Error) => {
hilog.error(0x0000, 'MsgForm', `更新失败: ${err.message}`)
})
}
private markMessageReadInDB(messageId: string): void {
// 更新数据库中的消息状态
hilog.info(0x0000, 'MsgForm', `标记已读: ${messageId}`)
}
private getUnreadCount(): number {
// 从数据库查询未读数量
return 2 // 模拟
}
private getLatestUnreadMessage(): { content: string; sender: string } {
return { content: '请确认明天的会议安排', sender: '李四' }
}
private handleDelete(formId: string, messageId: string): void {
// 删除消息逻辑
hilog.info(0x0000, 'MsgForm', `删除消息: ${messageId}`)
}
}
完整示例:深链接跳转
深链接(Deep Link)是卡片交互的高级用法。你可以在form_config.json里配置deepLink,用户点击卡片时自动跳转到App的指定页面,不需要在onClick里手动调用postCardAction。
// form_config.json
{
"forms": [{
"name": "orderCard",
"displayName": "我的订单",
"src": "./ets/widget/pages/OrderCard.ets",
"deepLink": "myapp://order/detail",
"defaultDimension": "2*4",
"supportDimensions": ["2*4", "4*4"]
}]
}
配置了deepLink后,用户点击卡片的任何区域(没有绑定onClick的区域),都会触发deepLink跳转。
// OrderCard.ets —— 订单追踪卡片
@Entry
@Component
struct OrderCard {
@State orderNo: string = ''
@State status: string = ''
@State location: string = ''
@State estimatedTime: string = ''
@State progress: number = 0
build() {
Column() {
// 订单号
Row() {
Text(this.orderNo)
.fontSize(12)
.fontColor('#999999')
Blank()
// 复制订单号——使用message模式,不触发deepLink
Text('复制')
.fontSize(11)
.fontColor('#2196F3')
.onClick(() => {
postCardAction(this, {
action: 'message',
params: { action: 'copyOrderNo', orderNo: this.orderNo }
})
})
}
.width('100%')
// 物流状态
Text(this.status)
.fontSize(16)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
.margin({ top: 8 })
// 当前位置
Row() {
Text('📍')
.fontSize(14)
Text(this.location)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 4 })
}
.margin({ top: 4 })
// 进度条
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.width('100%')
.color('#4CAF50')
.margin({ top: 12 })
// 预计到达
Row() {
Text(`预计 ${this.estimatedTime} 送达`)
.fontSize(11)
.fontColor('#999999')
Blank()
// 查看详情——router模式,携带订单号
Text('详情 >')
.fontSize(12)
.fontColor('#2196F3')
.onClick(() => {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: {
targetPage: 'orderDetail',
orderNo: this.orderNo
}
})
})
}
.width('100%')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
UIAbility侧处理深链接:
// EntryAbility.ets
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.handleDeepLink(want)
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.handleDeepLink(want)
}
private handleDeepLink(want: Want): void {
// 处理deepLink
const uri = want.uri || want.parameters?.['ohos.extra.param.key.form_identity'] as string
if (uri && uri.startsWith('myapp://')) {
const path = uri.replace('myapp://', '')
switch (path) {
case 'order/detail':
this.navigateToPage('OrderDetailPage', {})
break
case 'order/list':
this.navigateToPage('OrderListPage', {})
break
}
}
// 处理router参数
const params = want.parameters
if (params?.targetPage) {
this.navigateToPage(params.targetPage as string, params as Record<string, string>)
}
}
private navigateToPage(page: string, params: Record<string, string>): void {
AppStorage.setOrCreate('targetPage', page)
AppStorage.setOrCreate('pageParams', JSON.stringify(params))
}
}
踩坑与注意事项
坑1:postCardAction只能在onClick里调用
postCardAction只能在UI组件的onClick回调里调用,不能在onAppear、定时器、异步回调里调用。
// ❌ 错误:在onAppear里调用
onAppear(() => {
postCardAction(this, { action: 'router', params: {} }) // 报错!
})
// ❌ 错误:在定时器里调用
setInterval(() => {
postCardAction(this, { action: 'message', params: {} }) // 报错!
}, 1000)
// ✅ 正确:在onClick里调用
Button('点击')
.onClick(() => {
postCardAction(this, { action: 'router', params: {} })
})
坑2:router跳转需要UIAbility配置launchType
如果你的UIAbility的launchType是singleton(单实例),卡片点击跳转时会走onNewWant而不是onCreate。你必须同时处理两个回调。
// module.json5
{
"abilities": [{
"name": "EntryAbility",
"launchType": "singleton", // 单实例模式
...
}]
}
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 首次创建时调用
this.handleRouterAction(want)
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 单实例模式下,后续跳转都走这里
this.handleRouterAction(want)
}
private handleRouterAction(want: Want): void {
// 统一处理
}
}
坑3:message事件的数据大小限制
message模式发送的数据不能超过1KB。如果你需要传大量数据,用router模式跳转到UIAbility处理。
// ❌ 数据太大
postCardAction(this, {
action: 'message',
params: {
action: 'update',
data: veryLargeJsonObject // 可能超过1KB
}
})
// ✅ 只传关键信息,在FormAbility里查询完整数据
postCardAction(this, {
action: 'message',
params: {
action: 'update',
itemId: '12345' // 只传ID
}
})
坑4:call模式需要申请后台权限
call模式会在后台拉起UIAbility,但系统对后台启动有严格限制。你需要在module.json5里申请ohos.permission.KEEP_BACKGROUND_RUNNING权限,否则call模式可能被系统拒绝。
// module.json5
{
"requestPermissions": [{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}]
}
坑5:卡片区域点击优先级
如果卡片同时配置了deepLink和onClick,onClick的优先级更高。点击绑定了onClick的区域,触发onClick;点击未绑定onClick的区域,触发deepLink。
// 整个卡片配置了deepLink
// 但"复制"按钮绑定了onClick
// 点击"复制"→ 触发onClick(message模式)
// 点击其他区域 → 触发deepLink(跳转到订单详情)
HarmonyOS 6适配说明
- postCardAction增强:6.0新增了
call模式的直接方法调用。你可以在params里指定method,UIAbility会直接调用对应的方法,不需要在onNewWant里手动解析:
// 6.0新增的call模式
postCardAction(this, {
action: 'call',
abilityName: 'EntryAbility',
params: {
method: 'refreshData', // 直接调用UIAbility的refreshData方法
args: { type: 'weather' }
}
})
- 深链接支持通配符:6.0的deepLink支持路径参数,类似Web路由:
{
"deepLink": "myapp://order/{orderId}"
}
卡片点击时,系统会自动将orderId替换为实际值,你在UIAbility里解析即可。
- 交互事件统计:6.0新增了卡片交互事件的统计API,你可以获取卡片的点击次数、跳转成功率等数据:
import { formProvider } from '@kit.AbilityKit'
// 获取卡片交互统计
const stats = await formProvider.getFormInteractionStats(formId)
console.log(`点击次数: ${stats.clickCount}`)
console.log(`跳转成功: ${stats.routerSuccessCount}`)
- 多区域独立交互:6.0支持在同一个卡片内为不同区域设置不同的交互动作,不再需要整个卡片共用一个deepLink。
总结
卡片交互是让卡片从"信息展示"升级为"功能入口"的关键。router跳转打开页面,call调用后台方法,message发送轻量消息——三种模式各司其职,选对模式才能给用户最好的体验。
核心要点回顾:
- router模式:跳转页面,适合"查看详情"场景
- call模式:后台执行,适合"刷新数据"场景
- message模式:卡片内操作,适合"标记已读"场景
- postCardAction只能在onClick里调用
- 单实例UIAbility要同时处理onCreate和onNewWant
- message数据不超过1KB,call模式需要后台权限
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐ 三种模式概念简单,但配合使用需要经验 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每个卡片都需要交互 |
| 重要程度 | ⭐⭐⭐⭐⭐ 没有交互的卡片就是张图片 |
下一篇聊卡片UI设计——多尺寸适配、响应式布局、设计规范,让卡片好看又好用。
- 点赞
- 收藏
- 关注作者
评论(0)