- 本文主要参考 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-image 和 incoming-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 ,试试通过修改这个例子更深入理解共享元素动画吧!
