跨端: 回复/转发改内联底栏 + 入场动画对称化(照鸿蒙文档纠正三处误判)
用户:「点击回复按键与新建邮件部分的动画与 webui 不一致,动画不符合鸿蒙视觉
要求」。两个问题是分开的:结构是覆盖式弹层 vs WebUI 的内联底栏;动画则是我
单方面发明的不对称过渡 + 150ms 低于鸿蒙规范下限。
## 结构:覆盖式弹层 → 底部内联条(回复 / 转发)
WebUI `MailView.tsx:410/1057` 的 `ReplyBar`/`ForwardBar` 是 `border-t` 分出的
**内联底栏**,与正文并列(正文 `flex-1 overflow-y-auto` 保持可见可滚),高度由
内容决定。我们原先是整屏遮罩 + `height('60%')` + `position({x:0,y:0})`。
三条用户可感知的差异:弹层盖住正文(写回复时看不到原文)/固定 60% 高(写一行
也占半屏)/遮罩整屏变暗。结构不用动外层 —— 原版那两处本来就是正文 Stack 的
**兄弟**(同在 `Column` 里 ⇒ 本来竖直排列),错只错在给条加了遮罩/定高/绝对定位。
## 动画
① `paneRiseIn()` / `calendarSlide()` 去 `asymmetric`,改**对称**。
WebUI 是 `animation: rise-in 150ms … both` —— `both` 就是进出同一条关键帧。
我原先让出场只做 `opacity` 且更短(120ms),"出现时浮上来、消失时只淡出",
正是"与 webui 不一致"的来源。当初写不对称的理由(换窗格时两层同时半透明会
"闪")只对**换窗格**成立,对回复框/转发条不成立 —— 我把两种场景混用了。
② `durRise` 150 → **200ms**(用户选定"折中")。WebUI 是 150(web 常规档),
鸿蒙官方「元素淡入/位移进入」建议 **200-300ms**,150 比下限还低 25%。
## 照文档纠正三处误判(本轮的真正收获)
我为了搞清"为什么动画不播",先后编出过三个错误理论,读文档后逐条推翻:
① **不是 "NavDestination 吃掉子组件的 `.transition()`"**。
实测:`ComposeView` 根上的 `.transition()` 一直在播。我之所以连测七八轮都报
"没有中间帧",是因为**拿平均亮度当探针** —— 白底窗格 50% 透明叠在浅色背景上
平均亮度几乎不变。换成**位移**探针后,立刻看到"整栏下移 300vp 且半透明"的
中间帧。教训:**探针对被测变化不敏感时,量的是噪声**。
② **不 `customTransition` 也能做**。`NavDestination` 确实有 `customTransition`
(API 15+),但它是**整页转场**,我们要的只是内容块的一次上浮淡入。
(顺带记一条:`NavDestinationTransition.curve` 的类型是枚举 `Curve`,
不收 `ICurve` —— 试过用 `curves.cubicBezierCurve` 会编译报错。)
③ **`.opacity()` 在 `NavDestination` 上是生效的**。先前判定"不生效"同样是那个
废探针害的;换 `opacity(0)` 二元判定后整页消失,证明它一直生效。
## 连带修一个真 bug(判据抓的)
回复/转发改成内联后**失去了"弹层有固定高度"这层键盘保护** —— 官方默认
`KeyboardAvoidMode.OFFSET`(整页上移)会把贴底的「取消/发送/转发」顶出屏幕。
在 `EntryAbility` 里显式设 `RESIZE`(按剩余高度重排)。坑:`@kit.ArkUI` 与全局
作用域各有一个同名 `KeyboardAvoidMode`,**只有前者有 `RESIZE`**。
## 判据(4 条红全部结算,逐条说明为什么不是放宽)
· `harmony-nav` durRise:从"逐字等于 150"改为**区间 200-300**(钉住用户裁定,
退回 150 与写 800 都红,已变异验证)。
· `harmony-nav` asymmetric:**反转**为"不得 asymmetric"(旧断言把上一版设计锁住,
而 WebUI 本来就是对称的)。
· `harmony-admin` 弹层高度:原断言数的形状只属于废弃的覆盖式弹层 → 改为
**新结构下的等价不变式**(键盘避让必须 RESIZE)。这条判据当年抓的是真 bug,
该 bug 换了形态仍在,所以不能简单删。
· `cross-client-theme`:删掉我中途废弃留下的孤儿令牌 `riseCurveEnum`。
新增 4 条变异条目(全部 `红✓`);`mutants=52 ran=52 skipped=0`。
`files=33 checks=530 pass=530 fail=0`,`baseline=7/7✓`。
设备已验:回复/转发确为内联底栏(正文可见、`border-t` 分隔)。
This commit is contained in:
@ -398,10 +398,38 @@ test('★ 窗格切换有真的过场动画(transition 挂在会换的那棵
|
||||
const [, riseMs, riseCurve] = webuiRise;
|
||||
assert.equal(riseMs, '150', `WebUI rise-in 实测是 150ms,读到 ${riseMs}ms —— 参照物变了,要重核两端`);
|
||||
|
||||
// 鸿蒙的 durRise 必须= WebUI rise-in 的真实时长(不是 --dur-base 那个 180)
|
||||
assert.match(theme, new RegExp(`durRise:\\s*number\\s*=\\s*${riseMs}\\b`),
|
||||
`★ paneRiseIn 的时长必须等于 WebUI rise-in 的真实值 ${riseMs}ms。` +
|
||||
'写 180(=--dur-base)是拿“壁纸淡入的时长”当“面板入场的时长”——那是两块不同的东西。');
|
||||
/*
|
||||
* ★★ 2026-09-20 改:不再逐字要求鸿蒙 == WebUI 的那个数,改为**有依据的区间**。
|
||||
*
|
||||
* 用户裁定(原话:「点击回复按键与新建邮件部分的动画与 webui 不一致,动画不符合
|
||||
* 鸿蒙视觉要求」),三个选项里选了 `compromise_200` = **200ms**。
|
||||
* 两端各自的依据都成立:
|
||||
* · WebUI rise-in 实测 150ms —— 它是 Web,150ms 是 web 常规档;
|
||||
* · 鸿蒙官方「元素淡入/位移进入」建议 **200-300ms** —— 150ms 比下界还低 25%,
|
||||
* 在 60/120Hz 的移动端会读成"一瞬间跳出来",这正是用户说的"不符合鸿蒙视觉要求"。
|
||||
* ⇒ 判据不再钉死某一个数,而是钉**区间**:既拦住退回 150(低于鸿蒙下界),
|
||||
* 也拦住乱写(>300 会显得拖沓)。参照物仍从 WebUI 源码实时读出,不写第二份。
|
||||
*/
|
||||
const HARMONY_ENTER_MIN_MS = 200;
|
||||
const HARMONY_ENTER_MAX_MS = 300;
|
||||
assert.equal(riseMs, '150', `WebUI rise-in 实测是 150ms,读到 ${riseMs}ms —— 参照物变了,要重核两端`);
|
||||
const riseToken = /durRise:\s*number\s*=\s*(\d+)\b/.exec(theme);
|
||||
assert.ok(riseToken, '★ 要有 `durRise` 这个入场时长令牌(`paneRiseIn()` 的时长来源)');
|
||||
const riseVal = Number(riseToken[1]);
|
||||
assert.ok(riseVal >= HARMONY_ENTER_MIN_MS && riseVal <= HARMONY_ENTER_MAX_MS,
|
||||
`★ 入场时长要在鸿蒙官方建议区间 ${HARMONY_ENTER_MIN_MS}-${HARMONY_ENTER_MAX_MS}ms 内,实际 ${riseVal}ms。\n` +
|
||||
` 写 ${riseMs}(照抄 WebUI)低于鸿蒙下界 —— 用户 2026-09-20 原话「动画不符合鸿蒙视觉要求」。\n` +
|
||||
' 写 180(=--dur-base)则是拿"壁纸淡入的时长"当"面板入场的时长",那是两块不同的东西。');
|
||||
if (riseVal !== Number(riseMs)) {
|
||||
/*
|
||||
* 偏离 WebUI 数值时必须写明理由。用 `prose()` 读**原文**(含注释)——
|
||||
* `theme` 是 `code()` 读的(注释已被剥掉),拿它查注释必然失配。
|
||||
*/
|
||||
const themeRaw = prose(join(HARMONY_ETS, 'common', 'Theme.ets'));
|
||||
assert.match(themeRaw, /durRise[\s\S]{0,900}?(WebUI|rise-in)/,
|
||||
'★ 偏离 WebUI 数值时,`durRise` 附近必须有一段注释写明为什么偏离' +
|
||||
'(否则下一个人只会当它是随手写的数)。');
|
||||
}
|
||||
|
||||
// 曲线:必须与 rise-in 那条**逐字**一致(而不是与 --ease-out-soft 一致)
|
||||
const curveNums = riseCurve.split(',').map(s => s.trim().replace(/\.0+$/, ''));
|
||||
@ -467,11 +495,29 @@ test('★ 窗格切换有真的过场动画(transition 挂在会换的那棵
|
||||
assert.ok(!/Theme\.easeOutSoft/.test(body),
|
||||
'★ paneRiseIn 里不得出现 `easeOutSoft`(那是 transition 的曲线,不是 @keyframes 的)');
|
||||
|
||||
// transition 构造器存在,且入场/出场**不对称**(同长会闪)
|
||||
/*
|
||||
* transition 构造器存在,且**入场/出场对称**。
|
||||
*
|
||||
* ★★ 2026-09-20 反转(原断言要求 `asymmetric`,现要求**不得** asymmetric)。
|
||||
*
|
||||
* 用户原话:「点击回复按键与新建邮件部分的动画与 webui 不一致」。
|
||||
* 而 WebUI 那边**本来就是对称**的 —— `index.css:1326/1331/1336` 三条
|
||||
* `.rise-in`/`.pane-rise` 全部是 `animation: rise-in 150ms ... both`,
|
||||
* `both` = 进出都走同一条关键帧。所以"不对称"这件事是**鸿蒙单方面的发明**,
|
||||
* 它正是"与 webui 不一致"的来源之一。
|
||||
*
|
||||
* 原先写 asymmetric 的理由(注释里记着)是「同长会让新旧两层半透明叠着,
|
||||
* 看起来像闪一下」。那个担心对**换窗格**成立(两层同时存在),
|
||||
* 但对**回复框/转发条**不成立 —— 它出现时底下是一直存在的正文,不是"正在消失的另一半"。
|
||||
* 用同一个过渡同时服务两种场景,是当初把两件事混了。
|
||||
* ⇒ 按 WebUI 对齐:对称。(换窗格若将来发现"闪",应该用各自更合适的过渡去解。)
|
||||
*/
|
||||
assert.match(theme, /paneRiseIn\(\):\s*TransitionEffect/,
|
||||
'要有 paneRiseIn() 这个过渡构造器');
|
||||
assert.match(body, /TransitionEffect\.asymmetric\(/,
|
||||
'★ 入场/出场必须 asymmetric(出场更快)—— 同长会让新旧两层半透明叠着,看起来像"闪一下"');
|
||||
assert.ok(!/TransitionEffect\.asymmetric\(/.test(body),
|
||||
'★ `paneRiseIn` 不得用 `asymmetric` —— WebUI 的 `.rise-in`/`.pane-rise` 是 `both`(进出同一条),\n' +
|
||||
' 不对称是鸿蒙单方面的发明,正是用户说的"与 webui 不一致"。\n' +
|
||||
' (若某处**换窗格**确实需要出场更快,给那一处单独的过渡,不要改这个通用构造器。)');
|
||||
|
||||
// ② 关键:transition 要挂在 if/else 的**分支根**上
|
||||
const branchRoots = (mainCode.match(/\}\)\s*\n\s*\.transition\(Theme\.paneRiseIn\(\)\)/g) ?? []).length;
|
||||
|
||||
Reference in New Issue
Block a user