skip to content
usubeni fantasy logo Usubeni Fantasy

你没有用过的船新版本——跨页面共享元素动画

/ 6 min read

  • 本文主要参考 GitHub shared-element-transitions
  • 无法访问 GitHub 可以使用 Gitee 镜像
  • 共享元素动画使用要求,版本 101 后,开启 chrome://flags/#document-transition
  • 过渡接口不一定稳定,可能有变

效果示例

视频来源于 GitHub,存在无法播放的情况

原理浅析

跨页面共享元素动画实现的方法其实并非真的操作 DOM 移动,而是先把需要移动的页面或者元素存为图片,然后再通过对图片添加动画实现跨页面“共享元素”动画。未来还会更新加强版:computed style + 图片,可以更精细地控制 CSS 样式而不是只操作一张图片。也正因为动的是截图,整个画面在过渡时不会响应任何点击事件,一定程度上减少了极限操作造成的 bug。

值得注意的是,基于跨页面过渡的原理,我们可以推测,在动画执行的时候,目标页面其实已经渲染好了(才会有目标图片),所以使用跨页面过渡动画有一个缺点,那就是有一定延迟。

<top-layer>
<container(root)>
<image-wrapper(root)>
<outgoing-image(root) />
<incoming-image(root) />
</image-wrapper(root)>
</container(root)>
</top-layer>

可以想象过渡过程中,在画面顶层会有这么一组元素覆盖着原本的元素,所以你无法点击原来画面上的元素。默认情况下 outgoing-imageincoming-image 的过渡是一个淡入淡出动画,我们可以通过下面这些 CSS 选择器控制这两个图片的动画:

container(root) - ::page-transition-container(root)
image-wrapper(root) - ::page-transition-image-wrapper(root)
outgoing-image(root) - ::page-transition-outgoing-image(root)
incoming-image(root) - ::page-transition-incoming-image(root)

例如把动画延长到 5 秒:

::page-transition-outgoing-image(root),
::page-transition-incoming-image(root) {
animation-duration: 5s;
}

再例如把淡入淡出改为其他动画:

@keyframes slide-to-left {
to {
transform: translateX(-100%);
}
}
@keyframes slide-from-right {
from {
transform: translateX(100%);
}
}
::page-transition-outgoing-image(root) {
animation: 500ms ease-out both slide-to-left;
}
::page-transition-incoming-image(root) {
animation: 500ms ease-out both slide-from-right;
}

多元素参与过渡

上面的代码仅仅是对整个页面(root)添加过渡动画,然而更常见的需求肯定是对某个元素的过渡。为此我们需要把某个元素指定为过渡使用元素:

.site-header {
page-transition-tag: side-header;
/* Paint containment is required */
contain: paint;
}

P.S. break-inside: avoid;contain: paint; 是实现跨页过渡的重要属性

设置后,过渡用的“图片”结构会像这样:

<top-layer>
<container(root)>
<image-wrapper(root)>
<outgoing-image(root) />
<incoming-image(root) />
</image-wrapper(root)>
</container(root)>
<container(site-header)>
<image-wrapper(site-header)>
<outgoing-image(site-header) />
<incoming-image(site-header) />
</image-wrapper(site-header)>
</container(site-header)>
</top-layer>

对过渡动画的操作与 root 别无二致,仅仅是把 root 换成对应的 tag 就可以了:::page-transition-outgoing-image(site-header)

调用过渡 API

仅配置 CSS 无法实现过渡,还需要调用 document.createDocumentTransition 告诉浏览器执行过渡操作:

async function spaNavigate(data) {
// Fallback
if (!document.createDocumentTransition) {
await updateTheDOMSomehow(data);
return;
}
// With a transition
const transition = document.createDocumentTransition();
await transition.start(async () => {
// Once this callback has called, the browser has captured the page similar to a screenshot.
// This screenshot is now being displayed rather than the real DOM.
// Any animated content on the page (e.g. CSS animations, videos, GIFs) will now appear frozen.
await updateTheDOMSomehow();
// The DOM has now updated, but the user is still looking at the captured state.
// Once this async function returns, the transition will begin.
});
// The transition is now complete, and the captured state is removed to reveal
// the real DOM underneath.
}

这个过程步骤如下:

  • start 的 callback 运行时,浏览器已经获取当前截图,并且整个页面被截图覆盖,所以从用户角度看,整个画面都是静止无响应的
  • 在 callback 中对 DOM 进行操作,因为当前画面仍是截图,所以即使 DOM 操作完成,用户也依然看到旧画面
  • callback 运行结束后,拿到新的页面截图,过渡才正式开始
  • await start 后,过渡动画完成,移除过渡用的截图,新页面可操作

还可以通过修改 el.style.pageTransitionTag 临时调整过渡方式:

async function animate(direction) {
// Fallback
if (!document.createDocumentTransition) {
await mutate();
return;
}
// With a transition
const transition = document.createDocumentTransition();
document.querySelector("#title").style.pageTransitionTag = "site-title-" + direction;
await transition.start(() => mutate());
document.querySelector("#title").style.pageTransitionTag = "";
console.log("Transition complete!");
}

多页面过渡

上述操作仅限单页面应用,虽然这个 API 的目标是使多页面应用也能进行共享元素过渡,但是浏览器暂时未支持,只能通过代码来预习一下,基本概念不变,跨页面的重点是在页面隐藏和新页面展示前对元素进行操作:

document.addEventListener("pagehide", (event) => {
if (!event.transition) return;
document.getElementById("foo").style.pageTransitionTag = "foo";
event.transition.setData({ … });
});
document.addEventListener("beforepageshow", async (event) => {
if (!event.transition) return;
document.querySelector(".header").style.pageTransitionTag = "header";
await event.transition.ready;
// The pseudo-elements are now accessible and can be animated:
document.documentElement.animate(keyframes, {
...animationOptions,
pseudoElement: "::page-transition-container(header)",
});
});

来试试

初步了解跨页面过渡接口和 CSS 配置后,应该可以理解这个简单的例子啦!大家赶紧打开 chrome://flags/#document-transition ,试试通过修改这个例子更深入理解共享元素动画吧!

https://codepen.io/ssshooter/pen/GRxjGXJ

评论组件加载中……