HarmonyOS 7(API 26)在2026年6月HDC启动开发者Beta、9月发布API 26正式版,官方口径称其为“首个完成AI化改造的操作系统”。对一线开发而言,真正影响交付的不是界面能否画得更花,而是系统能力有没有接上:视觉一致性、过程可见性、跨设备连续性、被智能体调度这四件事里,前两件是及格线,后两件才更容易形成差异化。
一、先摆正坐标系:能力接入优先于视觉堆叠
原文给出一个很实用的判断:体验升级的主要瓶颈不再是设计稿实现不了,而是系统能力没接上。悬浮页签、实况胶囊这类设计,系统已经提供原生材质和原生容器,自绘往往成本更高、一致性更差。同时,新能力收益和风险同时到来:沉浸光感要挑设备,实况窗有频控红线,3DGS吃包体和首帧,任何一个不做降级,都可能把体验升级做成性能事故。
落地顺序建议按分层推进:先保底,再可见,再连续,最后被调度。下层没做好就做上层,等于在漏水的船上刷漆。三条执行原则比分层本身更重要:先测再改,L0基线(冷启、首帧、滑动帧率、内存水位)没测出来之前不要动L1材质;一次只改一条链路,同时上悬浮页签、实况窗、碰一碰会无法归因;每个新能力都必须有降级路径,三件套是能力检测而非版本号判断、运行时开关、兜底样式。
二、HdsTabs:悬浮页签与沉浸光感的最小实现
传统底部页签是固定条加内容区,屏幕高度被硬切掉一块。HDS悬浮页签把它变成内容之上的一层玻璃:barOverlap(true)让内容延伸到页签下方,barFloatingStyle再给这层玻璃加材质。视觉上信息密度更高,滚动时内容从玻璃下穿过。实现时,材质参数必须是可变的@Local,因为降级逻辑会在运行时改写它们。
- import { HdsTabs, HdsTabsController, hdsMaterial } from '@kit.UIDesignKit';
- @Entry
- @ComponentV2
- struct Index {
- @Local currentIndex: number = 0;
- @Local materialType: hdsMaterial.MaterialType = hdsMaterial.MaterialType.ADAPTIVE;
- @Local materialLevel: hdsMaterial.MaterialLevel = hdsMaterial.MaterialLevel.ADAPTIVE;
- private controller: HdsTabsController = new HdsTabsController();
- aboutToAppear(): void {
- this.degradeMaterial();
- }
- private degradeMaterial(): void {
- const supported: Array<hdsMaterial.MaterialType> = hdsMaterial.getSystemMaterialTypes();
- if (!supported.includes(hdsMaterial.MaterialType.IMMERSIVE)) {
- this.materialType = hdsMaterial.MaterialType.BACKGROUND_BLUR;
- this.materialLevel = hdsMaterial.MaterialLevel.SMOOTH;
- return;
- }
- this.materialType = hdsMaterial.MaterialType.IMMERSIVE;
- this.materialLevel = hdsMaterial.MaterialLevel.EXQUISITE;
- }
- @Builder
- miniBar() {
- Row() {
- Image($r('sys.media.ohos_ic_public_more')).width(24).height(24)
- Text('快捷操作').fontSize(14).margin({ left: 8 })
- }
- .padding({ left: 12, right: 12, top: 8, bottom: 8 })
- // 注意:这里绝对不要写 width('100%'),否则收起动画会失效
- }
- build() {
- HdsTabs({ controller: this.controller }) {
- TabContent() { Text('首页内容') }.tabBar(this.tabItem(0, $r('sys.symbol.house'), '首页'))
- TabContent() { Text('发现内容') }.tabBar(this.tabItem(1, $r('sys.symbol.star'), '发现'))
- TabContent() { Text('消息内容') }.tabBar(this.tabItem(2, $r('sys.symbol.message'), '消息'))
- TabContent() { Text('我的内容') }.tabBar(this.tabItem(3, $r('sys.symbol.person'), '我的'))
- }
- .barOverlap(true)
- .barPosition(BarPosition.End)
- .vertical(false)
- .onChange((index: number) => { this.currentIndex = index; })
- .barFloatingStyle({
- barBottomMargin: 16,
- gradientMask: { maskColor: '#66F1F3F5', maskHeight: 92 },
- systemMaterialEffect: {
- materialType: this.materialType,
- materialLevel: this.materialLevel
- },
- miniBar: { miniBarBuilder: () => this.miniBar() },
- enableMiniBarBackground: true,
- enableMiniBarClip: true,
- miniBarWidth: [328]
- })
- }
- }
复制代码
材质分级上,功耗从高到低是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必须准确。
- import { harmonyShare, systemShare } from '@kit.ShareKit';
- import { uniformTypeDescriptor as utd } from '@kit.ArkData';
- @Entry
- @Component
- struct SharePage {
- private registered: boolean = false;
- aboutToAppear(): void {
- this.registerKnock();
- }
- aboutToDisappear(): void {
- if (this.registered) {
- harmonyShare.off('knockShare');
- this.registered = false;
- }
- }
- private registerKnock(): void {
- const windowId = 999; // 实际取当前窗口的 windowId,不能写死
- const registry: harmonyShare.SendCapabilityRegistry = {
- windowId: windowId,
- sendOnly: true
- };
- harmonyShare.on('knockShare', registry, (target: harmonyShare.SharableTarget) => {
- this.onKnock(target);
- });
- this.registered = true;
- }
- private onKnock(target: harmonyShare.SharableTarget): void {
- if (!this.hasSharableContent()) {
- target.clarifyNonShare({ message: '当前页面暂无可分享内容' });
- return;
- }
- const data: systemShare.SharedData = new systemShare.SharedData({
- utd: utd.UniformDataType.PLAIN_TEXT,
- content: this.buildShareText()
- });
- target.share(data);
- }
- private hasSharableContent(): boolean { return true; }
- private buildShareText(): string { return '来自「玩转鸿蒙」的分享'; }
- build() { /* 页面内容 */ }
- }
复制代码
图片或文件把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是唯一保序机制,新版本号必须大于当前展示版本号,更新和结束才生效;不传则不校验。状态机要业务侧自己建,幂等、终态可达、版本仲裁都落在你的代码里。
- class SequenceGate {
- private lastSequence: number = 0;
- private seenEventIds: Set<string> = new Set();
- public next(eventId: string): number | null {
- if (this.seenEventIds.has(eventId)) {
- return null;
- }
- this.seenEventIds.add(eventId);
- return ++this.lastSequence;
- }
- public restore(serverSequence: number): void {
- this.lastSequence = serverSequence === 4294967295 ? 0 : serverSequence;
- }
- public finish(): void {
- this.seenEventIds.clear();
- }
- }
复制代码
链路选择是关键。主路是云侧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找不到渲染器。
- import { spatialRender } from '@kit.SpatialReconKit';
- import { Scene, RenderContext } from '@kit.ArkGraphics3D';
- async function loadGsModel(): Promise<void> {
- const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
- if (renderContext === null) {
- return;
- }
- renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);
- const scene = await Scene.load();
- const uri = 'OhosRawFile://assets/gltf/model.glb';
- const gsNode: spatialRender.GSNode =
- await spatialRender.GSPlugin.loadGSNode(scene, { uri, offset: 0 }, scene.root);
- }
复制代码
格式取舍: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深链场景要专门验证。
- import { InsightIntentEntryExecutor, insightIntent } from '@kit.AbilityKit';
- @InsightIntentEntry({
- intentName: 'PlayMusic',
- domain: 'MusicDomain',
- intentVersion: '1.0.1',
- displayName: '播放音乐',
- llmDescription: '播放用户点播的歌曲',
- keywords: ['播放', '音乐', 'PlayMusic'],
- abilityName: 'EntryAbility',
- executeMode: [insightIntent.ExecuteMode.UI_ABILITY_FOREGROUND],
- parameters: {
- type: 'object',
- properties: {
- songName: { type: 'string', description: '歌曲名称', minLength: 1 }
- },
- required: ['songName']
- }
- })
- export default class PlayMusicImpl extends InsightIntentEntryExecutor<string> {
- public songName: string = '';
- onExecute(): Promise<insightIntent.IntentResult<string>> {
- return Promise.resolve({ code: 0, result: 'execute success' });
- }
- }
复制代码
应用Skill方面,工程结构固定:entry下建skills目录,每个技能一个子目录,目录名等于SKILL.md的name,scripts下放入口脚本,entry/src/main/module.json5中通过skillProfiles注册。从给出的结构能确认的约束包括目录名与SKILL.md的name一致、入口脚本放在scripts下、module.json5中通过skillProfiles注册;实际接入时还要按官方文档补齐字段和校验,避免出现“目录建了但技能不参与编译”的问题。
整体落地时,建议按周切分链路,每期一条能力并保留对照组。先测L0基线,再改悬浮页签材质;先做碰一碰和实况窗的降级与频控,再谈大屏分栏、3DGS和意图调度。每个新能力都做能力检测、运行时开关和兜底样式,才能把HarmonyOS 7的体验升级真正落到可验收的工程结果上。 |