shared element:两个节点的连续 morph
FLIP 处理的是同一个 DOM 元素布局变化前后的过渡,列表生命周期处理重排。本页更进一步:列表视图里的小头像和详情视图里的大头像是两个不同的 DOM 节点,却代表同一个对象——切换视图时要让它看起来是同一个元素连续放大飞过去。
手法仍是 FLIP 的「先量首末、反推、播回 identity」,只是首末取自两个节点。
1 · 列表与详情之间
进入时量出列表头像的盒子作 first,让详情视图显示出来后量详情头像的盒子作 last,用 invert(first, last) 求出位移与缩放差,先把详情头像反推到列表头像的位置,再让一个 Animation 把这个因子从 1 播到 0。视觉上就是同一个头像飞过去。
2 · 时机比几何更难
几何部分与单元素 FLIP 完全一样,真正的难点在什么时候量 last。详情头像必须先真正参与布局,才能量出它该在的位置;而一旦它参与了布局又还没被反推,就会有一帧画出终态。
警示 · 在 Vue 里这一步要用 nextTick() 而不是 requestAnimationFrame()。nextTick 在 microtask 里冲刷 DOM 更新,仍在当前这一帧之内,反推 transform 赶得上;requestAnimationFrame
的回调发生在下一帧的渲染前,但样式变更已经在上一帧末尾被绘制过一次——终态会闪出来。原始的命令式版本直接改 hidden 属性,DOM 是同步更新的,所以根本不需要等待这一步。
两个头像的 background 颜色由 .av0 / .av1 / .av2 三个类给出,进入详情时要把类名同步过去。若忘了这一步,飞过去的形状对但颜色不对——这类「几何连续而外观跳变」的 bug 在共享元素里很常见,因为几何由 FLIP 保证,其余属性得自己对齐。
import { invert, invertTransform, Animation } from '@vega/anim';
// el 从 firstRect 的盒子, FLIP 回它自己当前的位置
function morphFrom(el, firstRect, dur) {
const last = el.getBoundingClientRect(); // 它真正该在的位置
const d = invert(box(firstRect), box(last)); // 位移 + 缩放差
el.style.transformOrigin = 'top left';
el.style.transform = invertTransform(d, 1); // 反推到 firstRect 处
return new Animation({
keyframes: [1, 0], duration: dur, ease: 'easeInOut',
onUpdate: (f) => el.style.transform = invertTransform(d, f), // 1→0 播回 identity
onComplete: () => el.style.transform = '',
});
}
// 进入详情: 量列表头像 (first), 挂出详情头像并从 first 飞过去
const first = chipAvatar.getBoundingClientRect();
showDetail();
morphFrom(heroAvatar, first, 460); // 两个不同节点, 过渡却连续
浏览器原生的 View Transitions 把这一整套自动化——给两个节点同名 view-transition-name 即可,对照见与原生对照。手写一遍是为看清它内部到底做了什么。单元素 FLIP 与拖拽惯性见高级交互,列表增删重排的 FLIP 见列表生命周期;为什么用 transform 反推而非动 left / width,见合成层。