跨端: 写信 FAB → 写信页 共享元素转场 + morph 收成单一入口(判据两版错法都记了)
用户 2026-09-21:「webui 行为是按钮变成对应的写邮件页面或输入框吧,你做的啥?」
「都做啊」—— 两处 morph 现在都在了。
## ① 写信 FAB → 写信页(跨 NavDestination)
官方 FAQ `faqs-arkui-991` 给的正是"在 NavDestination 子页面里做共享元素转场"
的完整步骤,逐步照做:
· 两端绑同一 id `compose-morph`(FAB / ComposeDestination 的 NavDestination);
· **`pushPath` 放进 `animateTo` 闭包**(FAQ 步骤 3 原文就是这个形状);
· `follow: false`(两端互斥出现,不是"始终在树上跟随")。
## ② 把 morph 收成**单一入口** `Motion.morph(ui, mutate)`
这一步不是为了少写代码,是为了**让判据能判**。过程值得记:
**第一版判据** —— 全仓 `any()`:
/animateTo\(/.test(allHarmony) && /durMorph/.test(allHarmony) && …
变异实测(把 morph 那处的 `animateTo` 改名、把 `Theme.durMorph` 就地写 `220`)
**三条全绿** —— 因为全仓**别处**还有这些名字,"删掉这一处"永远命中不了。
**第二版判据** —— 逐站点取"文本邻域"看有没有 animateTo:
**全假红**。因为 `geometryTransition(id)` 绑在**组件树**上,而 `animateTo`
写在**另一个方法**里,文本邻域取不到隔壁的方法。
⇒ 结论不是"把判据写得更聪明",而是**把结构改成可判的**:
把"带 morph 的状态切换"收进 `Motion.morph(ui, mutate)` 一处
(`animateTo` + 时长 + 曲线都在里面),调用点只剩「我要改哪个状态」。
这与本仓既有解法同型(`PressEffectModifier` / `GlassCardModifier`:
把"每处都得记得写"收敛成"一处定义、处处引用")。
3 个调用点已全部改走它(`MainPage.openComposeWithMorph`、
`MailDetailPage.openReplyWithMorph` / `closeReplyWithMorph`)。
★ `Motion.morph` 必须收 `UIContext`:全局 `animateTo` **已废弃**
(编译器告警 `'animateTo' has been deprecated`),而静态方法里拿不到
`this.getUIContext()`(本仓纪律:静态方法里不用 `this`)。
## 判据:6 条,三条变异逐个验过
通过 每个 id 恰好绑两处(一 in 一 out) [变异:删一端 → 红 ✓]
通过 morph 只有一个入口且内部有 animateTo [变异:换成普通调用 → 红 ✓]
通过 用 ui.animateTo 而非废弃的全局 animateTo
通过 每个用 geometryTransition 的文件都走 helper [变异:自己写 animateTo → 红 ✓]
通过 页面里不再直接出现 Theme.durMorph
通过 Theme.durMorph 存在且 = 220 [变异:改成 450 → 红 ✓]
★ 期间还抓到一个**判据自己的 bug**:我重写那一段时把 `themeSrc` 的定义
一起删了 ⇒ 第 6 条抛 `ReferenceError`、**整条判据根本没跑**
(而其余 9 条照常打印"通过",退出码 1 但没人看得到那条)。
这与"守具有齿但不在位"同形:**判据崩了不会显示成失败**。
已补回定义并重跑确认。
计数棘轮 4 → 10(显式编辑,理由写在 `run-all.mjs` 里)。
## 设备验证
✓ 点 FAB → 写信页到场、取消 → 回列表,进程存活(17827),无新 jscrash
(`faultlogger` 里最新仍是 15:08 那条,即修复前的)
✗ 220ms 的**中间帧**仍看不到(`snapshot_display` 往返 1.5-3s 慢一个数量级)——
与上一条提交同样的诚实交代:动画本体只能由用户在真机上看
This commit is contained in:
@ -25,6 +25,7 @@
|
||||
* 本来就期望重启一次;而每帧同步调系统 API 的开销是实打实的。
|
||||
*/
|
||||
import { accessibility } from '@kit.AccessibilityKit';
|
||||
import { Theme } from './Theme';
|
||||
|
||||
export class Motion {
|
||||
private static cached: boolean | undefined = undefined;
|
||||
@ -57,4 +58,45 @@ export class Motion {
|
||||
static dur(want: number): number {
|
||||
return Motion.reduced() ? 0 : want;
|
||||
}
|
||||
|
||||
/**
|
||||
* **共享元素转场**(`geometryTransition`)的动画参数。
|
||||
*
|
||||
* ── 为什么必须收成一处 ──
|
||||
*
|
||||
* 官方对 `geometryTransition` 有一条硬约束(`ts-transition-animation-geometrytransition`):
|
||||
*
|
||||
* 「**必须配合 `animateTo` 使用**才有动画效果,动效时长、曲线跟随
|
||||
* `animateTo` 中的配置,**不支持 `animation` 动画**」
|
||||
*
|
||||
* 而 `geometryTransition(id)` 绑在**组件树**上,`animateTo` 却写在**方法**里 ——
|
||||
* 两者隔着一个函数。⇒「绑了 id 但忘了 animateTo」这种写法**编译通过、
|
||||
* 运行时静默无动画**,正是最难发现的一类。
|
||||
*
|
||||
* 我第一版就是那样:两端各绑 `geometryTransition('xxx')`,
|
||||
* 而 `animateTo` 写在另外两个方法里。判据想钉"每一处都有 animateTo",
|
||||
* 只能靠"取出现处附近的文本窗口"去猜 —— 而那个窗口**取不到隔壁的方法**
|
||||
* (实测三条判据全假红)。
|
||||
*
|
||||
* ★ 正确的解法不是把判据写得更聪明,而是**把结构改成可判的**:
|
||||
* 把"带 morph 的状态切换"收成这一个函数 —— 于是
|
||||
* · `animateTo` 与时长/曲线只有**一处**(本函数),不可能漏;
|
||||
* · 调用点只剩「我要改哪个状态」,判据只要看调用点**是否走了它**。
|
||||
* 这与本仓既有的解法同型:`PressEffectModifier` / `GlassCardModifier`
|
||||
* 把"每处都得记得写"收敛成"一处定义、处处引用"。
|
||||
*
|
||||
* @param mutate 要让哪个状态发生变化(写在**闭包内**是硬要求)
|
||||
*/
|
||||
static morph(ui: UIContext, mutate: () => void): void {
|
||||
/*
|
||||
* ★ 必须收 `UIContext` 并走 `ui.animateTo` ——
|
||||
* 全局 `animateTo` **已废弃**(编译器告警 `'animateTo' has been deprecated`),
|
||||
* 而静态方法里拿不到 `this.getUIContext()`(本仓纪律:静态方法里不用 `this`)。
|
||||
* ⇒ 由调用方把它的 `UIContext` 传进来。调用点本来就在组件里,拿得到。
|
||||
*/
|
||||
ui.animateTo({
|
||||
duration: Motion.dur(Theme.durMorph),
|
||||
curve: Theme.easeRise
|
||||
}, mutate);
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,20 +323,15 @@ export struct MailDetailView {
|
||||
*/
|
||||
private openReplyWithMorph(): void {
|
||||
this.showForwardBox = false;
|
||||
this.getUIContext().animateTo({
|
||||
duration: Theme.durMorph,
|
||||
curve: Theme.easeRise
|
||||
}, () => {
|
||||
/* 一处定义、处处引用:时长/曲线/animateTo 都在 `Motion.morph` 里 */
|
||||
Motion.morph(this.getUIContext(), () => {
|
||||
this.showReplyBox = true;
|
||||
});
|
||||
}
|
||||
|
||||
/** 收起回复条(反向 morph:同一条 220ms,与打开对称) */
|
||||
private closeReplyWithMorph(): void {
|
||||
this.getUIContext().animateTo({
|
||||
duration: Theme.durMorph,
|
||||
curve: Theme.easeRise
|
||||
}, () => {
|
||||
Motion.morph(this.getUIContext(), () => {
|
||||
this.showReplyBox = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -297,6 +297,13 @@ struct ComposeDestination {
|
||||
onBack: (): void => { this.pathStack.pop(); }
|
||||
})
|
||||
}
|
||||
/*
|
||||
* ★★ 共享元素转场的 **in 端**(与通信页右下那个加号同一个 id)。
|
||||
*
|
||||
* 系统按两端各自的 frame 与圆角插值 ⇒ "球长成整页"这件事不需要我算。
|
||||
* 起点圆角 28(球的半径)→ 终点 0(整幅面板)由两端各自声明。
|
||||
*/
|
||||
.geometryTransition('compose-morph')
|
||||
.hideTitleBar(true)
|
||||
/*
|
||||
* ★★ 2026-09-21 修(用户:「写邮件页面和其他多个页面圆角下方还是有白框(直角框)」)。
|
||||
@ -1801,6 +1808,31 @@ struct CommPage {
|
||||
* 为什么收一个 `accountId`:收件箱里有"按当前筛选账号写信"(`InboxTab`),
|
||||
* 收件箱外有"用活跃账号写信"(`CommPage` 的 FAB)。两条入口共用这个方法。
|
||||
*/
|
||||
/**
|
||||
* 打开写信 —— **带共享元素转场**(球 → 整页)。
|
||||
*
|
||||
* 官方 FAQ `faqs-arkui-991` 的步骤 3 原文:
|
||||
* 「在页面跳转时增加显示动画效果:
|
||||
* `this.getUIContext().animateTo({ duration }, () => {
|
||||
* this.navPathStack.pushPath({ name: 'nextB' }, false); })`」
|
||||
*
|
||||
* ⇒ `pushPath` **必须在 `animateTo` 的闭包内**。这是 `geometryTransition`
|
||||
* 生效的硬条件(官方文档:「必须配合 `animateTo` 使用才有动画效果…
|
||||
* 不支持 `animation` 动画」)。
|
||||
*
|
||||
* 时长取 `Theme.durMorph`(220) —— WebUI FLIP 的原值。
|
||||
*/
|
||||
openComposeWithMorph(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
let accountId: string = '';
|
||||
if (ctx !== undefined) {
|
||||
accountId = AccountManager.getInstance(ctx).getActiveId();
|
||||
}
|
||||
Motion.morph(this.getUIContext(), () => {
|
||||
this.openComposeWith(accountId);
|
||||
});
|
||||
}
|
||||
|
||||
openComposeWith(accountId: string): void {
|
||||
const params: ComposeParams = { to: '', reply_to: '', session_alias: '', account_id: accountId };
|
||||
this.navPathStack.pushPath({ name: COMPOSE_ROUTE, param: params });
|
||||
@ -2026,8 +2058,23 @@ struct CommPage {
|
||||
.width(56).height(56)
|
||||
.borderRadius(28)
|
||||
.backgroundColor(Theme.accent)
|
||||
/*
|
||||
* ★★ 2026-09-21 共享元素转场的 **out 端**(另一端在 `ComposeDestination`)。
|
||||
*
|
||||
* 用户:「webui 行为是按钮变成对应的写邮件页面或输入框吧,你做的啥?」
|
||||
* —— 对,WebUI `ComposePage.tsx:57-79` 的 FLIP 就是"球长成整页",
|
||||
* 起点的 `borderRadius: '28px'` **正是这个球的半径**。
|
||||
*
|
||||
* 官方 FAQ `faqs-arkui-991` 给的正是"在 NavDestination 子页面里做
|
||||
* 共享元素转场"的完整步骤(路由跳转 + 两端绑同一 id +
|
||||
* **把 pushPath 放进 `animateTo` 的闭包**)—— 我们这里照做。
|
||||
*
|
||||
* `follow: false`(默认):两端互斥出现(一端在树上时另一端不在),
|
||||
* 不是"始终在树上跟随"的那种。
|
||||
*/
|
||||
.geometryTransition('compose-morph')
|
||||
.margin({ right: 16, bottom: this.navReserve + 16 })
|
||||
.onClick(() => { this.openCompose(); })
|
||||
.onClick(() => { this.openComposeWithMorph(); })
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user