# 〖共创稿事节〗HarmonyOS 7 新特性实战(65):观感三刀——圆框裁图、胶囊权重与术语语境;52 项门禁复跑的 2 伤 3 锈

上一篇写异兽详情分层去重怎么立合同、怎么维护钉点,这一篇回到更小的改动:三刀观感小改。三刀的共同点是都来自用户原话——「没必要用一个圆圈圈起来吧」「在标题旁像脚注」「收录改成发现吧」,也共同点是每一刀都走完整套纪律:构建、全量门禁、真机三态截图,一个不少。

真正的收获却在写这篇的当天:为取证复跑全量门禁,52 项里红了两处伤、三处锈。伤是 54 篇接 App Linking 时重构 module.json5,误删了闪控球/闪控窗的权限声明——运行时代码还在申请 FLOAT_VIEW,声明却没了;锈是三处门禁断言没跟上后来对浮动栏的双分支重构。另有 5 处术语漏网,藏在门禁没盯的组件角落。三刀本身很小,这 8 处却是这篇最想讲的:小改不等于省事,越小的改动越容易被「应该没问题」放过。(构建与门禁在处置后复跑,HAP 构建 50s 通过,52 项门禁全绿——详见第五节与证据清单。)

观感三刀全景:圆框裁图、胶囊权重、术语语境与全套纪律

一、三刀清单:用户原话与落点

刀用户原话(2026-10-16)落点关键改法
圆框裁图「顶部卡片的图片没必要用圆圈圈起来,还会导致图片无法显示全」ShanhaiBeastDetailHeroCard.ets拆掉 108×108 圆形容器,图片全显
胶囊权重「线索卡按钮像脚注」「在地图上选」是主入口权重反而最弱ShanhaiExploreWorldPanels.ets分级胶囊:主操作实心、次操作描边
术语语境「用户进度的『收录』改『发现』」全应用 8 个文件起步双语境判定:进度改发现,内容收录保留

三刀各有一个「反直觉」的教训,逐刀展开。

二、第一刀:圆框不是装饰,是裁切

详情页顶部卡片的异兽插画,此前包在一个 108×108 的圆形容器里:

// 改前:Stack 圆形容器 + clip,插画四角被裁掉
Stack() {
  Image(shanhaiBeastImage(this.beast.id))
    .width(104)
    .height(104)
    .objectFit(ImageFit.Contain)
}
.width(108)
.height(108)
.border({ width: 1, color: ShanhaiTheme.line(this.isDarkMode) })
.borderRadius(54)
.clip(true)

问题出在 clip(true) 与 borderRadius(54) 的组合:容器把内容裁成正圆,而插画是矩形,四个角必然被切掉。用户看得准——化蛇的插图宽高比 0.95(1224×1285),接近方形,圆框一裁就缺四角;巴蛇是 0.67 的竖版长图(1024×1536),蛇身贯穿画幅,裁切更明显。改法是拆掉容器、让插画以 ImageFit.Contain 全显:

// 改后:裸 Image,Contain 全显,不再被裁角
Image(shanhaiBeastImage(this.beast.id))
  .width(104)
  .height(104)
  .objectFit(ImageFit.Contain)

尺寸从 108 退到 104 不是随手写:104×104 与原图渲染尺寸一致,不再有「容器 108、内容 104」两层语义;1px 描边与圆角随容器一起删除。这个组件在窄/宽两套布局里各用一次(同源组件),改一处即全部生效。真机取证了两只极端比例:化蛇(近方形)与巴蛇(竖长图),都完整显示、无圆框。

这一刀的可复用结论:容器裁切是信息损失,不是造型。ImageFit.Cover 用裁切换构图是主动取舍;borderRadius + clip 无意中裁掉插画信息是被动损失——前者值得,后者只该在插画本来就是圆形素材时才用。

改后:同一插画以 Contain 全显,无圆框无裁角

三、第二刀:权重用同一套设计系统表达

探索页右上角有两个入口:线索卡的「全部线索/收起」,与区域切换卡的「在地图上选」。改之前都是 10sp 裸文字,后者连图标都没有——而「在地图上选」是打开满屏地图覆盖层的主入口,权重反而最弱。同页的区域 chip 有描边、圆角、控件质感底,摆在一起「不像同一套控件」。

改法不发明新样式,复用既有设计系统的两个档位:

// 主操作:实心主材质胶囊 + 地图定位图标(新增 svg,与 chevron 同款描边风格)
Row({ space: 4 }) {
  Image($r('app.media.explore_icon_map_pin'))
    .width(12)
    .height(12)
    .objectFit(ImageFit.Contain)
  Text('在地图上选')
    .fontSize(11)
    .fontWeight(FontWeight.Medium)
    .fontColor(ShanhaiButtonPalette.text(this.isDarkMode, true))
    .fontFamily(SHANHAI_TITLE_FONT)
}
.padding({ left: 8, right: 9, top: 5, bottom: 5 })
.attributeModifier(new ShanhaiControlMaterial(this.isDarkMode, true))
// 次级操作:描边胶囊,与区域 chip 同族;chevron 展开时旋转 90°(accordion 惯例)
Row({ space: 3 }) {
  Text(this.clueGridExpanded() ? '收起' : '全部线索')
    .fontSize(11)
    .fontWeight(FontWeight.Medium)
    .fontColor(this.cream())
    .maxLines(1)
    .textOverflow({ overflow: TextOverflow.Ellipsis })
  Image($r('app.media.explore_icon_chevron')).width(10).height(10)
    .rotate({ z: 1, angle: this.clueGridExpanded() ? 90 : 0 })
}
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.attributeModifier(new ShanhaiControlMaterial(this.isDarkMode))
.borderRadius(9)
.borderWidth(1)
.borderColor(this.lineColor())

分级落在三处差别上,规格逐项对照:

规格主操作「在地图上选」次操作「全部线索 / 收起」
材质档位ShanhaiControlMaterial(isDarkMode, true) 实心主材质ShanhaiControlMaterial(isDarkMode) + 1px 描边
字号 / 字色11sp Medium,主材质字色11sp Medium,cream
图标map_pin 12×12(新增 svg)chevron 10×10,展开旋转 90°
圆角 / 内边距胶囊内边距 8/9/5/5borderRadius(9),8/8/5/5

字色与图标细节:主操作字色取 ShanhaiButtonPalette.text(this.isDarkMode, true)(深青渐变上的暖白),标题字体走 SHANHAI_TITLE_FONT;次操作字色提到 cream、chevron 走 accordion 惯例的 90° 旋转。map_pin 是新增图标:24×24 viewBox、2.2 描边宽,与既有 chevron 同一画风。材质本身由 AttributeModifier 挂进组件——这套材质只管底色、渐变、描边和按压态(亮度 0.86),不碰尺寸、布局与回调,所以同一档位在不同组件上观感一致。

两个入口的位置也有讲究,都是被布局约束逼出来的决定:「收起」放标题行,是因为展开后网格被 9 个区域占满三行,再塞一个格子会多出第四行;「在地图上选」放标题行,是因为 chip 网格是 3 列排布(5 个 chip + 「更多」恰好两行),再塞一个会把第三行撑成单格。真机取证了三种状态:线索卡描边胶囊、展开后的收起胶囊(chevron 朝下)、区域展开后标题行双胶囊并存——主次权重一眼可辨。

区域展开态:标题行「收起」描边胶囊与「在地图上选」实心主胶囊并存

四、第三刀:术语跟着语境走,不做全局替换

第三刀最像文案活,实则是数据建模活。应用里「收录」一词承担了两个语境:用户进度(discoveredIds 驱动——已探索哪些异兽)与内容覆盖(资料已入库——详情页收录了原文与注解)。前者的「已收录/待收录/收录进度」全部改为「已发现/待发现/发现进度」,后者的「收录」保留不变。

为什么不做全局替换?因为两个语境在同屏出现:图鉴页副标题「发现山海异兽踪迹」讲进度,卡片正文「详情页收录原文与注解」讲馆藏。全局替换会把后一句改成「详情页发现原文与注解」——语义当场崩坏。判定标准只认数据来源:凡是 discoveredIds / discovered 状态驱动的字符串归「发现」,凡是描述馆藏体量与资料范围的归「收录」。当时的清扫动了 8 个文件——Index.ets、图鉴工具栏、应用视图模型、探索面板、首页面板、桌面卡片快照服务、图鉴页头、聚焦卡片,构建 35s 通过、门禁当时 39 项全过。

当日复测的分布:ets 树全量计数,发现系 30 处、收录系 28 处。收录系里能抽出一组「合法保留」清单——暂未收录可直接查看的原文定位(原文缺位提示)、这里收录 N 件展品(展厅体量)、首页「已收录」手风琴卡(馆藏体量,与三张进度卡的「已发现」相对)、已收入图鉴(入馆结果)。术语不是越少越好,是每个词只在一个语境里出现。

五、写作当日复跑:5 红、2 伤 3 锈

三刀当时的验证口径是「门禁 39/39 无回归」。为写这篇,2026-11-09 复跑全量门禁——套件已从 39 项长到 52 项——结果 5 红。逐项查证后分成两类:

门禁红因定性处置
verify-quick-view-contractmodule.json5 缺 USE_FLOAT_BALL / FLOAT_VIEW 声明实伤恢复声明(含 reason 与 usedScene)
verify-button-material按钮 25 ≠ 校准值 23锈回补校准(10-30 后两轮各加一枚)
verify-myth-map-prototypebarHeight 三元式断言不匹配锈断言改写为双分支形态
verify-navigation-compatibilitybarFloatingStyle 三元式断言不匹配锈断言改写 + else 分支禁下发
verify-emulator-regressionsonChange 回调 4 ≠ 3锈计数回补(3 → 4)

实伤那一条值得展开。 54 篇接 App Linking 时,module.json5 的 abilities 段被重构——skills 数组里插入 browsable intent,顺手把 requestPermissions 数组里两条闪控权限删掉了。但闪控窗(floatView)是 user_grant 权限:运行时代码还在 requestPermissionsFromUser 申请 FLOAT_VIEW,声明却没了——这类伤的可怕之处在于静默:编译不报错,冷启动不崩,只有用户第一次点「闪控速览」时权限弹窗才会缺席。quick-view 门禁逐条断言声明、reason、usedScene 三件套,当场把它揪出来。

// 恢复的两条声明:闪控球 system_grant 声明即可用;闪控窗 user_grant 还要运行时申请
{
  "name": "ohos.permission.USE_FLOAT_BALL"
},
{
  "name": "ohos.permission.FLOAT_VIEW",
  "reason": "$string:float_view_permission_reason",
  "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
}

三处锈同源:AGC 自检修复把浮动栏的三元式拆成了 if/else 双构建分支——API < 26 的设备上 Tabs.barFloatingStyle 整个方法不存在,传 undefined 也会 undefined is not callable 崩在冷启动,所以「支持时下发材质、不支持时零高度原生栏」改由分支结构保证。实现变了,三处断言还钉在旧形态上。改写保持意图不变,例如 navigation-compatibility 的意图「材质只能走 barFloatingStyle」改写为「原生分支存在下发 + else 分支禁止下发」:

// 断言随实现搬家:意图不变(材质只能走 barFloatingStyle),形态跟住双分支
assert.match(floating, /\.barFloatingStyle\(this\.floatingBarStyle\(\)\)/,
  'The immersive material must be applied through Tabs.barFloatingStyle');
const nativeStart = floating.indexOf('if (this.useNativeFloatingBar()) {');
const [, fallbackBranch] = floating.slice(nativeStart).split(/(?<=\}) else \{/);
assert.doesNotMatch(fallbackBranch, /^\s*\.barFloatingStyle/m,
  'The fallback branch must never touch barFloatingStyle (API < 26 crash)');

按钮计数那一条则是纯粹的「校准值没有跟着回填」:10-18 校准到 23 之后,48 篇「碰一碰分享」与 49 篇「闪控速览」各给详情页加了一枚原生按钮,两轮都过了逐按钮断言(每枚都挂 ShanhaiNativeButtonMaterial),只有总数没更新。回补到 25 时把两轮出处写进校准注释——校准值不带出处的数字,下次还是无头案。

除了 5 红还有第 6 类:门禁没盯的角落。按第三刀的语境判定复查全应用,又抓到 5 处漏网——2×2 桌面卡的进度行、护照印章格的「待收录」、馆长目标卡两处(查看详情并收录 / 尚未收录)、探索路线文案的「收录这条路线的全部神兽」。全部改回发现系后再复跑,52/52 全绿,构建 BUILD SUCCESSFUL in 50 s。

门禁复跑 5 红:2 伤 3 锈的定性与处置

收个尾

三刀各留一句可带走的话:

  • 容器裁切是信息损失,不是造型。 Cover 的裁切是主动取舍,borderRadius + clip 的裁切是被动损失;插画类内容默认 Contain 全显,造型交给边框和底图去做。
  • 权重不发明新样式,用既有设计系统的档位表达。 主操作实心材质 + 图标,次操作描边同族;同一档位在哪个组件上都要长一个样——观感一致性靠复用,不靠逐处微调。
  • 术语的判定标准是数据来源,不是词语本身。 discoveredIds 驱动的归「发现」,馆藏体量的归「收录」;全局替换省下的时间,会用语义崩坏还回来。
  • 门禁是合同,实现搬家时合同要跟着搬。 三处锈都是实现重构后断言没跟上;断言改写必须保意图不变——「锁不变量」,不是「锁字面量」。
  • 门禁没盯的地方靠语境复查补位。 5 处术语漏网没有任何门禁报红,是按第三刀的判定标准逐组件扫出来的;给它们补门禁之前,复查是唯一的网。

2 伤 3 锈的定性与上一篇的钉点维护是同一课:区分「实伤」与「断言过期」靠证据(声明还在不在、代码现状长什么样),处置各自不同——实伤修复,锈则把意图重新钉牢。52 项里红 5 项不是坏事,红在写作当天比红在发布后便宜得多。

附:本篇证据清单

  • 门禁复跑(2026-11-09):app 套件 52 项全量复跑,处置后 52/52 PASS;处置前 5 红(quick-view / button-material / myth-map-prototype / navigation-compatibility / emulator-regressions)。
  • 构建(2026-11-09):HAP 构建 BUILD SUCCESSFUL in 50 s(33 tasks,31 executed)。
  • 验收记录:acceptance/2026-10-16-hero-card-circle-removal/(化蛇 0.95 / 巴蛇 0.67 前后对照)、acceptance/2026-10-16-explore-action-capsules/(三态胶囊截图)、acceptance/2026-10-16-terminology-shoulu-to-shifaxian/(图鉴页与探索页术语落点)。
  • 源码落点:feature/beast/ShanhaiBeastDetailHeroCard.ets(圆框拆除)、feature/world/ShanhaiExploreWorldPanels.ets(分级胶囊)、common/theme/ShanhaiButtonMaterial.ets(材质档位)、common/ui/ShanhaiProfileStampCell.ets 与 feature/curator/ShanhaiCuratorTargetCard.ets(术语补漏)、module.json5(权限声明恢复)。
  • 门禁改动:verify-button-material.mjs(校准 23→25,附两轮出处)、verify-myth-map-prototype.mjs 与 verify-navigation-compatibility.mjs(断言随双分支改写)、verify-emulator-regressions.mjs(onChange 3→4)。
  • 关联稿件:浮动栏材质作用域的来历见系列第 55 篇;闪控速览与 FLOAT_VIEW 运行时申请见系列第 49 篇;App Linking 重构与权限误删的肇事现场见系列第 54 篇;钉点维护与「实伤/过期」定性同源见系列第 64 篇。
  • 已知欠账:术语漏网 5 处靠人工语境复查抓出,尚未落到任何门禁——后续若再发生术语清扫,应先给「发现系/收录系」立一份逐文件钉点再动笔;胶囊与圆框两刀当时按 39 项门禁验证,本次复跑按 52 项口径补足。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐