讲解 Web 转场动画 View Transitions API

3,162 阅读9分钟

🥳 请在谷歌浏览器(v118+) PC端 访问,体验 View Transitions API 神奇之处~
🥳 单击切换 [Dark / Light] 主题色
🥳 单击查看 [Vue Library] 卡片


系列

  1. 讲解 CSS 过渡和动画 transition/animation
  2. 讲解 JavaScript 动画 Web Animations API
  3. 讲解 Web 转场动画 View Transitions API (本篇)

前言

近期在梳理和学习 Web 动画的时候,偶然间发现一个神奇又实用的 API (即 View Transitions API),直译为 视图转换,不过我更喜欢叫它 转场动画,其含义和作用不言而喻。

当我将 Web 动画 分类时,很快发现 View Transitions 并不能简单归属于 CSS 动画 还是 JS 动画。它可以实现不同场景的转场过渡 (比如 页面跳转间渐入渐出动画,又或是 图片预览时逐渐移动并放大),但其动画帧的执行,既可以通过 CSS Animation 属性实现,也可以通过 JS Element.animate 完成。

Web 动画适用场景如下

  • CSS 动画

    • CSS Transition:
      元素外观样式或位置在一段时间内发生的变化进行平滑过渡

    • CSS Animation:
      元素外观样式或位置在一段时间内往复循环变化,又或是对动画有控制能力的需求 (如暂停动画、恢复动画)

  • JS 动画

    • setTimeout / setInterval / requestAnimationFrame
      动画过程中,在某个特定动画帧需要与 JS 交互的简单逐帧动画,则可以使用上述 API 实现。不过相比 setTimeoutsetInterval,更推荐 requestAnimationFrame API 实现。

    • Web Animations API
      可以视为 CSS Animation 的 JavaScript 版本,可以很容易实现 CSS TransitionCSS Animation 的动画需求,且在动画执行的前后可以进行 JS 交互,同时也具备控制动画的能力。比起 CSS 动画,它更具有 JS 可编程特性。

  • 转场动画

    • View Transitions API
      适用于不同场景之间的转场过渡。不过相比之前动画,前者更多是对某 Dom 元素进行动画帧绘制,而它则不局限于同一 Dom 元素 (即使是不同的 Dom 元素,也可以通过 view-transition-name 属性关联动画) 实现转场过渡。

转场动画 View Transitions

概述/简介

View Transitions 是一个新的 Web API 提案,允许在单页面应用(SPA) 和多页面应用(MPA) 中使用一组简单的过渡动画,其目的是支持类似于 Android Activity Transitions 完成不同场景间转场过渡。从而帮助用户在导航时更好地理解页面 A 和页面 B 之间的关系,同时降低认知负荷,通过提供令人愉悦的内容和动画来减少加载/渲染的感知延迟。

原理/流程

要理解 View Transitions API 转场动画的原理,则需要从它与传统动画的区别去理解:

  • 传统动画
    如果用传统动画的方式去实现不同场景的转场过渡,则不可避免的需要有一个承载不同场景的容器。而后对这个容器作为一个普通的 Dom 元素 定义 Animation 动画,进而模拟了不同场景的转场过渡。如 Vue Transition 组件:

      <template>
        <button @click="show = !show">
          <span>Toggle</span>
        </button> 
        <Transition>
          <p v-if="show">hello world</p>
        </Transition>
      </template>
     
      <script setup>
      import { ref } from "vue";
      const show = ref(false)
      </script>
      
      <style lange="less">
      .v-enter-active, 
      .v-leave-active {
         transition: opacity 0.5s ease; 
      } 
    
      .v-enter-from, 
      .v-leave-to { 
         opacity: 0; 
      }
      </style>
    
    
  • View Transitions
    相比传统动画定义承载不同场景父容器的方式,则是通过其 API 调用,让浏览器为新旧两种不同视图分别捕获并建立了快照 (即 ::view-transition-old 旧快照::view-transition-new 新快照),而后新旧两快照在 ::view-transition-image-pair 容器中完成转场动画的过渡。动画结束后则删除其相关伪元素 (快照和容器)。整个伪元素结构如下:

      ::view-transition # 视图过渡根元素,包含所有视图过渡组,且位于其他页面内容的顶部
      │
      ├─ ::view-transition-group(root) # 默认视图过渡组 (root)
      │  └─ ::view-transition-image-pair(root) # 承载一个过渡中旧视图状态和新视图状态的容器
      │     ├─ ::view-transition-old(root) # 旧视图状态
      │     └─ ::view-transition-new(root) # 新视图状态
      │
      ├─ ::view-transition-group(container) # 自定义视图过渡组 (通过 view-transition-name 定义)
      │  └─ ::view-transition-image-pair(container)
      │     ├─ ::view-transition-old(container)
      │     └─ ::view-transition-new(container)
      │
      ├─ ...
      │
    
    

    View Transitions 动画的生命周期如下:

    1. 开发者通过 document.startViewTransition(updateCallback) 启动转场动画,其中 updateCallback 函数是用来更新 Dom 状态 (即更新为新视图状态)
    2. 捕获当前状态被为旧视图状态
    3. 暂停 Dom 树渲染
    4. 回调函数 updateCallback 被调,用来更新文档状态 (可以是异步函数,返回 Promise)
    5. 回调函数 updateCallback 成功后,viewTransition.updateCallbackDone 被满足 (即 promise is resolved)
    6. 恢复 Dom 树渲染,而后捕获当前状态被为新视图状态
    7. 创建过渡伪元素 (即 ::view-transition-old::view-transition-new ...等)
    8. 渲染未暂停,显示过渡伪元素
    9. viewTransition.ready 被满足 (即 promise is resolved)
    10. 伪元素开始动画,直至动画完成
    11. 删除了过渡伪元素
    12. viewTransition.finished 被满足 (即 promise is resolved)

    在 W3C 标准文档中,有一个步进式演示,可以帮我们更直观地了解生命周期的每个阶段。
    感兴趣的朋友,可以前往 Demo 查看

定义/使用

  • 相关 CSS 伪元素

    • ::view-transition
      表示一系列视图过渡组的根元素,它包含所有视图过渡,而且位于所有其他页面内容的顶部。在场景过渡期间,它包含在相关的伪元素树中,是该树的顶级节点,并且有一个或多个 ::view-transition-group 子节点。

      在 UA 样式表中具有以下默认样式:

        html::view-transition {
          position: fixed;
          inset: 0;
        }
        
      
    • ::view-transition-group
      表示单个视图过渡组。在场景过渡期间,它包含在相关的伪元素树上,是 ::view-transition 子节点,并且有一个 ::view-transition-image-pair 子节点。

      在 UA 样式表中具有以下默认样式:

        html::view-transition-group(*) {
          position: absolute;
          top: 0;
          left: 0;
      
          animation-duration: 0.25s;
          animation-fill-mode: both;
        }
        
        /* 语法说明 */
        /* ::view-transition-group(*) => 匹配所有视图过渡组 */
        /* ::view-transition-group(root) => UA 创建的默认视图过渡组,用于整个页面的视图过渡 */
        /* ::view-transition-group(<custom-ident>) => 匹配 `view-transition-name` 指定的视图过渡组 */
        
      

      默认情况下,该元素最初会镜像表示 ::view-transition-old 伪元素的大小和位置,即旧视图状态。如果没有旧视图状态,则会镜像表示 ::view-transition-new 伪元素的大小和位置,即新视图状态。

    • ::view-transition-image-pair
      表示一个场景过渡时旧视图状态和新视图状态的容器。在场景过渡期间,它包含在相关的伪元素树上,是 ::view-transition-group 的子节点。并且可以有一个 ::view-transition-new 或一个 ::view-transition-old 子节点,或是两者都有。

      在 UA 样式表中具有以下默认样式:

        html::view-transition-image-pair(*) {
          position: absolute;
          inset: 0;
      
          animation-duration: inherit;
          animation-fill-mode: inherit;
        }
        
        /* 语法说明 */
        /* ::view-transition-image-pair(*) => 匹配所有视图过渡组 */
        /* ::view-transition-image-pair(root) => UA 创建的默认视图过渡组,用于整个页面的视图过渡 */
        /* ::view-transition-image-pair(<custom-ident>) => 匹配 `view-transition-name` 指定的视图过渡组 */
        
      

      默认情况下,::view-transition-image-pair 在视图过渡样式表中设置了 isolation: isolate,以便其子元素可以使用非正常混合模式进行混合,而不会影响其他视觉输出。

    • ::view-transition-old
      表示视图过渡的旧视图状态,即过渡前旧视图的静态屏幕截图。在场景过渡期间,它包含在相关的伪元素树上,是 ::view-transition-image-pair 的子节点,并且它不会有任何子节点。

      在 UA 样式表中具有以下默认样式:

        @keyframes -ua-view-transition-fade-out {
          to {
            opacity: 0;
          }
        }
      
        html::view-transition-old(*) {
          position: absolute;
          inset-block-start: 0;
          inline-size: 100%;
          block-size: auto;
      
          animation-name: -ua-view-transition-fade-out;
          animation-duration: inherit;
          animation-fill-mode: inherit;
        }
        
        /* 语法说明 */
        /* ::view-transition-old(*) => 匹配所有视图过渡组 */
        /* ::view-transition-old(root) => UA 创建的默认视图过渡组,用于整个页面的视图过渡 */
        /* ::view-transition-old(<custom-ident>) => 匹配 `view-transition-name` 指定的视图过渡组 */
        
      
    • ::view-transition-new
      表示视图过渡的新视图状态,即过渡后新视图的实时表示。在场景过渡期间,它包含在相关的伪元素树上,是 ::view-transition-image-pair 的子节点,并且它不会有任何子节点。

      在 UA 样式表中具有以下默认样式:

        @keyframes -ua-view-transition-fade-in {
          from {
            opacity: 0;
          }
        }
      
        html::view-transition-new(*) {
          position: absolute;
          inset-block-start: 0;
          inline-size: 100%;
          block-size: auto;
      
          animation-name: -ua-view-transition-fade-in;
          animation-duration: inherit;
          animation-fill-mode: inherit;
        }
        
        /* 语法说明 */
        /* ::view-transition-new(*) => 匹配所有视图过渡组 */
        /* ::view-transition-new(root) => UA 创建的默认视图过渡组,用于整个页面的视图过渡 */
        /* ::view-transition-new(<custom-ident>) => 匹配 `view-transition-name` 指定的视图过渡组 */
        
      

    其他说明: 从上述伪元素的样式上看,转场动画默认是渐入渐出的效果。当然我们完全可以通过重写伪元素样式中的 animation 相关属性,实现我们自己想要的转场动画效果。

  • 相关 JavaScript API

    如之前 动画生命周期 所述,转场动画由 document.startViewTransition(update)  调用开始,并返回一个 ViewTransition 的实例,具有如下属性和方法:

    方法 - skipTransition 跳过场景动画过渡部分,但不会跳过回调函数 (即更新 DOM 树) 的执行
    属性 - updateCallbackDone 一个具有只读属性的 Promise,会在 update 回调函数兑现时兑现
    属性 - ready 一个具有只读属性的 Promise,会在伪元素树被创建且过渡动画即将开始时兑现
    属性 - finished 一个具有只读属性的 Promise,会在转场过渡动画完成时兑现

      /**
       * 开始新的转场动画
       */
      const transition = document.startViewTransition(() => {
        /* 更新 Dom 树 */
      });
        
        
      /**
       * 实例方法
       */
      transition.skipTransition(); // 跳过场景转场动画,仅更新 DOM 树
        
        
      /**
       * 实例属性
       */
      transition.updateCallbackDone.then(() => {}); // promise: DOM 树是否已更新
      transition.ready.then(() => {}); // promise: 转场动画是否准备就绪 
      transition.finished.then(() => {}); // promise: 转场动画是否已完成
          
    

代码范例讲解

以文章篇头 Usage — View Transition 为例,我们分别讲解范例中的两种转场动画方式

  • 切换 [Dark / Light] 主题色

    1. 取消 View Transition 默认转场动画

        ::view-transition-image-pair(root) {
          isolation: auto;
        }
      
        ::view-transition-old(root),
        ::view-transition-new(root) {
          mix-blend-mode: normal;
          animation: none; /* 取消默认动画 */
        }
        
      
    2. 通过 Element.animate 自定义转场动画 clipPath

        document.getElementById('change-theme').addEventListener('click', (event) => {
          const x = event.clientX;
          const y = event.clientY;
          const maxX = Math.max(x, innerWidth - x)
          const maxY = Math.max(y, innerWidth - y)
          const radius = Math.hypot(, Math.max(y, innerHeight - y));
          const transition = document.startViewTransition(() => { /* 更新 Dom 树 */ });
      
          transition.ready.then(() => {
            document.documentElement.animate(
              {
                clipPath: [
                  `circle(0 at ${x}px ${y}px)`,
                  `circle(${radius}px at ${x}px ${y}px)`,
                ]
              },
              {
                duration: 800,
                easing: 'ease-in',
                pseudoElement: '::view-transition-new(root)',
              }
            )
          })
        })
        
      
  • 单击查看 [Vue Library] 卡片

    1. Popup Card 通过 view-transition-name 定义视图过渡组

        .popup {
           display: none;
           width: 52%;
           min-width: 280px;
           max-width: 455px;
           padding: 20px 0 14px;
           margin: 20px auto 0;
           border-radius: 8px;
           position: relative;
           box-sizing: border-box;
           view-transition-name: popup-transition; /* 定义视图过渡组 */
           /* ... */
        }
        
      
    2. 修改 popup-transition 视图过渡组的动画时长

        ::view-transition-image-pair(popup-transition) {
          isolation: auto;
        }
      
        ::view-transition-old(popup-transition),
        ::view-transition-new(popup-transition) {
          animation-duration: 0.15s;
        }
        
      
    3. 选择卡片定义与 Popup Card 一致的视图过渡组,以完成 CardPopup Card 转场动画

        document.getElementById('content').addEventListener('click', (event) => {
          const $elem = event.target
          const $card = $elem.closest('.list-item') // 所选择的卡片
          
          // 遍历匹配所选择的卡片,并设置 viewTransitionName = 'popup-transition'
          document.querySelectorAll('.list-item').forEach(element => {
            element.style.viewTransitionName = $card === element 
              ? 'popup-transition' // 定义视图过渡组
              : null
          })
      
          const transition = document.startViewTransition(() => { 
            /* 更新 Dom 树 */
          })
        })
      

浏览器兼容性

从下图看来,目前 Chrome 浏览器和 Opera 浏览器也只支持 View Transitions API (SPA 单页面应用)

image.png

不过在 View Transition API 设计中,原先是有计划支持 MPA 多页面应用的打算 (详情)。MPA 转场动画的实现流程如下,感兴的趣朋友可以先看为敬:

mpa-chart.svg