# 概述
Vue提供了一些抽像概念,可以帮助用户进行过渡和动画处理,特别是在响应一些变化时。这些抽像包括:
* 组件进入或退出DOM的勾子,同时在JS和CSS中,使用内置`<transition>`组件。
* 你可以设置转场模式来编排过渡的次序。
* 多个组件位置改变时的勾子,应用翻转技术提升性能,使用`<transition-group>`组件。
* 应用不同状态的过渡效果,使用`watchers`。
手册接下来三章我们将覆盖所有这些内容。但是除了提供这些有用的API之外,值得提到的是我们前面介绍的`class`和`style`可以对动画和过渡有更好的效果,更多的应用简单使用实例。
接下来章节,我们将回顾一些基本的WEB动画和过渡效果,将提供一些资源做更多的研究。如果你已经熟悉一些WEB动化以及一些与Vue指令一起使用的规范,可以跳过下一章节。其他情况还是乖乖的学一些WEB动画的基础知识吧。
## 依赖于Class的动画和转场
虽然`<trasition>`对于组件的进入和离开过渡已经比较完美了,你仍然可以激活动画而不需要移动组件,只需要添加额外的class:
~~~html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>class-based Transition&Animation</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
点击下面的按钮演示动画及转场效果!
<br />
<div :class={shake:shakeFlag}>
<button @click="shakeFlag = true">点我试试</button>
<span v-if="shakeFlag">动起来了啦!</span>
</div>
</div>
</body>
<script type="text/javascript">
const Counter = {
data() {
return {
shakeFlag: false
}
}
}
Vue.createApp(Counter).mount("#app")
</script>
<style type="text/css">
.shake {animation: shake 0.82s cubic-bezier(0.36,0.07,0.19,0.97) both;transform:translate3d(0,0,0);perspective: 1000px;}
@keyframes shake{
10%,90% {transform: translate3d(-1px,0,0);background-color: aqua;}
20%,80% {transform: translate3d(2px,0,0);background-color: blueviolet;}
30%,50%,70% {transform: translate3d(-4px,0,0);background-color: brown;}
40%,60% {transform: translate3d(4px,2px,2px);background-color: cornflowerblue;}
}
</style>
</html>
~~~
>建议了解下animation的关键帧概念。以及transform(变形)的属性translate(移动)。
## 使用style绑定的过渡
有一些过渡效果还可以使用插值,实例中使用`style`绑定到一个元素,然后就会出现效果。看看下面这个例子:
~~~html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>style-based Transition&Animation</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<div
@mousemove="xCoordinate"
:style="{backgroundColor: `hsl(${x},80%,50%)`}"
class="movearea"
>
<h3>鼠标移动经过这片区域</h3>
<p>x: {{x}}</p>
</div>
</div>
</body>
<script type="text/javascript">
const Counter = {
data() {
return {
x: 0
}
},
methods: {
xCoordinate(e) {
this.x = e.clientX
}
}
}
Vue.createApp(Counter).mount("#app")
</script>
<style type="text/css">
#app {width:100%;height: 100vh;}
.movearea {position: absolute;top:0;left:0;width:100vw;height: 100vh;padding:6vmin;transition: 0.2s background-color:ease;}
</style>
</html>
~~~
在这个例子中,我们根据鼠标移动的位置,使用插值创建了一个动画。css过渡效果应用到了元素上,当界面更新时,让元素知道使用什么类型的easing。(ease是一种过渡效果,类似渐进或渐出的效果吧?)
## 性能
你可能注意到了上面这个动画使用了如`transform`,且应用了奇怪的属性`prespective` - 为什么用这种构建方式来代替仅仅使用`margin`和`top`等?
我们可以创建一个相当平滑的动画会不出现性能问题。我们尽一切可能想要硬件加速元素,和使用属性不触发重新绘制。让我们仔细想想怎么来完成这件事情:
### **transform(变形) 和 opacity(透明度)**
我们可以检查一下资源如[CSS-Triggers]()来看看哪些元素在应用动画时会重绘。这里,如果你继续往下看`transform`,你会看到:
>更改transform不会触发任何几何变化和重绘,这非常棒。
这意味着操作可以像排版进程一样通过GPU来帮助完成。
opacity(透明度)也是一样。因此这可以完美的移植到WEB中。
### **硬件加速**
属性如`prespective`,`backface-visibility`,`transform:translateZ(x)`会让浏览器明白你需要使用硬件加速。
如果你想对一个元素使用硬件加速,你可以应用这些任何属性(不是所有都是必须的,只有一个):
~~~css
perspective: 1000px;
backface-visibility: hidden;
transform: translateZ(0);
~~~
有一些JS类库如GreenSock会认为你想要硬件加速而默认开启,所以不需要再人工设置。
## Timling(时间)
简单UI过渡,意思是从一个场景转到另一个场景而不需要中间过度的场景,通用使用的时间是0.1s-0.4s,然后大伙大部分人认为0.25s是一个最佳时间点。那你就可以把所有事情都设置为这个时间点吗?不,这不可以!如果你需要移动一个元素到一个很远的距离或有很多步骤或场景更新,0.25秒将不会是一个很好的选择,而需要增加时间。这也不意味着你不可以在应用中设置一个使用的默认值。
你可能还发现进入动作时间长点看起来比出场更加好一点。用户一般在引导动画时比退出更有耐心,因为他们都想尽快看到下一个动作。
## Easing
Easing是描述动画深度的一个重要方式。新手常犯的一个错误是在进入时使用`ease-in`,离开时使用`ease-out`,实际上是反过来的。
如果我们为一个元素应用这些状态,看起来会像:
~~~css
.button {
background: #1b8f5a;
/* 应用于初始状态, 因此这个变化会应用于返回状态 */
transition: background 0.25s ease-in;
}
.button:hover {
background: #3eaf7c;
/* 应用于鼠标经过状态, 所以这个状态被应用于鼠标经过触发 */
transition: background 0.35s ease-out;
}
~~~
Easing也可以指定动画材质的质量。看下面的例子,你觉得哪个小球的动作僵硬哪个柔顺一点呢?
>这里用到了一个GreenSock的JS类库,CDN地址为:https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.0/gsap.min.js
~~~html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Easing Transition&Animation</title>
</head>
<body>
<div class='container'>
<div class='unit'>
<div class='ball ball1'></div>
<div class='ball-shadow'></div>
</div>
<div class='unit'>
<div class='ball ball2'></div>
<div class='ball-shadow'></div>
</div>
</div>
<div class='overlay'></div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.0/gsap.min.js"></script>
<script type="text/javascript">
const ball1 = document.querySelector('.ball1')
const ball2 = document.querySelector('.ball2')
gsap.from(ball1, {
duration: 0.8,
y: 220,
repeat: -1,
yoyo: true,
ease: Power4.easeOut
});
gsap.from(ball2, {
duration: 0.8,
y: 225,
repeat: -1,
yoyo: true,
ease: Circ.easeOut
});
gsap.fromTo(ball2, {
duration: 0.8,
scaleY: 1
}, {
scaleY: 1.1,
repeat: -1,
yoyo: true,
ease: Circ.easeOut
});
</script>
<style type="text/css">
html {
background: #222;
}
.container {
width: 500px;
margin: 10px auto;
border: 1px solid #333;
height: 280px;
background: #111;
overflow: hidden;
}
.unit {
float: left;
width: 249px;
height: 280px;
background-color: #222426;
*zoom: 1;
filter: progid:DXImageTransform.Microsoft.gradient(gradientType=1, startColorstr='#FF222426', endColorstr='#FF111111');
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjUwJSIgY3k9IiIgcj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzIyMjQyNiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzExMTExMSIvPjwvcmFkaWFsR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
background-size: 100%;
background-image: -moz-radial-gradient(center, ellipse cover, #222426 0%, #111111 100%);
background-image: -webkit-radial-gradient(center, ellipse cover, #222426 0%, #111111 100%);
background-image: radial-gradient(ellipse cover at center, #222426 0%, #111111 100%);
}
.unit:first-child {
border-right: 1px solid #333;
}
.overlay {
position: absolute;
background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/grain.png");
top: 10px;
width: 500px;
height: 280px;
left: 50%;
margin-left: -250px;
z-index: 1000;
opacity: 0.11;
animation: filmgrain 0.4s steps(3) infinite;
}
.ball {
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #959595;
*zoom: 1;
filter: progid:DXImageTransform.Microsoft.gradient(gradientType=1, startColorstr='#FF959595', endColorstr='#FF494949');
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjUwJSIgY3k9IiIgcj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzk1OTU5NSIvPjxzdG9wIG9mZnNldD0iNzklIiBzdG9wLWNvbG9yPSIjNGU0ZTRlIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNDk0OTQ5Ii8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g');
background-size: 100%;
background-image: -moz-radial-gradient(center, ellipse cover, #959595 0%, #4e4e4e 79%, #494949 100%);
background-image: -webkit-radial-gradient(center, ellipse cover, #959595 0%, #4e4e4e 79%, #494949 100%);
background-image: radial-gradient(ellipse cover at center, #959595 0%, #4e4e4e 79%, #494949 100%);
transform: translateZ(0);
margin: 30px auto;
position: relative;
z-index: 300;
}
.ball-shadow {
position: absolute;
width: 50px;
height: 5px;
border-radius: 50%;
background: #000;
top: 280px;
margin-left: 100px;
z-index: 2;
opacity: 0;
box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.2);
animation: boom 1.6s 0.7s ease-in-out infinite;
}
@keyframes boom {
50% {
opacity: 0.8;
}
}
@keyframes filmgrain {
100% {
background-position: 200% 0%;
}
}
</style>
</html>
~~~
通过调整easing可以使你的动画获得很多独特的效果,并显的比较时尚。CSS允许你通过调整贝塞尔曲线来修改,Lea Verou的[小游戏](https://cubic-bezier.com/#.17,.67,.83,.67)对解释这个规则很有帮助。
虽然你可以通过贝塞尔曲线两个控制点来完成一些简单动画好的效果,JavaScript允许有多个控制点,因此可以有更多的变化幅度。

以弹跳球为例子,CSS中定义了每一个keyframe,上和下。JavaScript中我们可以使用ease解释所有这些动作,通过在[GreenSock API(GSAP)](https://greensock.com/)中定义`bounce`(其他JS类库有其他类型的默认easing)
这里是CSS中使用`bounce`的示例(animation.css中的示例):
~~~css
@keyframes bounceInDown {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, -3000px, 0) scaleY(3);
}
60% {
opacity: 1;
transform: translate3d(0, 25px, 0) scaleY(0.9);
}
75% {
transform: translate3d(0, -10px, 0) scaleY(0.95);
}
90% {
transform: translate3d(0, 5px, 0) scaleY(0.985);
}
to {
transform: translate3d(0, 0, 0);
}
}
.bounceInDown {
animation-name: bounceInDown;
}
~~~
而这里是GreenSock中通过JS实现一样的弹跳效果:
~~~js
gsap.from(element, { duration: 1, ease: 'bounce.out', y: -500 })
~~~
后面的章节,我们会在示例中使用GreenSock。它有一个非常好用的[ease visualizer](https://greensock.com/ease-visualizer)可以帮助你制作优秀的ease。
更多阅读:
* [动画接口设计:通过Val Head动画完善用户体验](https://www.amazon.com/dp/B01J4NKSZA/)
* [Rachel Nabros动画](https://abookapart.com/products/animation-at-work)