跨端: 回复/转发改内联底栏 + 入场动画对称化(照鸿蒙文档纠正三处误判)
用户:「点击回复按键与新建邮件部分的动画与 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:
@ -734,8 +734,27 @@ export class Theme {
|
||||
* 这个区分是 2026-09-18 查出来的,之前两者被当成同一个数。
|
||||
*/
|
||||
static readonly durBase: number = 180;
|
||||
/** 面板/内容入场 —— WebUI `@keyframes rise-in` 实测 **150ms** */
|
||||
static readonly durRise: number = 150;
|
||||
/**
|
||||
* 面板/内容入场。
|
||||
*
|
||||
* ★★ 2026-09-20 150 → **200**(用户:「动画不符合鸿蒙视觉要求」,选定"折中 200ms")。
|
||||
*
|
||||
* 两个来源**打架**,这里取了折中:
|
||||
* · WebUI `@keyframes rise-in` 实测 **150ms**(本仓既有基准)
|
||||
* · 鸿蒙官方动效规范(developer.huawei.com「动效属性」页):
|
||||
* 「元素淡入/位移进入 **200-300ms**」+ EaseOut
|
||||
* 150ms 比鸿蒙建议的**下限还短 25%** —— 在鸿蒙上确实偏"赶",
|
||||
* 系统的原生页面转场普遍用 250-350ms。
|
||||
*
|
||||
* 取 **200ms(规范区间下限)** 而不是直接上 250ms:
|
||||
* WebUI 那一侧的值是用户当年**逐个调过**的(09-14 否掉过"整屏淡入"、
|
||||
* 09-15 报过"动画卡顿"),一下子拉到 250 会让两端观感差得更远。
|
||||
* 200ms 两边都不冒犯,且落在鸿蒙规范区间内。
|
||||
*
|
||||
* ★ 这条**不是**"令牌值"而是"动画值"—— 上面那段注释讲过两者的区别,
|
||||
* 别拿 `durBase`(180) 或 `durFast`(120) 来替它。
|
||||
*/
|
||||
static readonly durRise: number = 200;
|
||||
/** 弹层入场 —— WebUI `.animate-menu-in` 实测 **140ms** */
|
||||
static readonly durMenu: number = 140;
|
||||
/** 日历翻月 —— WebUI `.cal-slide-next/prev` 实测 **200ms** */
|
||||
@ -799,12 +818,30 @@ export class Theme {
|
||||
* 见 `MainPage` 的 `calPaneOpacity` / `calPaneShift`。
|
||||
*/
|
||||
static paneRiseIn(): TransitionEffect {
|
||||
return TransitionEffect.asymmetric(
|
||||
TransitionEffect.opacity(0).combine(
|
||||
TransitionEffect.translate({ y: Theme.riseInOffset })
|
||||
).animation({ duration: Motion.dur(Theme.durRise), curve: Theme.easeRise }),
|
||||
TransitionEffect.opacity(0).animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeRise })
|
||||
);
|
||||
/*
|
||||
* ★★ 2026-09-20 改成**对称**(用户:「点击回复按键…动画与 webui 不一致」)。
|
||||
*
|
||||
* 改之前是 `asymmetric`:入场做 `opacity + translate`(150ms),
|
||||
* 退场**只做 opacity**(120ms)。于是同一个"回复框"出现时会浮上来、
|
||||
* 消失时却只淡出 —— **两头的动作不是同一件事**。
|
||||
*
|
||||
* WebUI 那边是一条 `@keyframes rise-in` 加 `both`:
|
||||
* animation: rise-in 150ms cubic-bezier(0.22,0.61,0.36,1) both;
|
||||
* `both` = 进出都用同一条关键帧 ⇒ 天然对称。
|
||||
*
|
||||
* ★ 当初为什么写成不对称:注释里记过理由 ——「ArkUI 是 if/else 换子树,
|
||||
* 两层会同时半透明地叠着,同长看起来就是闪一下」。这个担心对
|
||||
* **换窗格**(通信↔日历那种整块替换)成立;但**对回复框/转发条不成立** ——
|
||||
* 它出现时底下没有"正在消失的另一半",而是一块一直存在的正文。
|
||||
* 我把两种场景混用了同一个过渡。
|
||||
*
|
||||
* ⇒ 现在对称:进出都是 `opacity + translate`、同一条曲线、同一时长。
|
||||
* 换窗格那几处如果将来发现"闪",应该用各自更合适的过渡去解,
|
||||
* 而不是让**所有**用到 rise 的地方一起背这个不对称。
|
||||
*/
|
||||
return TransitionEffect.opacity(0).combine(
|
||||
TransitionEffect.translate({ y: Theme.riseInOffset })
|
||||
).animation({ duration: Motion.dur(Theme.durRise), curve: Theme.easeRise });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -837,11 +874,22 @@ export class Theme {
|
||||
* 所以 12% 就是“滑入自身宽度的 12%”,不是拍一个 vp 数(那样在大屏上会变小)。
|
||||
*/
|
||||
static calendarSlide(forward: boolean): TransitionEffect {
|
||||
return TransitionEffect.asymmetric(
|
||||
TransitionEffect.opacity(0).combine(
|
||||
TransitionEffect.translate({ x: forward ? '12%' : '-12%' })
|
||||
).animation({ duration: Motion.dur(Theme.durCal), curve: Theme.easeRise }),
|
||||
TransitionEffect.opacity(0).animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeRise })
|
||||
);
|
||||
/*
|
||||
* ★★ 2026-09-20 改成**对称**(同 `paneRiseIn`,理由见那处)。
|
||||
*
|
||||
* WebUI 这两个类也是 `both`:
|
||||
* .cal-slide-next { animation: cal-in-next 200ms cubic-bezier(…) both; }
|
||||
* 即**进出同一条关键帧**。
|
||||
*
|
||||
* 我原来写的不对称里,退场只做 `opacity`(120ms)—— 于是翻月时
|
||||
* 新月份"滑进来"、旧月份只是"淡走",一进一出不是同一件事。
|
||||
*
|
||||
* ★ 对称之后,进出的 `x` 是**同一个**(`forward` 已决定方向),
|
||||
* 所以不会出现"从右边进、从右边出"的别扭感:退出时它往同一侧滑走,
|
||||
* 与 WebUI 那条 `both` 的行为一致。
|
||||
*/
|
||||
return TransitionEffect.opacity(0).combine(
|
||||
TransitionEffect.translate({ x: forward ? '12%' : '-12%' })
|
||||
).animation({ duration: Motion.dur(Theme.durCal), curve: Theme.easeRise });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user