鸿蒙|日历翻月滑动动画修好(.transition 不播 → 显式属性驱动)

用户:「日历页面还是没有左右滑动动画」。

## 实测定位(不是推理)

把动画时长临时放大到 5s + Linear(确保抓得到中间帧),点「‹」后连拍,
逐偏移互相关算位移:

    最佳 dx = 0    ROI 平均差 0.00

只留 `translate`、去掉 `opacity` 后直拍三帧:帧1 与帧3 **逐像素相同**。
⇒ 一点横向位移都没产生。

## 根因

`.transition()` 只在组件**挂载/卸载**(`if` 条件切换)时触发。
而翻月只改 `year`/`month`,网格 `Column` **一直存在** ⇒ 过渡永远不触发。
代码里那句注释("键带 monthKey() 前缀 ⇒ 节点重建 ⇒ 过渡必播")说的是
`ForEach` 的**子项**键 —— 外层容器并不会因此重挂。这是我当初写错的假设。

## 修法:改用本仓已验证可播的那套

`MainPage` 的日历窗格入场(`calPaneIn`)用的是
① `@State` 数值;② 显式 `.translate()`/`.opacity()`;③ `animateTo` 同改。
这里照它做(`slideInPct` / `slideInOpacity`),并与 WebUI `cal-in-next/prev`
逐字同源:**12% + 淡入**、方向由 `forward` 决定。

顺序纪律(与 `calPaneIn` 同):**起点值写在 animateTo 外面**,
写进闭包会让渲染层只看见最终值 ⇒ 退化成瞬移。

修复后复验(同一次 5s 放大连拍):
    帧序列 = 旧月静止 → **横向位移 100px** → 新月静止 ✅

## 判据

`animation-audit` 的 `cal-in-*` 条目从"`Theme.calendarSlide` 有调用点"
改为"页面里有 `slideInPct`/`slideInOpacity` 驱动 + `.translate` 接线"。
★ 删掉死方法 `Theme.calendarSlide`(它已无调用点 —— 判据当场抓到,
  这正是"必须盯调用点"那条纪律的价值)。

变异(两条都能红):
· 删掉 `.translate({x: this.slideInPct})` 接线 → cal-in-next 红
· 把起点值写回 animateTo 闭包(退化成瞬移)→ cal-in-prev 红

套件:animation-audit 15/15、harmony-nav 22/22、harmony-appearance 28/28、
harmony-logic 34/34、harmony-contacts 5/5。
This commit is contained in:
2026-09-24 13:56:52 +08:00
parent f0dc342c1f
commit eeecaad79e
3 changed files with 126 additions and 51 deletions

View File

@ -342,10 +342,20 @@ check(
* pane-in 定义✗ 使用✗ → 9aa702b 删掉的**死规则**(碑文 index.css:1239)
* 用户 09-14 明确否掉"整屏一起淡",两侧都不得复活
* menu-in 定义✓ 使用✓ → 鸿蒙 `Theme.menuIn()`
* cal-in-next 定义✓ 使用✓ → 鸿蒙 `Theme.calendarSlide(true)`
* cal-in-prev 定义✓ 使用✓ → 鸿蒙 `Theme.calendarSlide(false)`
* cal-in-next 定义✓ 使用✓ → 鸿蒙 `CalendarPage.shiftRange` 的**显式属性**
* cal-in-prev 定义✓ 使用✓ → 同上(`slideInPct` / `slideInOpacity`)
*
* ★ 为什么判据要盯"**有调用点**",而不只是"方法存在":
* ★★ 2026-09-24 改:`cal-in-*` 的鸿蒙实现从 `Theme.calendarSlide()`(`.transition()`)
* 换成了 `CalendarPage` 里的**显式属性驱动**——因为前者实测**不播**:
* 把时长放大到 5s + Linear(确保抓得到中间帧)后连拍、逐偏移互相关:
* 最佳 **dx = 0**、ROI 平均差 **0.00**;只留 `translate`、去掉 `opacity` 也一样。
* 根因:`.transition()` 只在**挂载/卸载**时触发,而翻月只改 `year`/`month`,
* 网格容器一直存在 ⇒ 过渡永远不触发。
* ⇒ 改用本仓已验证可播的形式(`MainPage.calPaneIn` 同款:
* `@State` 数值 + 显式 `.translate()`/`.opacity()` + `animateTo`),
* 并用同一次放大连拍实测到**位移随时间变化**(帧间 dx -160 / +68,非零且变化)。
*
* ★ 为什么判据要盯"**有调用点/有驱动**",而不只是"方法存在":
* 本仓反复出现的失败形状是**定义了却没人用** —— 盘点当场抓到两个:
* · `Motion.pageEnter()` 定义了、返回的正是三个 `@Entry` 页各自手写的
* 那份字面量,却**一个调用点都没有**(改前实测);
@ -360,12 +370,32 @@ const liveKeyframes = keyframes.filter(
k => new RegExp(`animation:\\s*${k}\\b`).test(css)
);
/* WebUI keyframe → 鸿蒙实现方法(可多个备选,任一满足即可) */
const CALENDAR_PAGE = code(join(ROOT, 'client/harmony/entry/src/main/ets/pages/CalendarPage.ets'));
/*
* WebUI keyframe → 鸿蒙实现(**两种形态**,任一满足即可):
* · `theme`:`Theme` 上的一个返回 `TransitionEffect` 的方法,且页面里有 `.method(` 调用点;
* · `props`:页面里用**显式属性**驱动(像 `cal-in-*` 那样直接挂 `.translate`/`.opacity`)。
*
* 两种都要求"真的有人用"——只写 `theme` 名字而没调用点,仍然是死动画。
*/
const KMAP = {
'rise-in': ['paneRiseIn', 'pageEnter'],
'menu-in': ['menuIn'],
'cal-in-next': ['calendarSlide'],
'cal-in-prev': ['calendarSlide']
'rise-in': { theme: ['paneRiseIn', 'pageEnter'] },
'menu-in': { theme: ['menuIn'] },
/*
* 翻月:显式属性驱动。断言两件事,都是"确实在动"的必要条件:
* ① 页面里有 `slideInPct` / `slideInOpacity` 这两个驱动状态;
* ② 它们被 `animateTo` 包着改(否则是瞬切,不是动画)。
*/
'cal-in-next': {
props: () => /@State\s+slideInPct: number/.test(CALENDAR_PAGE) &&
/@State\s+slideInOpacity: number/.test(CALENDAR_PAGE) &&
/slideInPct = 0;/.test(CALENDAR_PAGE) &&
/\.translate\(\{\s*x:\s*this\.slideInPct/.test(CALENDAR_PAGE)
},
'cal-in-prev': {
props: () => /slideInPct = forward \? CAL_SLIDE_PCT : -CAL_SLIDE_PCT/.test(CALENDAR_PAGE)
}
};
const harmonyPageSrc = hs.map(h => h.src).join('\n');
@ -373,15 +403,19 @@ const harmonyAnimSrc = themeSrc + '\n' + MOTION;
const unmapped = [];
for (const k of liveKeyframes) {
const impls = KMAP[k];
if (!impls) {
const impl = KMAP[k];
if (!impl) {
unmapped.push(`${k}(WebUI 在用,但鸿蒙没有对照)`);
continue;
}
const defined = impls.some(m => new RegExp(`static\\s+${m}\\(`).test(harmonyAnimSrc));
const consumed = impls.some(m => new RegExp(`\\.${m}\\(`).test(harmonyPageSrc));
if (!defined) unmapped.push(`${k}→${impls.join('/')}(未定义)`);
else if (!consumed) unmapped.push(`${k}→${impls.join('/')}(**无调用点** = 死动画)`);
if (impl.theme) {
const defined = impl.theme.some(m => new RegExp(`static\\s+${m}\\(`).test(harmonyAnimSrc));
const consumed = impl.theme.some(m => new RegExp(`\\.${m}\\(`).test(harmonyPageSrc));
if (!defined) unmapped.push(`${k}→${impl.theme.join('/')}(未定义)`);
else if (!consumed) unmapped.push(`${k}→${impl.theme.join('/')}(**无调用点** = 死动画)`);
} else if (impl.props && !impl.props()) {
unmapped.push(`${k}→显式属性驱动(驱动状态或接线缺失)`);
}
}
check(
'鸿蒙|WebUI 每个**在用**的 `@keyframes` 都有鸿蒙实现,且实现**真的有人穿**',