查看: 99|回复: 3

鸿蒙7应用体验升级:HdsTabs悬浮页签与实况窗适配

[复制链接]
发表于 14 分钟前 | 显示全部楼层 |阅读模式
HarmonyOS 7(API 26)在2026年6月HDC启动开发者Beta、9月发布API 26正式版,官方口径称其为“首个完成AI化改造的操作系统”。对一线开发而言,真正影响交付的不是界面能否画得更花,而是系统能力有没有接上:视觉一致性、过程可见性、跨设备连续性、被智能体调度这四件事里,前两件是及格线,后两件才更容易形成差异化。

一、先摆正坐标系:能力接入优先于视觉堆叠

原文给出一个很实用的判断:体验升级的主要瓶颈不再是设计稿实现不了,而是系统能力没接上。悬浮页签、实况胶囊这类设计,系统已经提供原生材质和原生容器,自绘往往成本更高、一致性更差。同时,新能力收益和风险同时到来:沉浸光感要挑设备,实况窗有频控红线,3DGS吃包体和首帧,任何一个不做降级,都可能把体验升级做成性能事故。

落地顺序建议按分层推进:先保底,再可见,再连续,最后被调度。下层没做好就做上层,等于在漏水的船上刷漆。三条执行原则比分层本身更重要:先测再改,L0基线(冷启、首帧、滑动帧率、内存水位)没测出来之前不要动L1材质;一次只改一条链路,同时上悬浮页签、实况窗、碰一碰会无法归因;每个新能力都必须有降级路径,三件套是能力检测而非版本号判断、运行时开关、兜底样式。

二、HdsTabs:悬浮页签与沉浸光感的最小实现

传统底部页签是固定条加内容区,屏幕高度被硬切掉一块。HDS悬浮页签把它变成内容之上的一层玻璃:barOverlap(true)让内容延伸到页签下方,barFloatingStyle再给这层玻璃加材质。视觉上信息密度更高,滚动时内容从玻璃下穿过。实现时,材质参数必须是可变的@Local,因为降级逻辑会在运行时改写它们。
  1. import { HdsTabs, HdsTabsController, hdsMaterial } from '@kit.UIDesignKit';
  2. @Entry
  3. @ComponentV2
  4. struct Index {
  5.   @Local currentIndex: number = 0;
  6.   @Local materialType: hdsMaterial.MaterialType = hdsMaterial.MaterialType.ADAPTIVE;
  7.   @Local materialLevel: hdsMaterial.MaterialLevel = hdsMaterial.MaterialLevel.ADAPTIVE;
  8.   private controller: HdsTabsController = new HdsTabsController();
  9.   aboutToAppear(): void {
  10.     this.degradeMaterial();
  11.   }
  12.   private degradeMaterial(): void {
  13.     const supported: Array<hdsMaterial.MaterialType> = hdsMaterial.getSystemMaterialTypes();
  14.     if (!supported.includes(hdsMaterial.MaterialType.IMMERSIVE)) {
  15.       this.materialType = hdsMaterial.MaterialType.BACKGROUND_BLUR;
  16.       this.materialLevel = hdsMaterial.MaterialLevel.SMOOTH;
  17.       return;
  18.     }
  19.     this.materialType = hdsMaterial.MaterialType.IMMERSIVE;
  20.     this.materialLevel = hdsMaterial.MaterialLevel.EXQUISITE;
  21.   }
  22.   @Builder
  23.   miniBar() {
  24.     Row() {
  25.       Image($r('sys.media.ohos_ic_public_more')).width(24).height(24)
  26.       Text('快捷操作').fontSize(14).margin({ left: 8 })
  27.     }
  28.     .padding({ left: 12, right: 12, top: 8, bottom: 8 })
  29.     // 注意:这里绝对不要写 width('100%'),否则收起动画会失效
  30.   }
  31.   build() {
  32.     HdsTabs({ controller: this.controller }) {
  33.       TabContent() { Text('首页内容') }.tabBar(this.tabItem(0, $r('sys.symbol.house'), '首页'))
  34.       TabContent() { Text('发现内容') }.tabBar(this.tabItem(1, $r('sys.symbol.star'), '发现'))
  35.       TabContent() { Text('消息内容') }.tabBar(this.tabItem(2, $r('sys.symbol.message'), '消息'))
  36.       TabContent() { Text('我的内容') }.tabBar(this.tabItem(3, $r('sys.symbol.person'), '我的'))
  37.     }
  38.     .barOverlap(true)
  39.     .barPosition(BarPosition.End)
  40.     .vertical(false)
  41.     .onChange((index: number) => { this.currentIndex = index; })
  42.     .barFloatingStyle({
  43.       barBottomMargin: 16,
  44.       gradientMask: { maskColor: '#66F1F3F5', maskHeight: 92 },
  45.       systemMaterialEffect: {
  46.         materialType: this.materialType,
  47.         materialLevel: this.materialLevel
  48.       },
  49.       miniBar: { miniBarBuilder: () => this.miniBar() },
  50.       enableMiniBarBackground: true,
  51.       enableMiniBarClip: true,
  52.       miniBarWidth: [328]
  53.     })
  54.   }
  55. }
复制代码

材质分级上,功耗从高到低是EXQUISITE、GENTLE、SMOOTH。默认策略可设为旗舰IMMERSIVE加GENTLE,中端BACKGROUND_BLUR加SMOOTH,低端直接NONE。ADAPTIVE交给系统调度适合“不想管”,但线上体验会随设备漂移,不好做验收。

几个真踩过的坑要提前规避:不要在build()里调用getSystemMaterialTypes(),同步查询会参与每帧重建,应挪到aboutToAppear()并用@Local承接;迷你栏根节点不要写width('100%'),否则父容器无法收敛宽度,收起动画失效,且预览器可能正常;开了systemMaterialEffect就不要再给页签栏叠不透明背景,否则深色模式会灰上叠灰;光感区域上的文字对比度要留gradientMask,示例中的maskHeight为92就是为了避免深色图片加深色文字糊成一片。验收时看冷启首帧不超过基线10%、连续滑动60秒掉帧率低于5%、深浅色和护眼模式下页签栏文字对比度不低于4.5:1、低端机走NONE仍成立、折叠屏展开折叠不跳变不重叠。

三、碰一碰:把分享压缩成一个动作

碰一碰的交互本质是:对端设备轻碰,你的应用收到回调,你决定给什么数据,系统负责传输。代码只有三步,但注册时机和窗口ID必须准确。
  1. import { harmonyShare, systemShare } from '@kit.ShareKit';
  2. import { uniformTypeDescriptor as utd } from '@kit.ArkData';
  3. @Entry
  4. @Component
  5. struct SharePage {
  6.   private registered: boolean = false;
  7.   aboutToAppear(): void {
  8.     this.registerKnock();
  9.   }
  10.   aboutToDisappear(): void {
  11.     if (this.registered) {
  12.       harmonyShare.off('knockShare');
  13.       this.registered = false;
  14.     }
  15.   }
  16.   private registerKnock(): void {
  17.     const windowId = 999; // 实际取当前窗口的 windowId,不能写死
  18.     const registry: harmonyShare.SendCapabilityRegistry = {
  19.       windowId: windowId,
  20.       sendOnly: true
  21.     };
  22.     harmonyShare.on('knockShare', registry, (target: harmonyShare.SharableTarget) => {
  23.       this.onKnock(target);
  24.     });
  25.     this.registered = true;
  26.   }
  27.   private onKnock(target: harmonyShare.SharableTarget): void {
  28.     if (!this.hasSharableContent()) {
  29.       target.clarifyNonShare({ message: '当前页面暂无可分享内容' });
  30.       return;
  31.     }
  32.     const data: systemShare.SharedData = new systemShare.SharedData({
  33.       utd: utd.UniformDataType.PLAIN_TEXT,
  34.       content: this.buildShareText()
  35.     });
  36.     target.share(data);
  37.   }
  38.   private hasSharableContent(): boolean { return true; }
  39.   private buildShareText(): string { return '来自「玩转鸿蒙」的分享'; }
  40.   build() { /* 页面内容 */ }
  41. }
复制代码

图片或文件把utd换成IMAGE或FILE并带上uri;链接类内容额外补title、description、thumbnailUri,系统会据此选中不同卡片模板。预览图最小支持1:4,超出部分会被裁切,关键信息不要放最边上。windowId必须取真实窗口ID,写死会导致轻碰无响应且没有日志;注册时机是进入可分享界面,反注册时机是离开,不是应用启动时注册一次;sendOnly为true只适合纯发送端,双端都设true会让双向分享失效;内容未加载完时轻碰,应给clarifyNonShare文案而不是静默reject。API 26起,PC/2in1和Tablet作为发送端可以getInfo()拿到coordinate.screenX/screenY,做轻碰位置动效时用得上。观测指标看碰一碰触发率、分享完成率、取消率:触发率低是引导问题,取消率高是内容或传输问题。

四、实况窗:sequence挡乱序,事件ID挡重推

先纠正一个普遍误解:官方模型里没有pending、active、expanded、ended这种状态机。实况窗只有两种形态:卡片态和胶囊态;一个event字段对应15个白名单场景,加一个sequence版本号。sequence是唯一保序机制,新版本号必须大于当前展示版本号,更新和结束才生效;不传则不校验。状态机要业务侧自己建,幂等、终态可达、版本仲裁都落在你的代码里。
  1. class SequenceGate {
  2.   private lastSequence: number = 0;
  3.   private seenEventIds: Set<string> = new Set();
  4.   public next(eventId: string): number | null {
  5.     if (this.seenEventIds.has(eventId)) {
  6.       return null;
  7.     }
  8.     this.seenEventIds.add(eventId);
  9.     return ++this.lastSequence;
  10.   }
  11.   public restore(serverSequence: number): void {
  12.     this.lastSequence = serverSequence === 4294967295 ? 0 : serverSequence;
  13.   }
  14.   public finish(): void {
  15.     this.seenEventIds.clear();
  16.   }
  17. }
复制代码

链路选择是关键。主路是云侧Push直更:本地创建成功后,把实况窗id、pushToken、event、业务状态存到服务端;状态变化时服务端通过Push Kit场景化消息更新或结束,不依赖应用进程存活。旁路是前台校准:应用在前台时用长连接最新数据本地更新,并带sequence参与仲裁,这条路不承担主链路职责,进程不在时静默消失。把两条路的数据源合并,才是解决“胶囊显示的位置比地图页晚30秒”这类投诉的正确姿势,问题往往不是推送慢,而是胶囊和地图页在看两份数据。

红线有两组。频控是双轨制、各自独立计数,Push侧超频最坑的是“看起来成功了”,所以业务侧必须自己节流,位置类更新10到15秒一次,中间用端侧插值补帧。生命周期有硬时间线,业务兜底必须远早于4小时,否则系统替你收口时,用户已经看了两小时过期数据。还有细节:云侧更新时若activityId对应的实况窗不存在,该ID会被限制发消息24小时,服务端要等创建确认后再开始推;event与已开通权益不匹配会报1003500005,权益需逐个场景申请;普通实况窗不需要在module.json5声明扩展,也不需要PUBLISH_AGENT_REMINDER,那是后台代理提醒的权限,两回事。

五、平行视界:不写业务代码拿到大屏双栏

选型标准很朴素:页面结构天然是“首页到二级页”,且还没做任何大屏适配,先上平行视界;需要三分栏、复杂断点、强定制转场,再上API分栏。两者不冲突,可以先兜底后演进。配置上,entry/src/main/module.json5加一行引用easyGo,然后在entry/src/main/resources/base/profile/easy_go.json里配置displayModeOptions。wideWindowMode和squareWindowMode设为navigationSplit,navigationSplitOptions里配homePage、relatedPage、mode、wideSplit、squareSplit、enableReducedContainerSize、supportLandscapeFullscreen、fullScreenPages。mode为1是导航模式,适合IM、邮箱、办公;0是购物模式,右推左,适合电商和内容层层深入,API 26起支持。wideSplit和squareSplit的ratio范围是1:2到2:1,超出按边界值处理。enableReducedContainerSize为true时分栏后lpx、横向断点、窗口宽度按右侧页面尺寸计算,对“分栏后布局被拉爆”的应用几乎是必开项。fullScreenPages用于大图预览、视频播放这类页面临时退出分栏,返回后恢复。运行时判断是否处于分栏态用UIContext.isEasySplit(),API 24起。

四个容易翻车的点:只在entry模块生效,写在HAP或HSP里无效,且是应用级生效;routerSplitOptions与navigationSplitOptions互斥,同时存在会解析失败;开了平行视界就别混用Router和Navigation,否则左右页错配、返回栈异常;homePage要主动配,不配时系统靠识别猜,复杂Navigation结构经常猜错,主页是Navigation首页就写navBar,是NavDestination就写它的name。

六、3DGS:加载模型之前先做资产治理

3DGS用百万级带颜色、不透明度、协方差的高斯椭球表达空间,相比NeRF能实时渲染。HarmonyOS 7把它做成端侧能力:Spatial Recon Kit负责重建与编辑,ArkGraphics3D负责渲染。加载模型时必须先注册GS渲染插件,否则loadGSNode找不到渲染器。
  1. import { spatialRender } from '@kit.SpatialReconKit';
  2. import { Scene, RenderContext } from '@kit.ArkGraphics3D';
  3. async function loadGsModel(): Promise<void> {
  4.   const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
  5.   if (renderContext === null) {
  6.     return;
  7.   }
  8.   renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
  9.   const scene = await Scene.load();
  10.   const uri = 'OhosRawFile://assets/gltf/model.glb';
  11.   const gsNode: spatialRender.GSNode =
  12.     await spatialRender.GSPlugin.loadGSNode(scene, { uri, offset: 0 }, scene.root);
  13. }
复制代码

格式取舍:MP4体积最小、最适合分发;PLY精度高、适合离线制作;GLB兼容性最好、适合和普通3D资产混排。资产治理比渲染更重要:首帧优先,先用封面图或低模占位,loadGSNode异步完成后交叉淡入;模型按需下载,不要把3DGS资产塞进rawfile拖着包体走,走云端下发加本地缓存;必须有降级,设备不支持时退回360度序列帧或普通视频;控制高斯点数量,移动端按目标机型实测帧率再定,别把PC制作时的原始点数直接上线。适用场景是家居摆放预览、商品360度查看、看房、展陈、教育演示,不适合做纯装饰背景。

七、从被打开到被调度:意图装饰器与Skill入口

HarmonyOS 7给了两条路。装饰器意图是一个最小的可分发单元,llmDescription要当产品文案写,“播放用户点播的歌曲”比“音乐播放能力”命中率高一个量级;parameters要认真写JSON Schema,系统靠它生成参数,字段描述缺失会导致调用时参数为空;意图执行文件如果没被其它文件import,要通过insight_intent.json的insightIntentsSrcEntry声明,否则不参与编译;@InsightIntentPage有退化链路,navigationId或navDestinationName匹配失败会退化成pagePath跳转,Navigation深链场景要专门验证。
  1. import { InsightIntentEntryExecutor, insightIntent } from '@kit.AbilityKit';
  2. @InsightIntentEntry({
  3.   intentName: 'PlayMusic',
  4.   domain: 'MusicDomain',
  5.   intentVersion: '1.0.1',
  6.   displayName: '播放音乐',
  7.   llmDescription: '播放用户点播的歌曲',
  8.   keywords: ['播放', '音乐', 'PlayMusic'],
  9.   abilityName: 'EntryAbility',
  10.   executeMode: [insightIntent.ExecuteMode.UI_ABILITY_FOREGROUND],
  11.   parameters: {
  12.     type: 'object',
  13.     properties: {
  14.       songName: { type: 'string', description: '歌曲名称', minLength: 1 }
  15.     },
  16.     required: ['songName']
  17.   }
  18. })
  19. export default class PlayMusicImpl extends InsightIntentEntryExecutor<string> {
  20.   public songName: string = '';
  21.   onExecute(): Promise<insightIntent.IntentResult<string>> {
  22.     return Promise.resolve({ code: 0, result: 'execute success' });
  23.   }
  24. }
复制代码

应用Skill方面,工程结构固定:entry下建skills目录,每个技能一个子目录,目录名等于SKILL.md的name,scripts下放入口脚本,entry/src/main/module.json5中通过skillProfiles注册。从给出的结构能确认的约束包括目录名与SKILL.md的name一致、入口脚本放在scripts下、module.json5中通过skillProfiles注册;实际接入时还要按官方文档补齐字段和校验,避免出现“目录建了但技能不参与编译”的问题。

整体落地时,建议按周切分链路,每期一条能力并保留对照组。先测L0基线,再改悬浮页签材质;先做碰一碰和实况窗的降级与频控,再谈大屏分栏、3DGS和意图调度。每个新能力都做能力检测、运行时开关和兜底样式,才能把HarmonyOS 7的体验升级真正落到可验收的工程结果上。
回复

使用道具 举报

发表于 4 分钟前 | 显示全部楼层

Re: 鸿蒙7应用体验升级:HdsTabs悬浮页签与实况窗适配

这篇把优先级排序说得很透,“先保底再可见再连续最后被调度”这个顺序我认同,尤其“下层没做好就做上层等于漏水的船上刷漆”这个比喻很贴切。实际项目里最容易犯的就是设计稿一来就堆材质和动效,结果冷启动和首帧全崩了,回头再拆反而更贵。 降级三件套里“能力检测而非版本号判断”这点特别关键,代码里用 getSystemMaterialTypes 去查 IMMERSIVE 是否在支持列表里,比判断 API 版本稳妥得多,这个写法我打算直接参考。材质按 EXQUISITE、GENTLE、SMOOTH 分级对应功耗,旗舰上 IMMERSIVE、中低端退回 BACKGROUND_BLUR 加 SMOOTH,这个取舍也合理,毕竟沉浸光感不是所有设备都扛得住。 miniBar 那里提醒不要写 width('100%') 否则收起动画失效,这种细节一看就是踩过坑总结出来的,很有价值。另外“一次只改一条链路”也很实在,同时上悬浮页签、实况窗、碰一碰,真出问题根本没法归因。 想再请教一下:实况窗的频控红线一般怎么在提测前做验证,是看系统日志还是自己埋点统计触发次数?还有就是运行时开关这块,你们是做成统一的降级配置中心,还是每个能力各自维护一个开关?
回复 支持 反对

使用道具 举报

发表于 4 分钟前 | 显示全部楼层

Re: 鸿蒙7应用体验升级:HdsTabs悬浮页签与实况窗适配

楼主的思路挺认同的,尤其是“下层没做好就做上层等于在漏水的船上刷漆”这个比喻很到位。很多团队一上来就堆悬浮页签、沉浸光感,结果冷启和滑动帧率没测过,体验升级直接变成性能事故,这种翻车现场见得太多了。 HdsTabs 那段代码里两个细节很有价值:一是材质参数必须用 @Local 声明,因为降级逻辑运行时改写,如果用普通成员变量,材质切换不会触发重新渲染;二是 miniBar 里绝对不能写 width('100%'),否则收起动画失效,这种坑不写出来基本要踩一遍才知道。degradeMaterial 用 getSystemMaterialTypes() 做能力检测而不是判断版本号,这个原则值得推广到所有新能力上,版本号能骗人,运行时能力列表骗不了。 分层那套“先保底、再可见、再连续、最后被调度”的落地顺序也很实用,配合“一次只改一条链路”,至少出问题能归因。想请教一下 L0 基线这块,楼主一般是用什么口径来卡的,比如冷启和首帧的阈值大概在什么范围算合格?另外实况窗的频控红线具体是怎么触发的,是按应用维度还是按设备维度限流?这块如果能在帖子里再展开一点就更完整了。
回复 支持 反对

使用道具 举报

发表于 4 分钟前 | 显示全部楼层

Re: 鸿蒙7应用体验升级:HdsTabs悬浮页签与实况窗适配

楼主这套分层思路挺实在的,“先保底再可见再连续最后被调度”这句基本可以直接拿去当迭代排期表用。我比较认同的是把材质参数做成可变的 @Local 这一点,很多团队踩坑都是因为在构造里写死 IMMERSIVE,结果低端机上首帧直接炸,回头再改还得动组件结构。 几个补充想法: 一是 miniBar 那条注释很关键,条形容器给满宽之后收起动画的测量基准就变了,这类“样式上看着没问题、动效却失效”的坑,建议在代码评审清单里单独列一栏,专门盯动效相关约束,比事后靠肉眼验收靠谱。 二是 barOverlap 之后内容区被页签压住的部分,最好同时把手势和可点击区域一起测,尤其是列表最后一项和底部悬浮按钮,视觉上穿过玻璃很好看,但误触和遮挡的投诉往往滞后一两个版本才冒出来,那时候改动成本就高了。 三是关于降级三件套里的运行时开关,建议不要把判断散落在各个页面,收在一个统一的能力探测和策略模块里,档位只在一处算,页面只读结果。不然悬浮页签、实况窗、光感三套降级逻辑各写各的,后面做 AB 或者灰度会非常难归因,也正好呼应你说的“一次只改一条链路”。 四是 L0 基线这块,冷启和首帧之外可以再加个滚动时的掉帧分布,不只是平均帧率,因为玻璃材质的问题往往是滚动到某个特定区域才抖,平均值会把抖动抹平。 最后想请教下,材质分级除了设备档位之外,你们有没有按场景做区分,比如暗色模式或者低电量下是否直接降一档?另外
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-10 00:14 , Processed in 0.021390 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部