乐趣区

关于前端:太丝滑了了解一下原生的视图转换动画-View-Transitions

欢送关注我的公众号:前端侦探

在原生 APP 中,咱们常常会看到那种丝滑又舒服的页面切换动画,比方这样的

Android 里个别称之为共享元素(shareElement)动画,也就是动画前后有一个(或多个)雷同的元素,起到前后过渡的成果,能够很分明的看到元素的变动过程,而并不是简略的隐没和呈现。

当初,web 中(chrome 111+)也迎来了这样一个个性,叫做视图转换动画 View Transitions,又称“转场动画”,也能很轻松的实现这类成果,一起理解一下吧

一、疾速意识 View Transitions

先从一个简略的例子来认识一下。

比方,上面有一个网格列表

<div class="list" id="list">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
  <div class="item">5</div>
  <div class="item">6</div>
  <div class="item">7</div>
  <div class="item">8</div>
  <div class="item">9</div>
  <div class="item">10</div>
</div>

简略润饰后如下

而后咱们实现一个简略交互,点击每个元素,元素就会被删除

list.addEventListener('click', function(ev){if (ev.target.className === 'item') {ev.target.remove()
  }
})

能够失去这样的成果

性能失常,就是有点太过僵硬了

当初轮到 View Transitions 出场了!咱们只须要在扭转状态的中央增加document.startViewTransition,如下

list.addEventListener('click', function(ev){if (ev.target.className === 'item') {document.startViewTransition(() => { // 开始视图变换
      ev.target.remove()});
  }
})

当然为了兼容不反对的浏览器,能够做一下判断

list.addEventListener('click', function(ev){if (document.startViewTransition) { // 如果反对就视图变换
    document.startViewTransition(() => { // 开始视图变换
      ev.target.remove()});
  } else { // 不反对就执行原来的逻辑
    ev.target.remove()}
})

当初成果如下

删除前后当初有一个淡入淡出的成果了,也就是默认的动画成果,咱们能够把这个动画时长设置大一点,如下

::view-transition-old(root), /* 旧视图 */
::view-transition-new(root) { /* 新视图 */
  animation-duration: 2s;
}

这两个伪元素咱们前面再做介绍,先看成果

是不是显著感觉过渡变慢了许多?

然而这种动画还是不够难受,是一种整体的变动,看不出删除前后元素的地位变动。

接下来咱们给每个元素指定一个标识,用来标记变动前后的状态,为了不便管制,能够借助 CSS 变量

<div class="list" id="list">
  <div class="item" style="--i: a1">1</div>
  <div class="item" style="--i: a2">2</div>
  <div class="item" style="--i: a3">3</div>
  <div class="item" style="--i: a4">4</div>
  <div class="item" style="--i: a5">5</div>
  <div class="item" style="--i: a6">6</div>
  <div class="item" style="--i: a7">7</div>
  <div class="item" style="--i: a8">8</div>
  <div class="item" style="--i: a9">9</div>
  <div class="item" style="--i: a10">10</div>
</div>

这里通过 view-transition-name 来设置名称

.item{view-transition-name: var(--i);
}

而后能够失去这样的成果,每个元素在变动前后会主动找到之前的地位,并且平滑的挪动过来,如下

残缺代码能够查看

  • view-transition sort (codepen.io)’)

是不是十分丝滑?这就是 View Transitions 的魅力!

二、View Transitions 的外围概念

为啥仅仅加了一点点代码就是实现了如此顺畅的动画呢?为啥浏览器能够晓得前后的元素地位关系呢?这里简略介绍一下变动原理。

整个 JS 局部只有一行外围代码,也就是document.startViewTransition,示意开始视图变换

document.startViewTransition(() => {// 变动操作});

整个过程包含 3 局部

  1. 调用document.startViewTransition,浏览器会捕获以后页面的状态,相似于实时截图,或者“快照”
  2. 执行理论的 dom 变动,再次记录变动后的页面状态(截图)
  3. 触发两者的过渡动画,包含透明度、位移等变动,也能够自定义 CSS 动画

上面是一个示意图

在动画执行的过程中,还会在页面根节点主动创立以下伪元素

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

上面是控制台的截图

其中,::view-transition-old示意 旧视图 的状态,也就是变动之前的截图,::view-transition-new示意 新视图 的状态,也就是变动之后的截图。

默认状况下,整个页面 root 都会作为一个状态,也就是下面的::view-transition-group(root),在切换前后会执行淡入淡出动画,如下

:root::view-transition-new(root) {animation-name: -ua-view-transition-fade-in; /* 淡入动画 */}
:root::view-transition-old(root) {animation-name: -ua-view-transition-fade-out; /* 淡出动画 */}

这也是为什么在应用了 document.startViewTransition 后整个页面会有淡入淡出的成果了

为了让每个元素都有本人的过渡状态,这里须要给每个元素都指定名称

.item{view-transition-name: item-1;}

这样指定名称后,每个名称都会创立一个::view-transition-group,示意独立的分组

这样在变动前后 view-transition-name 雷同的局部就会一一主动执行过渡动画了,以第 4、5 个元素为例(在 3 删除当前)

最初就会失去这样的成果

外围概念就这些了,上面再来看几个例子

三、不同元素之间的过渡

视图变动其实和元素是否雷同没有关联,有关联的只有 view-transition-name,浏览器是依据view-transition-name 寻找的,也就是雷同名称的元素在前后会有一个过渡动画。

比方上面这样一个例子

每个按钮在关上弹窗时,都能够分明的看到弹窗是从哪里呈现的,如何实现这样的成果呢?

从实质上看,其实就是 按钮到弹窗的视图变换 ,依照后面讲到的,可能会想到给前后加上雷同的view-transition-name,上面是HTML 构造

<div class="bnt-group" id="group">
  <button> 按钮 1 </button>
  <button> 按钮 2 </button>
  <button> 按钮 3 </button>
</div>
<dialog id="dialog">
  <form method="dialog">
    我是弹窗
    <button> 敞开 </button>
  </form>
</dialog>

尝试一下

button,dialog{view-transition-name: dialog;}

而后增加点击关上弹窗事件

group.addEventListener('click', function(ev){if (ev.target.tagName === 'BUTTON') {if (document.startViewTransition) {document.startViewTransition(() => {dialog.showModal()
      });
    } else {dialog.showModal()
    }
  }
})

这样会有什么问题吗?运行如下

很显著报错了,意思就是一个页面中不能有雷同的 view-transition-name。严格来讲,是 不能同时呈现,如果其余元素都是暗藏的,只有一个是显示的,也没有问题。其实认真想一下,也很好了解,如果同时有两个雷同的名称,并且都可见,最初变换的时候该以哪一个为准呢?

所以,在这种状况下,正确的做法应该是动静设置view-transition-name,比方默认不给按钮增加名称,只有在点击的时候才增加,而后在变换完结之后再移除按钮的view-transition-name,实现如下

group.addEventListener('click', function(ev){if (ev.target.tagName === 'BUTTON') {
    ev.target.style.viewTransitionName = 'dialog' // 动静增加 viewTransitionName
    if (document.startViewTransition) {document.startViewTransition(() => {
        ev.target.style.viewTransitionName = '' // 完结后移除 viewTransitionName
        dialog.showModal()});
    } else {dialog.showModal()
    }
  }
})

这样就实现了动静缩放的成果

大抵曾经实现想要的成果,不过还有一个小问题,咱们把速度加快一点(把动画时长设置长一点)

能够分明的看到,本来的按钮先放大到了弹窗大小,而后逐步隐没。这个过程是咱们不须要的,有没有方法去掉呢?

当然也是能够的!本来的按钮其实就是旧视图,也就是点击之前的截图,咱们只须要将这个视图暗藏起来就行了

::view-transition-old(dialog) {display: none;}

这样就完满实现了从哪里点击就从哪里关上的成果

残缺代码能够查看:

  • view-transition-dialog (codepen.io)’)

四、自定义过渡动画

通过后面的例子能够看出,默认状况下,视图转换动画是一种淡入淡出的动画,而后还有如果地位、大小不同,也会平滑过渡。

除此之外,咱们还能够手动指定过渡动画。比方上面这个例子

这是一个光明模式的繁难模型,实现也非常简单,筹备两套主题,这里用 color-scheme 实现

无关 color-scheme 的更多详情能够参考这篇文章:CSS color-scheme 和夜间模式

.dark{color-scheme: dark;}

而后通过点击动静给 html 切换 dark 类名

btn.addEventListener('click', function(ev){document.documentElement.classList.toggle('dark')
})

这样就失去了主题切换成果

接着,咱们增加视图转换动画

btn.addEventListener('click', function(ev){if (document.startViewTransition) {document.startViewTransition(() => {document.documentElement.classList.toggle('dark')
    });
  } else {document.documentElement.classList.toggle('dark')
  }
})

这样就失去了一个默认的淡入淡出的切换成果(为了不便察看,将动画时长缩短了)

你能够把前后变动设想成是两张截图的变动,如果要实现点击呈现圆形裁剪动画,其实就是在新视图上执行一个裁剪动画,因为是齐全重叠的,所以看着像是一种穿透扩散的成果

动画很简略,就是一个 clip-path 动画

@keyframes clip {
  from {clip-path: circle(0%);
  }
  to{clip-path: circle(100%);
  }
}

咱们把这个动画放在 ::view-transition-new

::view-transition-new(root) {
  /* mix-blend-mode: normal; */
  animation: clip .5s ease-in;
  /* animation-duration: 2s; */
}

成果如下

是不是还有点奇怪?这是因为默认的一些款式导致,包含原有的淡出成果,还有混合模式

所以还须要去除这些影响

::view-transition-old(root) {animation: none;}
::view-transition-new(root) {
  mix-blend-mode: normal;
  animation: clip .5s ease-in;
}

当然你能够把鼠标点击的地位传递到页面根节点

btn.addEventListener('click', function(ev){document.documentElement.style.setProperty('--x', ev.clientX + 'px')
  document.documentElement.style.setProperty('--y', ev.clientY + 'px')
  if (document.startViewTransition) {document.startViewTransition(() => {document.documentElement.classList.toggle('dark')
    });
  } else {document.documentElement.classList.toggle('dark')
  }
})

动画里间接通过 CSS 变量获取

@keyframes clip {
  from {clip-path: circle(0% at var(--x) var(--y));
  }
  to{clip-path: circle(100% at var(--x) var(--y));
  }
}

这样就实现了完满的扩散切换成果

残缺代码能够查看:

  • view transition theme change (codepen.io)’)

五、其余案例

找了几个乏味的例子

只有波及到前后过渡变动的,都能够思考用这个个性,例如上面的拖拽排序

https://codepen.io/argyleink/pen/rNQZbLr

再比方这样一个数字过渡动画

https://codepen.io/argyleink/pen/jOQKdeW

还有相似于 APP 的转场动画(多页面跳转)

https://simple-set-demos.glitch.me/6-expanding-image/

五、总结和阐明

总的来说,原生的视图转换动画能够很轻松的实现两种状态的过渡,让 web 也能实现媲美原生 APP 的动画体验,上面再来回顾一下整个变动过程:

  1. 调用document.startViewTransition,浏览器会捕获以后页面的状态,相似于实时截图,或者“快照”
  2. 执行理论的 dom 变动,再次记录变动后的页面状态(截图)
  3. 触发两者的过渡动画,包含透明度、位移等变动,也能够自定义 CSS 动画
  4. 默认状况下是整个页面的淡入淡出变动
  5. ::view-transition-old示意 旧视图 的状态,也就是变动之前的截图,::view-transition-new示意 新视图 的状态,也就是变动之后的截图。
  6. 如果须要指定具体元素的变动,能够给该元素指定view-transition-name
  7. 前后变动不肯定要同一元素,浏览器是依据 view-transition-name 寻找的
  8. 同一时间页面上不能呈现两个雷同 view-transition-name 的元素,不然视图变动会生效

另外,视图转换动画应该作为一种 体验加强 的性能,而非必要性能,在应用动画时其实拖慢了页面关上或者更新的速度,并且在动画过程中,页面是齐全“解冻”的,做不了任何事件,因而须要掂量好动画的工夫,如果页面自身就很慢就更不要应用这些动画了。最初,如果感觉还不错,对你有帮忙的话,欢送点赞、珍藏、转发❤❤❤

欢送关注我的公众号:前端侦探

退出移动版