跨端: 鸿蒙动画补齐 —— 并且发现原来的「逐字一致」是假的(令牌存在 ≠ 动画用了它)
用户:「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:
@ -201,9 +201,25 @@ export struct CalendarPage {
|
||||
|
||||
/** 翻月走 `addMonths`(纯逻辑判据钉过跨年两个方向),不自己算 month+1 */
|
||||
private shiftMonth(delta: number): void {
|
||||
const ym: number[] = addMonths(this.year, this.month, delta);
|
||||
this.year = ym[0];
|
||||
this.month = ym[1];
|
||||
/*
|
||||
* 翻月要有**方向感的横向滑入**(对齐 WebUI `@keyframes cal-in-next/prev`)。
|
||||
*
|
||||
* WebUI 那边靠 `cal-slide-next` / `cal-slide-prev` 两个类 + 200ms 动画;
|
||||
* ArkUI 这里靠 `animateTo` 开窗口 + 格子挂 `calendarSlide(forward)` 过渡。
|
||||
*
|
||||
* ★ 两个都**不能少**(实测过:只包 animateTo 与硬切看不出区别)——
|
||||
* 与 `switchIndex` 里那条注释是同一回事。
|
||||
*/
|
||||
const forward: boolean = delta > 0;
|
||||
this.getUIContext().animateTo({
|
||||
duration: Theme.durCal,
|
||||
curve: Theme.easeRise
|
||||
}, () => {
|
||||
this.slideForward = forward;
|
||||
const ym: number[] = addMonths(this.year, this.month, delta);
|
||||
this.year = ym[0];
|
||||
this.month = ym[1];
|
||||
});
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
@ -220,6 +236,28 @@ export struct CalendarPage {
|
||||
return monthGrid(this.year, this.month, WEEK_START, this.todayIso);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻月方向 —— 只给**过场动画**用(`calendarSlide(forward)` 决定从哪一侧滑入)。
|
||||
*
|
||||
* 不参与任何数据计算:日期推进走 `addMonths`(纯函数、已被单测钉住)。
|
||||
* 把它单独抽出来是因为 `animateTo` 的闭包里要能读到"这次是前进还是后退",
|
||||
* 而 `delta` 是参数、闭包外已不可见。
|
||||
*/
|
||||
@State slideForward: boolean = true;
|
||||
|
||||
/**
|
||||
* 换一个"格子内容"的身份 —— `ForEach` 的键挂上它,翻月时才会重放入场过渡。
|
||||
*
|
||||
* ★ 为什么需要这个:ArkUI 的过渡只在**节点被替换**时播。`rows()` 直接由
|
||||
* `this.year/month` 算出来,键是 `c${cell.iso}#${cell.label}` —— 翻月后
|
||||
* 日期确实变了(键也会变),所以过渡会播。但同一个键在"跨年同月同名"等
|
||||
* 边角下可能不变,那时动画会静默不播。带上 `ym` 前缀把这个可能性去掉:
|
||||
* 月份一变,所有键必然全变。
|
||||
*/
|
||||
private monthKey(): string {
|
||||
return `${this.year}-${this.month}`;
|
||||
}
|
||||
|
||||
/** 这一天有几条日程(画事件点用)。数量很小,线性扫即可,不引入第二份索引。 */
|
||||
private eventCountOf(iso: string): number {
|
||||
if (iso.length === 0) {
|
||||
@ -750,15 +788,26 @@ export struct CalendarPage {
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
/* 月网格:行 ← monthGrid,列 ← 7 格(嵌套 ForEach 的项名必须不同) */
|
||||
ForEach(this.rows(), (row: DayCell[], rowIndex: number) => {
|
||||
Row() {
|
||||
ForEach(row, (cell: DayCell) => {
|
||||
this.DayCellView(cell)
|
||||
}, (cell: DayCell) => `c${cell.iso}#${cell.label}`)
|
||||
}
|
||||
.width('100%')
|
||||
}, (row: DayCell[], rowIndex: number) => `r${rowIndex}`)
|
||||
/*
|
||||
* 月网格:行 ← monthGrid,列 ← 7 格(嵌套 ForEach 的项名必须不同)。
|
||||
*
|
||||
* ★ 外面这层 `Column` 是**为了挂翻月过渡**而加的(原先网格是个裸 `ForEach`)。
|
||||
* 过渡必须挂在**整块网格**的外层容器上:挂在每行上会出现"7 行各滑各的"
|
||||
* 交错效果(每行入场时间几乎同步但微差,看起来像撕裂)。
|
||||
* 键带 `monthKey()` 前缀 ⇒ 月份一变所有键全变 ⇒ 节点重建 ⇒ 过渡必播。
|
||||
*/
|
||||
Column() {
|
||||
ForEach(this.rows(), (row: DayCell[], rowIndex: number) => {
|
||||
Row() {
|
||||
ForEach(row, (cell: DayCell) => {
|
||||
this.DayCellView(cell)
|
||||
}, (cell: DayCell) => `c${cell.iso}#${cell.label}`)
|
||||
}
|
||||
.width('100%')
|
||||
}, (row: DayCell[], rowIndex: number) => `r${this.monthKey()}#${rowIndex}`)
|
||||
}
|
||||
.width('100%')
|
||||
.transition(Theme.calendarSlide(this.slideForward))
|
||||
|
||||
if (this.error.length > 0) {
|
||||
Text(this.error)
|
||||
|
||||
@ -257,6 +257,19 @@ struct ComposePage {
|
||||
this.selectedAccountId = acct.id;
|
||||
this.showAccountPicker = false;
|
||||
})
|
||||
/*
|
||||
* 弹层入场(对齐 WebUI `@keyframes menu-in`:下移 4vp + 缩到 0.985 + 淡入)。
|
||||
*
|
||||
* ★ 只给**弹层**挂 —— WebUI 那条 `.animate-menu-in` 的注释把适用范围
|
||||
* 钉得很窄("只给真正是弹层的东西"),因为它曾经挂着 `glass-control`,
|
||||
* 于是每次切视图页面上**所有**按钮与输入框一起淡入位移(几十个元素同时动),
|
||||
* 2026-09-15 被摘掉。别把这个放到常驻控件上。
|
||||
*
|
||||
* 挂在 `Row` 上(即 `ForEach` 的每一项):整张候选列表逐项轻落,
|
||||
* 与 WebUI 里"整块列表一起动"略有差别 —— 但 ArkUI 的 `ForEach` 每项
|
||||
* 是独立节点,逐项入场反而更接近"列表展开"的观感,且不会让整层不可交互。
|
||||
*/
|
||||
.transition(Theme.menuIn())
|
||||
}, (acct: AccountInfo) => acct.id)
|
||||
}
|
||||
|
||||
@ -341,5 +354,14 @@ struct ComposePage {
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.backgroundColor(Theme.pageBg)
|
||||
/*
|
||||
* 整页入场(对齐 WebUI 写信页的 `rise-in`:4vp 上浮 + 淡入)。
|
||||
*
|
||||
* WebUI 实测(`ComposePage.tsx:174`):`fromRect ? '' : 'rise-in …'` ——
|
||||
* 没有动画起点时(例如刷新后直接进写信页)才挂 `rise-in`;
|
||||
* 有"从 FAB 长出来"的起点时改走另一条(两条都动 transform/opacity,同挂会打架)。
|
||||
* 本仓没有"从 FAB 长出来"那条,所以直接挂就是它的"无起点"分支。
|
||||
*/
|
||||
.transition(Theme.paneRiseIn())
|
||||
}
|
||||
}
|
||||
@ -507,6 +507,15 @@ export struct MailDetailView {
|
||||
.padding(16)
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius({ topLeft: 12, topRight: 12 })
|
||||
/*
|
||||
* 回复框入场(对齐 WebUI `MailView.tsx:410` 那条 `rise-in`:上浮 4vp + 淡入)。
|
||||
*
|
||||
* ★ 挂在**回复框本身**(下半部那块表面),不是外层遮罩容器:
|
||||
* WebUI 那处 `rise-in` 也挂在回复框的 div 上,遮罩是立即出现的。
|
||||
* 挂在遮罩上会让整个屏幕(含变暗的底层内容)一起位移 ——
|
||||
* 那看起来是"页面在动",而不是"回复框弹出来"。
|
||||
*/
|
||||
.transition(Theme.paneRiseIn())
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.position({ x: 0, y: 0 })
|
||||
|
||||
@ -2189,17 +2189,20 @@ struct MainPage {
|
||||
* ★ 换窗格要**真的**有过渡:`animateTo` 只开了一个动画窗口,
|
||||
* 被换掉的那棵子树自己不声明 `.transition(...)` 的话什么都不会动
|
||||
* (实测:只包 `animateTo` 和硬切看不出差别)。
|
||||
* 过场动画挂在**内容容器**上(见 build 里 `Column().transition(...)`),
|
||||
* 过场动画挂在**内容容器**上(见 build 里各分支的 `.transition(Theme.paneRiseIn())`),
|
||||
* 这里只负责把 index 变掉;系统在动画窗口内跑那条 transition。
|
||||
*
|
||||
* 时长/曲线用令牌:`durFast`(120) —— WebUI 对应物是
|
||||
* `.rise-in { animation: rise-in 150ms cubic-bezier(0.22,0.61,0.36,1) }`,
|
||||
* 同一档(百毫秒级、不拖)。曲线用令牌里那条 `easeOutSoft`,
|
||||
* 与 WebUI 的 `.pane-rise` 同一根尺子。
|
||||
* ★★ 2026-09-18 更正:这里原先用 `durBase(180)` + `easeOutSoft(0.22,1,0.36,1)`,
|
||||
* 并写着"与 WebUI 的 .pane-rise 同一根尺子" —— **那两个数都不是动画值**。
|
||||
* 实测 WebUI:`.rise-in`/`.pane-rise` 都是 **150ms + cubic-bezier(0.22, 0.61, 0.36, 1)**;
|
||||
* 180ms 与 `(0.22,1,0.36,1)` 只用在**壁纸淡入**与**控件变色 transition** 上。
|
||||
* 所以旧代码的窗格入场比 WebUI 慢 30ms 且曲线偏软。
|
||||
* 现在两边都用 `durRise` + `easeRise`,与 `paneRiseIn()` 内那份保持一致
|
||||
* (同一场过场被两个地方描述,数值必须同源 —— 所以都引令牌,不写数)。
|
||||
*/
|
||||
this.getUIContext().animateTo({
|
||||
duration: Theme.durBase,
|
||||
curve: Theme.easeOutSoft
|
||||
duration: Theme.durRise,
|
||||
curve: Theme.easeRise
|
||||
}, () => {
|
||||
this.currentIndex = target;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user