ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸿蒙服务卡片开发实战:用音乐卡片跑通桌面控制完整链路

鸿蒙服务卡片开发实战:用音乐卡片跑通桌面控制完整链路 简介面向鸿蒙应用开发者这是一份音乐服务卡片实践项目主要解决在HarmonyOS中从零搭建桌面万能卡片的问题适合初中级开发者参考。资源共60个文件包含Java与JavaScript两种语言的源代码、JSON格式的配置文件、HML和XML页面描述、CSS样式以及PNG、JPG图片素材压缩包约18.24MB目录结构清晰便于按模块查找并阅读。项目围绕服务卡片的界面搭建、生命周期管理、播放状态同步、手势交互与动画效果展开同时涉及分布式设备协同和多端适配能力有助于理解卡片在手机、手表等不同设备间的运行机制。内附Gradle构建脚本及工程配置文件可直接导入DevEco Studio运行调试并提供了示例音频与图片资源便于边看边练。目前已有414人学习下载适合正在学习鸿蒙服务卡片或准备构建音乐类元服务的开发者学习使用。1. 拿到这个 zip你其实拿到了一条完整的卡片链路“鸿蒙开发的音乐服务卡片.zip”这个名字看着像一个压缩包但打开之后你会发现它更像一个已经被验证过的桌面服务卡片样板工程。鸿蒙开发里最容易被高估的是界面最容易被低估的恰恰是服务卡片这条链路从 form_config.json 里的网格规格到 FormExtensionAbility 的生命周期再到点击事件怎么回传、播放状态怎么刷新任何一个环节对不上卡片在桌面上就是一张不会说话的图片。这个 zip 解决的就是这件事把你的播放器能力挂到桌面让你不用解锁手机就能切歌、暂停、看当前曲目。这个东西适合谁适合已经能独立写完一个鸿蒙应用页面、但还没碰过服务卡片的开发者。也适合那些正在评估“鸿蒙应用开发要不要上卡片”的团队——先拿这个工程跑通再决定投入。音乐场景是最典型的服务卡片用例控制类卡片比纯展示卡片复杂一个台阶因为它涉及双向通信和状态同步。所以把它啃下来其他类型的卡片基本就是改配置的问题了。我见过不少人下载了这类 zip 以后卡在第一步不知道先看哪个文件、不知道用什么设备跑、不知道卡片为什么拉不起来。这篇就把这条链路完整捋一遍。2. 从解压到桌面出现卡片先把工程跑起来再谈理解2.1 打开工程先认这四个文件别急着点 Run我拿到一个服务卡片相关工程不会先跑而是先把目录结构扫一遍。一个标准的鸿蒙服务卡片工程会围绕四个文件展开。第一个是resources/base/profile/form_config.json它决定了这个卡片能有多大、可不可以定时刷新、默认用哪个尺寸。第二个是卡片的 UI 文件一般在ets/card/或ets/component/下ArkTS 卡片就认这个文件里的Entry组件。第三个是FormExtensionAbility的入口文件它负责回应系统发来的“创建卡片”“更新卡片”“移除卡片”等事件。第四个是module.json5里的extensionAbilities配置节点它把上面三个文件串起来少了这个节点系统根本不会把你的代码当卡片加载。你不需要一开始把每个文件都读懂但建议按这个顺序读先读module.json5里 form 相关的extensionAbilities再读form_config.json然后读FormExtensionAbility最后才是卡片 UI。因为前两个是“系统怎么看待这张卡片”后两个是“卡片自己怎么干活”。我见过有同学一上来就改卡片 UI改完发现桌面上拉出来的还是旧样子——大概率是src路径配错了改了 UI 文件但form_config.json里还指着旧文件。如果工程里带了AppScope/目录那是应用级的配置和服务卡片本身关系不大可以先放着。真正要注意的是entry模块下的oh-package.json5里面声明了依赖。有的卡片工程会依赖ohos.app.form.FormExtensionAbility或新版 SDK 里的kit.FormKit依赖没同步好后面会有一堆红波浪线。2.2 跑卡片的三个前置条件真机、签名、安装方式服务卡片和普通页面最大的不同在于它不是你 App 里的一个页面而是由系统桌面进程拉起来的一块独立 UI。这就导致一个现实问题——鸿蒙模拟器对服务卡片的支持非常有限。你在模拟器里把工程跑起来应用列表里能看到应用但长按图标大概率拉不出卡片或者拉出来是白屏。这不是你代码的问题是模拟器环境里的 Form 服务不完整。所以做服务卡片请直接准备一台 HarmonyOS NEXT 真机手机和平板都行hdc都能连。签名方面个人开发者在 DevEco Studio 里登录华为账号开启自动签名就能往真机上装hap包。卡片调试不需要企业证书自动签名就够用。连接好设备后先用hdc list targets确认设备被识别再选择对应的真机运行配置。如果设备列表是空的检查一下开发者模式和 USB 调试有没有开hdc连不上鸿蒙平板或手机时九成是这个问题。安装有两种路径。一种是 DevEco Studio 直接点 Run它会自动签名、打包并安装entry-default-signed.hap。另一种是命令行手动装适合需要在多台设备上验证的情况hdc list targets hdc install entry-default-signed.haphdc install后面跟的是 hap 包的路径注意是signed的那个。没签名的包装不上系统会直接拒绝。装完之后回到桌面长按应用图标如果配置了卡片预览会在弹出的菜单里看到这张音乐卡片没有预览的话去“服务卡片”里手动添加。注意HDC 连接失败时,先看设备是否弹出“允许 USB 调试”的授权框鸿蒙设备每次重连都可能重新询问。没点允许的话hdc list targets永远只显示一个空列表。2.3 卡片和主应用的关系同一个包两条命跑起来之后你会在应用列表里看到两个东西一个是你的主应用图标一个是可以在桌面上添加的卡片。它们来自同一个包但运行方式完全不同。主应用是普通的 UIAbility而卡片是由FormExtensionAbility提供的独立能力。长按桌面添加卡片时系统实际上是向应用的FormExtensionAbility发了一个“创建卡片”的请求卡片创建成功后它的 UI 渲染和事件处理都走的是另一套生命周期。理解了这个关系你就知道为什么 “卡片点击按钮后直接操作播放器” 这件事没有想象中简单。卡片 UI 里的按钮点击发出去的只是一条消息真正干活的是应用侧。这也是服务卡片最核心的交互模型卡片只负责“展示和收集意图”应用负责“执行和回传状态”。后面改造音乐功能时所有代码都会围绕这个模型展开。3. 读源码的两条主线一张卡片是怎么被“画”出来和“养”起来的3.1 form_config.json尺寸、刷新和入口的“总开关”服务卡片不是随随便便想多大就多大。鸿蒙定义了标准的网格尺寸form_config.json里的supportDimensions字段就是用来声明你的卡片支持哪几种规格。常见的组合是[1*2, 2*2, 2*4, 4*4]音乐类卡片一般选2*2和2*42*2用于纯控制2*4可以多放一条歌曲信息和封面。defaultDimension是用户添加卡片时默认展示的尺寸记得填一个已声明过的值否则系统可能拒绝创建。刷新配置是另一个必须理解的字段。updateEnabled表示是否允许定时刷新updateDuration的单位是 30 分钟最低只能填 1也就是 30 分钟刷一次。填 2 就是 1 小时。很多人会直觉地把updateDuration当成“秒数”来填结果填了个 5以为 5 秒刷新实际系统按 30 分钟的最小粒度给你算。scheduledUpdateTime是每天固定时间点刷新一次和updateDuration互斥同时配置时以scheduledUpdateTime为准。对于播放器卡片来说这两个字段更多是兜底方案真正的状态刷新要靠updateForm主动推后面第 4 章会讲。下面是一个典型的音乐卡片配置我一般会这么写{ name: music_card, description: 桌面音乐控制器, src: ./ets/card/MusicCard.ets, uiSyntax: arkts, window: { designWidth: 720, autoDesignWidth: true }, colorMode: auto, supportDimensions: [2*2, 2*4], defaultDimension: 2*2, updateEnabled: true, updateDuration: 1 }这段配置里三个参数最关键。src指向卡片 UI 的入口文件路径必须从ets/目录开始写写错的话系统拉卡片时会报找不到模块supportDimensions决定了用户在桌面添加卡片时能选哪些尺寸想减少适配工作量就只留一种uiSyntax填arkts说明这张卡片用的是 ArkTS 声明式开发而不是旧的 JS 卡片语法。window.designWidth是卡片 UI 的基准宽度音乐卡片一般用 720配合autoDesignWidth适配不同屏幕。改完配置记得重新安装 hap只热重载有时候不刷新配置。3.2 FormExtensionAbility卡片从创建到销毁的一生卡片 UI 是“脸”FormExtensionAbility才是“脑子”。它的名字暗示了定位它是系统与你的卡片逻辑之间的桥梁。当用户在桌面上点击“添加卡片”系统实例化你的FormExtensionAbility调用onAddForm当预定的刷新时间到了系统调用onUpdateForm当用户把卡片从桌面删掉系统调用onRemoveForm。onAddForm是这里最重要的一个方法因为它负责返回卡片的初始数据。它的返回值是一个formBindingData对象里面装的是卡片 UI 要显示的初始内容。下面是一段常见写法// CardFormService.ets import { FormExtensionAbility, formBindingData, formProvider } from kit.FormKit; import { Want } from kit.AbilityKit; export default class CardFormService extends FormExtensionAbility { onAddForm(want: Want) { const formData { songName: 未在播放, playState: paused, progressPercent: 0 }; return formBindingData.createFormBindingData(formData); } onUpdateForm(formId: string) { // 定时刷新或系统拉起时回调一般在这里补一次状态同步 } onRemoveForm(formId: string) { // 卡片被删除可以在这里清理资源 } }createFormBindingData里的字段会被持久化卡片 UI 通过LocalStorageProp对应读取。注意onAddForm的参数want里带了parameters里面有卡片 ID 等信息新版本 SDK 里更推荐从want.parameters里取内容再决定初始化数据但基础用法直接返回formData就够了。旧版本 SDK 的导入路径是ohos.app.form.FormExtensionAbility新版本统一走kit.FormKit。我建议直接看oh-package.json5里的 SDK 版本新工程用新写法老工程别硬迁。3.3 卡片 UI 是“受限”的这不是一个普通页面ArkTS 卡片的 UI 写法和普通页面很像但它是运行在一个受限环境里的。最直接的限制是不能使用自定义组件、不能使用Navigation、动画能力和系统字体支持也有限。你不能把主应用里那一套复杂组件树的 UI 代码复制到卡片里用。音乐卡片这类简单控制 UI 反而正好适合卡片一个列布局三五个按钮几条文本没了。还有一个容易被忽略的点卡片 UI 里不能直接调用大部分系统能力。你不能在卡片里直接创建AVPlayer也不能直接getContext()之后胡作非为。卡片的职责被刻意限定得很窄跨出 UI 展示和事件发送就违规了。所以卡片里拿到按钮点击结果后标准做法是调用postCardAction把事件塞给FormExtensionAbility让“脑子”去调用播放器播完再调updateForm把新状态画回去。### 3.4 更新卡片内容主动推和定时拉的取舍 服务卡片的更新机制有两条路。一条是系统定时刷新也就是 form_config.json 里的 updateEnabled 和 updateDuration。另一条是应用主动调用 formProvider.updateForm() 推数据。音乐卡片的特殊之处在于播放状态的变化是零散的、偶发的你根本不知道用户什么时候会点暂停。定时刷新在播放场景下没有意义——系统每 30 分钟拉一次拉到的可能还是半小时前的状态。所以真正的做法是在 FormExtensionAbility 里处理完事件后主动 updateForm 一次。 但主动推送也有频率管控。你在短时间内频繁调用 updateForm系统会丢弃部分请求或打印告警日志。这意味着进度条那种“每秒动一下”的 UI 在桌面上是不现实的。行业的常见做法是只展示歌曲名、歌手名和播放/暂停按钮最多加个静态的百分比数字。真要显示进度就按分钟级更新并且要把更新逻辑放在主应用侧而不是卡片侧。 ## 4. 把示例卡片改成自己的音乐控制台双向通信的完整闭环 ### 4.1 让按钮“活”起来postCardAction 的事件回路 示例工程如果只展示不能点那它就不是音乐卡片只是一张海报。让按钮可交互的关键是 postCardAction。这个函数是卡片 UI 侧最核心的 API它接受两个参数卡片的 this 上下文和一个描述动作的对象。动作有三种router 用于拉起应用内指定页面message 用于给 FormExtensionAbility 发消息call 用于拉起应用并直通某个方法。音乐卡片主场景是后台播放控制所以标准选择是 message。 下面这段代码是一个最小可用的控制按钮闭包三个按钮分别发 prev、playOrPause、next 三个指令 typescript // MusicCard.ets Entry Component struct MusicCard { LocalStorageProp(songName) songName: string 未在播放; LocalStorageProp(playState) playState: string paused; build() { Column({ space: 8 }) { Text(this.songName) .fontSize(16) .fontColor(#FFFFFF) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 20 }) { Button(上一首) .onClick(() { postCardAction(this, { action: message, params: { op: prev } }); }) Button(this.playState playing ? 暂停 : 播放) .onClick(() { postCardAction(this, { action: message, params: { op: playOrPause } }); }) Button(下一首) .onClick(() { postCardAction(this, { action: message, params: { op: next } }); }) } } .padding(16) .width(100%) .height(100%) .backgroundColor(#202020) } }postCardAction的params是一段自由结构的数据事件真正的接收方是FormExtensionAbility里的onFormEvent方法。这里有个新手上路最容易踩的小坎params里只能放可序列化的基本类型和对象不要把函数、播放器实例往里塞序列化直接失败。音乐卡片上只需要字符串和数字所以完全够用。4.2 onFormEvent 到播放器消息怎么变成真正的动作卡片把op扔出来了接下来是接住。onFormEvent(formId, message)是FormExtensionAbility里专门接 “message 类事件” 的方法收到的就是你在params里塞的那段数据不过它是个 JSON 字符串要自己序列化回来。拿到指令后你有两条路一条是在FormExtensionAbility里自己跑播放器另一条是转发给主应用的UIAbility。对音乐场景我强烈建议走第二条。因为FormExtensionAbility是轻量级的它的生命周期完全由系统的卡片服务管理在后台挂不了太久的常驻逻辑。完整做法是通过UIAbilityContext.startAbility拉一个后台任务到主应用或者用内部事件总线通知主应用。如果你只是想要一个演示级效果可以直接在onFormEvent里写状态切换然后立刻updateForm回去形成闭环// CardFormService.ets import { formBindingData, formProvider } from kit.FormKit; onFormEvent(formId: string, message: string) { let msg JSON.parse(message); let nextState playing; if (msg.op playOrPause) { // 这只是一个状态位真实项目在这里换成对播放器的调用 nextState playing; } const updateData formBindingData.createFormBindingData({ songName: 示例曲目 - 001, playState: nextState }); formProvider.updateForm(formId, updateData).catch((err) { console.error(updateForm fail: ${JSON.stringify(err)}); }); }这里有个细节updateForm的入参不是任意数据而必须是formBindingData.createFormBindingData()的返回值。很多人在这里直接传普通对象编译不报错但运行时更新不生效。另外formId是系统在onAddForm时提供的并透传到onFormEvent你想精确更新某一张卡片就必须用这个 ID 定位。同一应用可以有多张卡片实例每张都有自己的 ID广播式地全部更新要做循环不能指望一次调用搞定。4.3 播放器状态怎么同步到卡片监听、回调、回写真实项目里音乐卡片最麻烦的不是“发指令”而是“收状态”。用户可能不是在卡片上操作的而是在应用里点了播放或者在锁屏上切了歌。此时卡片上的状态如果还是旧的用户会觉得卡片坏的。解决思路很简单把状态变更变成一次广播。常见做法是主应用里维护一个全局的播放状态单例播放器任何状态变化state、title、duration都同步写进这个单例同时调用formProvider.updateForm把最新状态推到所有存活卡片上。关键代码只有两步。第一步播放器状态回调里拿到formId列表第二步遍历列表逐张更新。需要先拿到存活卡片的 ID 列表import { formProvider } from kit.FormKit; formProvider.getAllFormsInfo().then((forms) { forms.forEach((form) { formProvider.updateForm(form.formId, formBindingData.createFormBindingData({ songName: currentSong, playState: currentState })).catch(() {}); }); });注意频率。getAllFormsInfo别在歌曲每一秒的进度回调里调用它涉及跨进程查询高频调用会有性能开销。我的做法是歌曲切换时调一次播放/暂停状态变更时调一次进度更新最多 30 秒推一次。如果工程里的音乐服务用的是媒体会话AVSession监听回调里配备这一套逻辑桌面卡片几乎实时更新。4.4 刷新频率的真相别做“实时”音乐卡片很多人在拿到音乐卡片示例工程后第一反应是“我要在卡片上做一个每秒走一格的进度条”。鸿蒙服务卡片在真机上的高频刷新并不尽如人意主动推送更新也有限频系统对短时间内的多次updateForm会做丢弃或告警所以每秒更新的进度条会表现出明显的掉帧、卡顿甚至让整张卡片卡死。这不是性能优化能解决的问题是平台机制的限制。所以我对音乐卡片的定义从来不是“实时控制器”而是“准实时状态 事件入口”。卡片上显示当前的歌曲名、播放状态点击按钮能切歌能暂停这就够了。进度信息要么不做要么做成分钟级的粗略展示。用户对桌面小卡片的预期本来就只是“瞅一眼点一下”不要指望它替代应用内的播放页。这个边界想清楚了你的刷新策略就自然合理了。5. 从导入工程到真机卡片最容易翻车的 5 个点5.1 现象工程导入后kit.FormKit一直报红找不到模块原因本地 DevEco Studio 的 SDK 版本和工程oh-package.json5里声明的compileSdkVersion不匹配。kit.FormKit是较新版本 SDK 才有的导入路径旧版 SDK 只有ohos.app.form.*。解决打开build-profile.json5看compileSdkVersion是多少再对照本地 SDK 的版本。如果工程的 SDK 版本比本地高建议直接升级 DevEco Studio 到对应大版本如果只是想快速验证卡片效果可以把工程的compileSdkVersion调低并把kit.FormKit的导入改成旧路径ohos.app.form.FormExtensionAbility。改完同步依赖重新构建。5.2 现象真机上应用能跑起来但长按图标没有“服务卡片”入口原因最常见的是module.json5里的extensionAbilities节点配错type没有写form或者metadata里没有指向form_config.json的 profile 资源。系统找不到卡片定义自然不会在桌面上展示添加入口。解决检查extensionAbilities配置里type是否为form且metadata里的resource字段是否指向$profile:form_config。常见错误是把resource写成了$profile:form_config.json带.json后缀反而解析失败。另外确认这个配置写在entry模块的module.json5里而不是AppScope下的那个。5.3 现象卡片拉出来了但白屏或者一直在转圈原因卡片 UI 路径配置错误或卡片 UI 里用了系统不允许的组件和 API。白屏的本质是系统在渲染卡片时抛了异常但桌面不会直接给你弹错误详情。解决先用 DevEco Studio 的日志窗口过滤Form、FormExtensionAbility关键字看有没有明确的报错栈。路径错误的话检查form_config.json的src字段是否准确指向ets/card/xxx.ets路径以./ets/开头且文件真实存在。如果路径没问题再看卡片 UI 里有没有用禁用的组件比如自定义组件、Canvas、RichText等把它们换成系统基础组件。5.4 现象点卡片上的按钮应用里什么反应都没有原因多半是postCardAction的action选错了或者FormExtensionAbility里的方法名不对。卡片点击事件走的是onFormEvent有些开发者按页面事件习惯写成了onEvent系统根本没有这个方法就叫它消息就丢了。也可能是action用了router或call但并没有配置对应的目标页面。解决先在卡片 UI 侧确认postCardAction的action是message。然后检查FormExtensionAbility里是否重写了onFormEvent(formId: string, message: string)名字拼写不要错。最后在onFormEvent里打一条日志把message原文打印出来hilog里看到内容说明链路通了一半看不到就是事件没进来。5.5 现象播放状态在应用内变了但桌面卡片还是旧状态原因应用侧没有主动调updateForm。卡片的定时刷新粒度是 30 分钟你如果指望系统帮你把新状态带上去用户至少要等半小时才看到变化。另一个常见原因是调了updateForm但传参不是formBindingData.createFormBindingData()的返回值。解决确认应用侧在状态变化点调用了formProvider.updateForm。传参必须是createFormBindingData的返回对象不要手写一个普通对象进去。同时把更新的频率压在合理范围——歌曲切换和播放/暂停是必刷节点进度类数据降低到 30 秒一次。hilog里出现updateForm相关的丢弃告警就说明你推得太频繁了。6. 换掉示例内容做成你自己的音乐卡片最小改造清单这套工程在你手里能跑起来之后接下来的问题是怎么把它从“示例”变成“自己的”。我的最小改造清单只有三步。第一步替换卡片 UI。AccountCard这类示例里的文本、配色、按钮布局换成你自己应用的视觉规范注意灰度和透明度适配深色壁纸上要保证文字可读。第二步把onFormEvent里的模拟状态切换换成真实播放器调用。你在应用里用AVPlayer怎么播在onFormEvent里就怎么调唯一区别是入口事件变成了卡片发来的op字段。第三步把getAllFormsInfo updateForm的逻辑挂到你播放器的状态回调里确保不是只有卡片上点了按钮才更新。改造完成以后我习惯用三个动作验证卡片是否合格。第一个动作桌面添加一张2*4的卡片按播放、切歌、暂停各操作一次观察卡片上的文本和按钮状态是否在 2 秒内跟上。第二个动作回到应用内播放一首歌返回桌面卡片应该已经显示当前歌名——这是验证“应用侧主动推送”是否生效的黄金用例。第三个动作锁屏再解锁看卡片是不是还在状态是否还正确这一步能暴露后台进程是否被回收、刷新链路是否中断。每次都做三个动作不放过任何一次状态不同步。我自己的习惯是每改完一次刷新策略都先在真机上连续点 20 次播放/暂停再打开日志看updateForm有没有被限频。宁可卡片状态晚一秒显示也不要因为高频刷新把系统服务拖死。服务卡片这个东西看起来简单做起来磨心态但只要把“卡片只负责表达意图应用负责执行”这条边界刻在脑子里它就是个稳妥可靠的功能。希望这个工程和这套方法帮到你——下次再看到“某某服务卡片.zip”这类资源你已经知道打开后先看什么、改什么也知道该在哪里停下来说“够了不上实时进度条了”。本文还有配套的精品资源点击获取
返回列表