跨端: 鸿蒙动画补齐 —— 并且发现原来的「逐字一致」是假的(令牌存在 ≠ 动画用了它)

用户:「A,同时把鸿蒙app的动画补齐」。

## 先纠一条错的前提(这是本轮最有价值的发现)

`Theme.ets` 的注释与 `harmony-nav` 的判据**都**写着:「三个数与 WebUI **逐字一致**:
`--ease-out-soft: cubic-bezier(0.22,1,0.36,1)`、`--dur-fast: 120ms`、`--dur-base: 180ms`」,
`paneRiseIn()` 就按 `durBase(180)` + `easeOutSoft` 做。

三个令牌**确实存在**(`index.css:310-312`)—— 但这句话把「令牌存在」当成了「动画用了它」:

| WebUI 里 | 真实用途 |
|---|---|
| `--dur-base: 180ms` | **只**用在壁纸淡入(`:747`) |
| `--ease-out-soft (0.22,1,0.36,1)` | **只**用在 transition(壁纸、控件变色) |
| **所有 @keyframes 动画** | 硬编码 **150ms / 200ms** + `cubic-bezier(0.22, 0.61, 0.36, 1)` |

全仓 `(0.22,0.61,0.36,1)` 出现 **5 次**(rise-in×3 / cal-in×2),`(0.22,1,0.36,1)` 只出现
**1 次**(令牌定义处)。**是两根不同的曲线** —— 旧代码的面板入场比 WebUI 慢 30ms 且曲线偏软。

所以令牌拆成两组,各对齐各的:控件类 `durFast + easeOutSoft`;动画类
`durRise(150)/durMenu(140)/durCal(200) + easeRise(0.22,0.61,0.36,1)`。

## 补的动画(对齐 WebUI 三个 @keyframes)

- `menuIn()` —— 弹层:下移 4vp + 缩到 0.985 + 淡入(`@keyframes menu-in`)。
  ★ 适用范围照搬 WebUI 注释那条窄口径(「只给真正是弹层的东西」):那条规则曾挂着
  `glass-control`,于是每次切视图**所有按钮与输入框一起淡入位移**,09-15 被摘掉。
  挂到写邮件页的账号候选列表上。
- `calendarSlide(forward)` —— 日历翻月:从 ±12% 横向滑入(`cal-in-next/prev`)。
  方向由新增 `@State slideForward` 带进 `animateTo` 闭包;网格键加 `monthKey()` 前缀
  保证月份一变所有键全变(否则"跨年同名月"那类边角会静默不播)。
- 写信页整页 `rise-in`、回复框 `rise-in`(WebUI 挂在回复框本身,不是外层遮罩 ——
  挂遮罩上会让整个屏幕一起位移,看起来是"页面在动"而不是"框弹出来")。

**实测确认真的会播**(不是"编译过了"):按本仓记录的手法把 `durCal` 临时改成 8000
做慢动作,连拍三帧 —— 截图硬证**两张月历同时在屏**(九月淡出、十月从右侧 12% 滑入),
验完还原成 200ms。

## 判据:从「令牌存在」改成「动画真的用了那个令牌」

`harmony-nav` 那条判据原文锚在令牌上,所以它对上面那个 bug **一辈子全绿**。
重写为:
- 从 WebUI `index.css` **读出** `rise-in` 的真实时长与曲线(`150ms` + 那条 bezier),
  再断言鸿蒙的 `durRise` 与曲线字面量与之逐字一致 —— 而不是"仓库里有没有 180 这个数";
- ★ 把 `paneRiseIn()` 的**函数体抠出来**单独断言它引的是 `durRise/easeRise`,
  且**不得出现** `durBase/easeOutSoft`。

**这一步不能省 —— 我第一版就漏了它**:断言了令牌存在、也断言了曲线字面量存在,
但没断言函数用了它们;于是把函数体换回旧的错值后判据**仍然全绿**。
变异自检抓住了这一点,补上函数体断言后同一个变异 ⇒ 判红 ✓。
与今天修的另一处同源:**判据要锚在"这个东西被用在哪",不是"它存在"**。
This commit is contained in:
2026-09-18 10:56:57 +08:00
parent a87a88ea2a
commit af2d2b5cad
6 changed files with 264 additions and 49 deletions

View File

@ -267,29 +267,62 @@ export class Theme {
static readonly fontBody: number = 14;
/*
* ── 动画令牌(对照 WebUI `index.css` 的 `--dur-*` / `--ease-out-soft`)──
* ── 动画令牌(对照 WebUI `index.css`)──
*
* 用户(2026-09-17):「一方面一点动画都没有」。
* 实测确认:改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过** ——
* 所有页面切换、详情推入、面板展开都是硬切。
*
* 三个数**与 WebUI 逐字一致**(`index.css:280-282`):
* --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1)
* --dur-fast: 120ms (控件变色:hover / active)
* --dur-base: 180ms (面板与内容入场)
* 这样两端的“快/慢”手感是同一个尺子,而不是各自拍一个数。
* ★★ 2026-09-18 更正:这里原先写着「三个数与 WebUI **逐字一致**」,**那句是错的**。
* 它把「令牌存在」当成了「动画用了那个令牌」。实测(`client/electron/src/index.css`):
*
* `--dur-base: 180ms` 只用在**壁纸淡入**(:747 `transition: opacity …`)
* `--ease-out-soft (0.22,1,0.36,1)` 只用在 **transition**(壁纸、控件变色 :1169)
* 而**所有 @keyframes 动画**用的是 150ms + `cubic-bezier(0.22, 0.61, 0.36, 1)`
*
* 全仓 `(0.22,0.61,0.36,1)` 出现 **5 次**(rise-in ×3 / cal-in ×2),
* `(0.22,1,0.36,1)` 只出现 **1 次**(就是令牌定义处)。**这是两根不同的曲线。**
* 旧代码把面板入场按 `(0.22,1,0.36,1)` + 180ms 做 ⇒ 比 WebUI **慢 30ms 且曲线偏软**,
* 而当时的判据(`harmony-nav` "缓动必须是 0.22,1,0.36,1")锚在**令牌**上,
* 所以这个错它一辈子抓不到。
*
* 所以下面分**两组**,各自对齐各自该对齐的东西:
* 控件类(transition) —— `durFast 120` + `easeOutSoft`
* 动画类(@keyframes)—— 各自时长 + `easeRise`
*/
/** 控件变色(hover / active)—— WebUI `--dur-fast: 120ms` */
static readonly durFast: number = 120;
/**
* 壁纸淡入 —— WebUI `--dur-base: 180ms`。
*
* ★ **只用于壁纸/整体淡入,不是面板入场**(面板入场是 `durRise`)。
* 这个区分是 2026-09-18 查出来的,之前两者被当成同一个数。
*/
static readonly durBase: number = 180;
/** 面板/内容入场 —— WebUI `@keyframes rise-in` 实测 **150ms** */
static readonly durRise: number = 150;
/** 弹层入场 —— WebUI `.animate-menu-in` 实测 **140ms** */
static readonly durMenu: number = 140;
/** 日历翻月 —— WebUI `.cal-slide-next/prev` 实测 **200ms** */
static readonly durCal: number = 200;
/**
* 缓动曲线:`cubic-bezier(0.22, 1, 0.36, 1)` —— 与 WebUI `--ease-out-soft` 同一根曲线。
* **transition** 用的缓动:`cubic-bezier(0.22, 1, 0.36, 1)` = WebUI `--ease-out-soft`。
*
* 用 `curves.cubicBezierCurve` 而不是 `Curve.EaseOut` 之类的枚举:
* 枚举是系统预设的**另一根**曲线,观感与 WebUI 对不上(而“对齐 WebUI”正是本项要求)。
* 枚举是系统预设的**另一根**曲线,观感与 WebUI 对不上。
*/
static readonly easeOutSoft: ICurve = curves.cubicBezierCurve(0.22, 1, 0.36, 1);
/**
* **@keyframes 动画**用的缓动:`cubic-bezier(0.22, 0.61, 0.36, 1)`。
*
* ★ 与 `easeOutSoft` **不是同一根曲线**(前者第二段控制点是 0.61,后者是 1)。
* 两者别互换:WebUI 里 transition 用 `--ease-out-soft`,而所有动画
* (rise-in / cal-in)硬编码用的是这一根。
*/
static readonly easeRise: ICurve = curves.cubicBezierCurve(0.22, 0.61, 0.36, 1);
/**
* 窗格入场时的上浮量(vp)。
*
@ -307,24 +340,60 @@ export class Theme {
* (`combine`/`animation` 会就地改自身),多处共用同一个实例会互相干扰;
* 每处调用各拿一个新的。
*
* ★ 两边时长**不对称**:出场(120ms)比入场(180ms)快。
* 同长会让新旧两层半透明地叠着,看起来像"闪一下"——
* 用户对"闪"敏感(09-14 否掉过整屏淡入)。
* ★ 时长/曲线用 `durRise` + `easeRise`(**150ms + 0.22,0.61,0.36,1**)——
* 即 WebUI `rise-in` 的**真实取值**。改之前用的是 `durBase(180)` + `easeOutSoft`,
* 那是令牌值而不是动画值(见上面令牌区的更正说明)。
*
* # 实测如何验证它真的生效
*
* `snapshot_display` 有 ~1s 往返,180ms 的过渡**抓不到**(连拍三帧全一样,
* 会得出"动画没做"的错误结论)。做法:临时把 `durBase`/`durFast` 改成 20000,
* 再过 6s 截图 —— 正常应看到新旧两层**同时半透明**地叠着
* (实测已确认:收件箱与日历交叉淡入,样例点 (628,1450) 是混合值 164,165,167)。
* 验完把时长还原成 120/180。
* ★ 两边**不对称**:出场用 `durFast`(120ms)比入场快。
* WebUI 那边 `rise-in` 是对称的(一个 @keyframes 两用)—— 因为它是**逐个元素**
* 挂载即播,新旧两棵子树不会同时可见。而 ArkUI 这里是 `if/else` 换子树,
* 两层会**同时半透明地叠着**,同长看起来就是“闪一下”(实测 6 秒取样确认),
* 所以出场必须更快地让位。这是两端机制不同带来的**必要差异**,不是随手拍数。
*/
static paneRiseIn(): TransitionEffect {
return TransitionEffect.asymmetric(
TransitionEffect.opacity(0).combine(
TransitionEffect.translate({ y: Theme.riseInOffset })
).animation({ duration: Theme.durBase, curve: Theme.easeOutSoft }),
TransitionEffect.opacity(0).animation({ duration: Theme.durFast, curve: Theme.easeOutSoft })
).animation({ duration: Theme.durRise, curve: Theme.easeRise }),
TransitionEffect.opacity(0).animation({ duration: Theme.durFast, curve: Theme.easeRise })
);
}
/**
* 弹层入场:下移 4vp + 缩到 0.985 + 淡入(对齐 WebUI `@keyframes menu-in`)。
*
* WebUI `.animate-menu-in` 的注释把适用范围钉得很窄,这里照搬同一个口径:
* 「只给**真正是弹层**的东西(候选/下拉菜单自己穿)」——
* 它原本还挂着 `html.view-switch .glass-control`,于是**每次切视图页面上所有
* 按钮与输入框一起淡入位移**(几十个元素同时动),2026-09-15 被摘掉。
* 所以本方法**只给弹层**(账号选择器、下拉候选),不要挂到常驻控件上。
*
* 入场方向是**从上往下**(`translateY(-4px)` → 0):弹层从触发点的下方展开,
* 从上方“落”下来。与 `rise-in` 的**上浮**方向相反,别混。
*/
static menuIn(): TransitionEffect {
return TransitionEffect.opacity(0).combine(
TransitionEffect.translate({ y: -4 })
).combine(
TransitionEffect.scale({ x: 0.985, y: 0.985 })
).animation({ duration: Theme.durMenu, curve: Theme.easeRise });
}
/**
* 日历翻月的横向滑入(对齐 WebUI `@keyframes cal-in-next/prev`)。
*
* `forward = true` → 下一月,从**右侧**滑入(`translateX(12%)`);
* `forward = false` → 上一月,从**左侧**滑入(`translateX(-12%)`)。
*
* 百分比在 ArkUI 的 `translate` 里是**相对元素自身尺寸**的 —— 与 CSS 语义一致,
* 所以 12% 就是“滑入自身宽度的 12%”,不是拍一个 vp 数(那样在大屏上会变小)。
*/
static calendarSlide(forward: boolean): TransitionEffect {
return TransitionEffect.asymmetric(
TransitionEffect.opacity(0).combine(
TransitionEffect.translate({ x: forward ? '12%' : '-12%' })
).animation({ duration: Theme.durCal, curve: Theme.easeRise }),
TransitionEffect.opacity(0).animation({ duration: Theme.durFast, curve: Theme.easeRise })
);
}
}