〖共创稿事节〗HarmonyOS 7 新特性实战(65):观感三刀——圆框裁图、胶囊权重与术语语境;52 项门禁复跑的 2 伤 3 锈
# 〖共创稿事节〗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 无意中裁掉插画信息是被动损失——前者值得,后者只该在插画本来就是圆形素材时才用。

三、第二刀:权重用同一套设计系统表达
探索页右上角有两个入口:线索卡的「全部线索/收起」,与区域切换卡的「在地图上选」。改之前都是 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/5 | borderRadius(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-contract | module.json5 缺 USE_FLOAT_BALL / FLOAT_VIEW 声明 | 实伤 | 恢复声明(含 reason 与 usedScene) |
| verify-button-material | 按钮 25 ≠ 校准值 23 | 锈 | 回补校准(10-30 后两轮各加一枚) |
| verify-myth-map-prototype | barHeight 三元式断言不匹配 | 锈 | 断言改写为双分支形态 |
| verify-navigation-compatibility | barFloatingStyle 三元式断言不匹配 | 锈 | 断言改写 + else 分支禁下发 |
| verify-emulator-regressions | onChange 回调 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。

收个尾
三刀各留一句可带走的话:
- 容器裁切是信息损失,不是造型。
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 项口径补足。
更多推荐
所有评论(0)