HarmonyOS 6实战:HarmonyOS轻量化交互的两种方案改造与实践(上)

张开发
2026/4/4 18:35:42 15 分钟阅读
HarmonyOS 6实战:HarmonyOS轻量化交互的两种方案改造与实践(上)
HarmonyOS 6实战HarmonyOS轻量化交互的两种姿势上篇一、服务卡片AI助手实现常驻系统页服务卡片改造实战踩坑记录二、实况窗更新位置与进程服务mock版生命周期管理踩坑记录总结我们之前做了个AI旅行助手迭代了AI侧的“帮我规划路线”、“推荐个餐厅”、“查一下天气”已经实现了基本的AI地图类应用。但实际用起来有个小交互一直让我不太舒服这个AI助手藏在App里得打开应用才能用。你想啊我早上出门前想查一下今天的路线得先找到App图标点开等启动页等加载再等AI反应过来。这一套流程下来十几秒过去了。能不能更快一点用户一解锁手机就能看到今天的路线推荐点一下就能用这是桌面级的轻量化交互。还有另一个场景用户在地图里选好了路线想看看详情得点个按钮弹出一个页面操作路径又长了。能不能让面板直接在地图上用手一拖就能拉起来看详情不用了再推回去这是应用内的轻量化交互。这两件事一个在桌面一个在应用内但核心思路相同把信息和服务推到用户眼前而不是让用户去找。于是有了这次改造。这篇文章分上下两篇上篇讲服务卡片和实况窗系统级轻量化交互下篇讲可拖拽滑动面板应用内手势交互。今天是上篇。一、服务卡片AI助手实现常驻系统页服务卡片就是长按桌面上的App图标弹出来的那个小窗口。可以是1×2、2×2、2×4等不同尺寸。它有几个特点不用打开App就能看到信息点一下就能跳转到应用内具体页面可以定时更新比如每天早上8点刷新今天的路线推荐我们的AI助手正好适合做成卡片——每天早上推几个常用功能用户看一眼有兴趣就点进去直接跳转到对应的功能页。我们使用ide的AI工具可以实时生成卡片基本 结构大概是下面这样的。├── ets/ │ ├── form/ │ │ ├── pages/ │ │ │ └── FormCard.ets # 卡片UI页面 │ │ ├── viewmodel/ │ │ │ └── FormViewData.ets # 卡片数据 │ │ └── constants/ │ │ └── FormConstants.ets # 常量 │ └── formability/ │ └── FormAbility.ets # 卡片能力扩展 └── resources/base/profile/ └── form_config.json # 卡片配置服务卡片改造实战卡片需要一个能力扩展类继承FormExtensionAbility。它管卡片的生命周期什么时候创建、什么时候更新、什么时候销毁。// products/phone/src/main/ets/formability/FormAbility.etsimport{formBindingData,FormExtensionAbility,formInfo}fromkit.FormKit;import{Want}fromkit.AbilityKit;exportdefaultclassFormAbilityextendsFormExtensionAbility{// 添加卡片时调用 - 返回卡片数据onAddForm(want:Want){letformData;returnformBindingData.createFormBindingData(formData);}// 临时卡片转正常卡片时调用onCastToNormalForm(formId:string){// 临时卡片成功转换为正常卡片时的通知}// 更新卡片时调用onUpdateForm(formId:string){// 通知卡片提供方更新指定卡片}// 卡片可见性变化时调用onChangeFormVisibility(newStatus:Recordstring,number){// 接收系统的卡片事件}// 卡片自定义事件触发时调用onFormEvent(formId:string,message:string){// 指定的消息事件被触发}// 移除卡片时调用onRemoveForm(formId:string){// 通知卡片提供方指定卡片已被销毁}// 获取卡片状态onAcquireFormState(want:Want){returnformInfo.FormState.READY;}}每个回调方法的用途回调方法触发时机主要用途onAddForm用户添加卡片初始化卡片数据onUpdateForm定时/主动更新刷新卡片内容onRemoveForm用户删除卡片清理资源onFormEvent卡片内事件触发处理用户点击onAcquireFormState查询卡片状态返回就绪状态卡片UI用的是ArkTS和普通页面写法差不多但有几个特殊的地方。关键组件是FormLink它不是普通的Button或Text而是卡片专用的跳转组件。点击它可以跳转到App内部指定的页面。这个设计很贴心——卡片里不能直接用router.pushUrl系统专门给了FormLink来干这件事。// products/phone/src/main/ets/form/pages/FormCard.etsimport{FormLink}fromkit.FormKit;import{FormViewData}from../viewmodel/FormViewData;import{FunctionType}fromohos/commons/Index;EntryComponentstruct FormCard{readonlyACTION_TYPE:stringrouter;readonlyABILITY_NAME:stringEntryAbility;readonlyMESSAGE:stringadd detail;build(){Column(){FormLink({action:this.ACTION_TYPE,abilityName:this.ABILITY_NAME,params:{message:this.MESSAGE}}){Column(){Row(){Image($r(app.media.ic_public_input_search)).width(15vp).margin({left:10vp,right:10vp})Text($r(app.string.textInput_holder)).fontColor(#99000000).width(80%).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})}.borderRadius(22vp).width(100%).height(44vp).backgroundColor(#0d000000).margin({top:20vp})Row(){ForEach(FormViewData.FUNCTIONS,(item:FunctionType){Column(){Image(item.icon).width(40vp).height(40vp)Text(item.desc).fontSize(12vp).padding({top:4vp})}},(item:FunctionType)item.id.toString())}.justifyContent(FlexAlign.SpaceBetween).height(60%).width(100%)}.width(90%)}}.width(100%)}}FormLink的配置说明属性说明示例action动作类型router表示路由跳转routerabilityName跳转的目标Ability名称EntryAbilityparams传递的参数对象{ message: xxx }卡片里显示的内容比如“今日推荐路线”可以从AI服务拉取。我们建了一个简单的数据模型来管理。// products/phone/src/main/ets/form/viewmodel/FormViewData.etsimport{FunctionType}fromohos/commons/Index;exportclassFormViewData{staticreadonlyFUNCTIONS:ArrayFunctionType[{id:1,icon:$r(app.media.icon_drive),desc:$r(app.string.drive_form)},{id:2,icon:$r(app.media.icon_Metro),desc:$r(app.string.metro_form)},{id:3,icon:$r(app.media.icon_taxi),desc:$r(app.string.taxi_form)},{id:4,icon:$r(app.media.icon_bus),desc:$r(app.string.bus_form)},{id:5,icon:$r(app.media.icon_hotel),desc:$r(app.string.hotel_form)}];}FunctionType接口定义// 通常在 commons/Index.ets 中定义exportinterfaceFunctionType{id:number;icon:Resource;desc:Resource;}卡片需要两个配置一个form_config.json描述卡片属性一个在module.json5里注册。form_config.json// products/phone/src/main/resources/base/profile/form_config.json{forms:[{name:form,displayName:$string:form_display_name,description:$string:form_desc,src:./ets/form/pages/FormCard.ets,uiSyntax:arkts,window:{designWidth:720,autoDesignWidth:true},colorMode:auto,isDynamic:false,isDefault:true,updateEnabled:false,scheduledUpdateTime:10:30,updateDuration:1,defaultDimension:2*4,supportDimensions:[2*4]}]}配置项说明配置项说明我们的设置name卡片标识formsrc卡片UI页面路径./ets/form/pages/FormCard.etsuiSyntaxUI语法arktsdefaultDimension默认尺寸2*4supportDimensions支持的尺寸列表[2*4]updateEnabled是否启用定时更新falsescheduledUpdateTime定时更新时间10:30updateDuration更新间隔小时1module.json5// products/phone/src/main/module.json5 { module: { extensionAbilities: [ { name: FormAbility, srcEntry: ./ets/formability/FormAbility.ets, label: $string:FormAbility_label, description: $string:FormAbility_desc, type: form, metadata: [ { name: ohos.extension.form, resource: $profile:form_config } ] } ] } }配完之后长按桌面上的App图标就能看到“添加卡片”的选项了。踩坑记录坑1路径问题form_config.json里的src路径要从ets开始写比如./ets/form/pages/FormCard.ets。我一开始写成了pages/FormCard.ets结果编译不过报错找不到文件。排查了半天才搞清楚是路径不对。坑2卡片里不能用router.pushUrl普通页面里跳转用router.pushUrl但在卡片里不行。卡片组件需要用FormLink包裹系统会自动处理跳转。这个花了我半天才搞明白看了好几遍官方文档才找到答案。坑3更新时机要合理updateDuration设得太短会频繁刷新影响性能和电量设得太长内容可能过时。我们根据业务场景设了1小时更新一次。二、实况窗更新位置与进程服务mock版服务卡片解决了“桌面级”的快捷访问问题。但还有一个场景用户通过AI叫了车想随时知道司机到哪了。总不能一直开着App盯着看吧这就是实况窗的用武之地。它可以在状态栏、锁屏、通知栏等位置展示实时信息。比如叫车后锁屏上就能看到“司机还有1公里”不用反复打开App刷。我们改造AI助手的时候顺便加了这个功能。用户通过AI叫了车AI生成打车订单后自动启动实况窗实时更新司机位置。但是因为我们还没有实际接入打车功能所以这里是mock数据。├── viewmodel/ │ └── LiveViewController.ets # 实况窗控制器 └── constants/ └── LiveConstants.ets # 实况窗常量比服务卡片简单很多核心就是一个控制器类。// features/live/src/main/ets/viewmodel/LiveViewController.etsimport{liveViewManager}fromkit.LiveViewKit;import{wantAgent}fromkit.AbilityKit;import{BusinessError}fromkit.BasicServicesKit;import{Logger}fromohos/commons/Index;import{LiveConstants}from../constants/LiveConstants;exportclassLiveViewController{privatestaticdefaultViewLiveViewController.buildDefaultView();/** * 启动实况窗 */publicasyncstartLiveView():PromiseliveViewManager.LiveViewResult{// 1. 检查实况窗是否可用if(!awaitLiveViewController.isLiveViewEnabled()){thrownewError(Live view is disabled.);}// 2. 启动实况窗try{returnawaitliveViewManager.startLiveView(awaitLiveViewController.defaultView);}catch(error){thrownewError(Live view is disabled.);}}/** * 构建默认实况窗数据 */privatestaticasyncbuildDefaultView():PromiseliveViewManager.LiveView{return{id:0,event:PICK_UP,liveViewData:{primary:{title:The driver has taken the order,content:[{text:distance from you},{text:1 km,textColor:#FF0A59F7}],keepTime:15,clickAction:awaitLiveViewController.buildWantAgent(),layoutData:{layoutType:4,underlineColor:#00ffffff,title:Deep Space Gray · Question M7,content:Pard 123456,descPic:taxi.png}},capsule:{type:1,status:1,icon:navigate.png,backgroundColor:#FF0A59F7,title:1 km}}};}/** * 检查实况窗是否启用 */privatestaticasyncisLiveViewEnabled():Promiseboolean{try{returnawaitliveViewManager.isLiveViewEnabled();}catch(error){returnfalse;}}/** * 构建点击跳转的WantAgent */privatestaticasyncbuildWantAgent():PromiseWant|undefined{constwantAgentInfo:wantAgent.WantAgentInfo{wants:[{bundleName:LiveConstants.BUNDLE_NAME,abilityName:LiveConstants.ABILITY_NAME,}asWant],actionType:wantAgent.OperationType.START_ABILITIES,requestCode:0,actionFlags:[wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]};try{constagentawaitwantAgent.getWantAgent(wantAgentInfo);returnagent;}catch(error){returnundefined;}}/** * 停止实况窗 */publicasyncstopLiveView(){if(!LiveViewController.isLiveViewEnabled()){thrownewError(Live view is disabled.);}liveViewManager.stopLiveView(awaitLiveViewController.defaultView);}}exportdefaultnewLiveViewController();下面是一些常量配置实际我们需要从后台接口拿但是没有服务我们根据官网教程实现了一个简化版本的。// features/live/src/main/ets/constants/LiveConstants.etsexportclassLiveConstants{staticreadonlyCAPSULE_COLOR:string#FF0A59F7;staticreadonlyCAPSULE_ICON:stringnavigate.png;staticreadonlyLIVE_VIEW_TITLE:string司机已接单;staticreadonlyLIVE_VIEW_CONTENT:string距离你 ;staticreadonlyLIVE_VIEW_DISTANCE:string1公里;staticreadonlyLIVE_VIEW_DISTANCE_COLOR:string#FF0A59F7;staticreadonlyTAXI_BRAND_INFO:string深空灰·问界M7;staticreadonlyTAXI_LICENCE_INFO:string牌123456;staticreadonlyTAXI_ICON:stringtaxi.png;staticreadonlyUNDERLINE_COLOR:string#00ffffff;staticreadonlyTIME:number15;staticreadonlyBUNDLE_NAME:stringcom.example.multitravelnavigation;staticreadonlyABILITY_NAME:stringEntryAbility;}生命周期管理实况窗不能一直开着得在合适的时机启动和停止。我们在EntryAbility里加了控制// products/phone/src/main/ets/entryability/EntryAbility.etsimport{LiveViewController}fromohos/live/Index;exportdefaultclassEntryAbilityextendsUIAbility{// 窗口销毁时 - 停止实况窗onWindowStageDestroy():void{LiveViewController.stopLiveView();}// 进入后台时 - 启动实况窗onBackground():void{LiveViewController.startLiveView();}}实况窗的数据结构稍微有点复杂但拆开看就清楚了LiveView是顶层对象包含id唯一标识、event事件类型、liveViewData具体数据。interfaceLiveView{id:number;// 实况窗ID启动时传0event:string;// 事件类型如 PICK_UPliveViewData:LiveViewData;}liveViewData分两部分primary是主要内容区展开状态capsule是状态栏胶囊收起状态。interfaceLiveViewData{primary:PrimaryData;capsule:CapsuleData;}primary里包含title标题、content文字内容数组支持不同颜色、keepTime保持秒数、clickAction点击跳转、layoutData布局细节。interfacePrimaryData{title:string;content:ContentItem[];keepTime:number;clickAction:Want;layoutData:LayoutData;}interfaceContentItem{text:string;textColor?:string;}interfaceLayoutData{layoutType:number;underlineColor:string;title:string;content:string;descPic:string;}capsule是状态栏胶囊的配置interfaceCapsuleData{type:number;status:number;icon:string;backgroundColor:string;title:string;}踩坑记录坑1忘记检查isLiveViewEnabled有些设备或系统版本不支持实况窗直接调用startLiveView会崩溃。一定要先调用isLiveViewEnabled检查。这个坑我在调试的时候遇到过真机运行正常但在某个旧版本模拟器上直接崩了。坑2keepTime设置要合理keepTime设置太短用户还没看清就消失了太长又占着位置影响其他通知。官方示例用的是15秒我们沿用这个值效果还不错。坑3点击跳转的WantAgent需要正确处理wantAgent.getWantAgent是异步操作要确保在实况窗启动前完成。我们的代码里用了await保证了顺序。坑4应用退出后实况窗还在实况窗是系统级的应用退出后它可能还留在状态栏。我们在onWindowStageDestroy里调用了stopLiveView确保退出时清理。坑5胶囊内容长度限制状态栏胶囊空间有限title别写太长。官方示例用的是1 km我们沿用这个风格保持在10个字以内。总结这一篇我们做了两件事服务卡片和实况窗。能力位置触发方式核心API适用场景服务卡片桌面用户主动添加FormExtensionAbilityFormLink快捷入口、信息展示实况窗状态栏/锁屏应用主动启动LiveViewManager实时状态追踪这两个能力不冲突可以同时用。卡片负责“快捷入口”让用户快速打开AI助手实况窗负责“实时状态”让用户不用进App就能追踪打车进度。代码量不算大但涉及的知识点不少。服务卡片需要理解FormExtensionAbility的生命周期和FormLink的跳转机制实况窗需要搞懂LiveView的数据结构和WantAgent的配置。这就是轻量化交互的价值——把信息和服务推到用户眼前而不是让用户去找。下篇预告我们将继续探讨“应用内”的轻量化交互——可拖拽滑动面板。用手一拖就能拉起详情面板不用了再推回去让信息“藏”在应用内需要时随时取用。

更多文章