元素一旦绑定 transition
之后,会对下个 Event Loop 中变更的样式(如异步 hover 或通过 js 变更 style/class),给予过度效果。要想更细颗粒度定制动画,可为元素添加诸如 animation: keyframeName 1s
的样式,animation
在绑定时立即执行动画。
直接在 style 里修改动画不如绑定/解绑 class 方便
<button class='btn'> 按钮1 </button>
<div class='car'></div>
<style>
/* transition动画 */
.btn {
transition: all .5s; background: yellow
}
.btn:hover {
background: red
}
/* animation动画 */
.car {
height: 100px;
width: 100px;
background: green;
position: relative;
animation: carMove 1s;
animation-fill-mode: forwards;
}
@keyframes carMove {
0% { top: 10px }
20% { top: 200px }
100% { top: 250px }
}
</style>
两者有一些异同之处:
- transition 需在绑定后的下一个 Event Loop 中实现动画,而 animation 在当时立即执行,因此,transition 常用于反复执行的场合,如 hover、toggle 等
- animation 在绑定时候立即执行,执行完会默认恢复到原态,所以在 0% 帧和 100% 帧处可能有跳变,如果加上
animation-fill-mode: forwards
属性,会保持 100% 帧的,像 transition 动画那样。因此在串联执行多个 animation 动画时,常用 forwards 来保持连贯,避免每次都要“重头再来”。 - 两者都可在后续的 Event Loop 中修改样式,触发动画
动画在执行的时候,会真实的改动到 DOM 中的元素上,比如我们可以通过以下案例监测到 clientHeight 的变化
animation 在时间粒度上的操控更灵活,可以通过 % 来定义特定时间帧。我们先来看一个案例,其中 color 样式并未在 0% 帧定义,看看是如何过渡的
@keyframes test {
0% {
width: 50%
}
50% {
color: red
<!--从color:black(继承自元素样式) 开始过渡,过渡时间0-50%-->
}
100% {
width: 80%
<!--从width:50%开始过渡,过渡时间0%-100% -->
color: yellow
<!--从color:black->red->yellow-->
}
}
<!--综上:-->
<!--对于 width 属性,只有两帧过渡效果-->
<!--对于 color 属性,有三帧过渡效果(初始帧继承自元素样式)-->
所以,对于帧中样式的省略,满足下面的规则:
- 0% 帧会继承元素本身属性,这些继承的属性优先级低,可能被新定义的覆盖
- 如果用户未定义 0% 帧,则默认创建,其属性完全继承自元素
- 0% 帧以后的所有帧,如果没有某个特定样式,则跳过该帧计算过渡曲线。例如上述案例中,50% 的帧中没有 width 样式,则 width 的过渡曲线从 0% 到 100% 按照 ease 曲线过渡
animation-fill-mode 属性要定义在 animation 之后才有效。(或具有更高样式优先级)
我们在执行了一次 animation 动画之后,有时候想再重复执行一次,直接想到的做法是:
- 移除已执行的 animation
- 再插入该 animation
但是需要注意的是,以上 1、2 两个步骤不能在同一个 Event Loop 中完成,通常两步之间需要一小段间隔(如 setTimeout 或在 Vue 中的 nextTick 里执行)
<div class='container'>
<div class='car car-move'></div>
</div>
<style>
.car-move {
animation: moves 2s;
}
.car {
background:green;
height:100px;
width: 100px;
animation-fill-mode: forwards; /* 需在 animation 后 */
}
@keyframes moves {
/* 略 */
}
</style>
<script>
// DOMContentLoad 回调中执行
var car = document.querySelector('.car')
// 移除动画
setTimeout(()=>{
car.className = 'car'
}, 1)
// 1毫秒之后再添加
setTimeout(()=>{
car.className = 'car car-move'
}, 2)
</script>
这种方式可以实现,但更好的实践是操作DOM:
- 移除元素
- 重新插入
通过不超过两行代码即可完成,实现动画的重播:
var container = document.querySelector('.container')
// appendChild 操作会隐含做“摘除”的操作,相当于一行语句完成移除+插入
container.appendChild(container.children[0])
在 Vue
中,如果要执行DOM中元素的移除和插入,更方便快捷,直接用 v-if 控制即可。需要注意的是,无法在一个 Event Loop 中完成,可利用 nextTick 函数辅助完成
<div class='car car-move' v-if='switch'></div>
<script>
// ...
this.switch = false // 移除
this.$nextTick(() => {this.switch=true}) // 再插入
// ...
</script>
先看看效果:
这种动画效果应用场景广泛:
- 数据展示大屏
- 网页抽奖游戏
- 提升用户行为的反馈性
全部代码如下:
<div class='tiger'>
<div> 1 </div>
<div> 2 </div>
</div>
<style>
.tiger {
height: 3rem;
line-height: 3rem;
overflow: hidden;
}
.tiger div {
font-size: 2rem; font-weight: bold;
animation: scroll 1s;
animation-fill-mode: forwards
}
@keyframes scroll {
100% { transform: translateY(-100%) }
}
</style>
<script>
let tiger = document.querySelector('.tiger')
let numbers = tiger.children
tiger.onclick = function(){
let lastNumber = numbers[1].innerHTML;
numbers[0].innerHTML = lastNumber;
numbers[1].innerHTML = parseInt(lastNumber) + 1;
tiger.appendChild(numbers[0]) // 两个数字,所以要插入两次(隐式移除)
tiger.appendChild(numbers[0])
}
</script>
通过代码可以发现,数字“更新”的本质是:
- 两个卡片同时向上位移,溢出 hidden
- 数字有更新时候,需重复动画,重复的方式是从DOM中移除,再添加
- 再添加的DOM中,两个卡片内的数字已经更新过了,添加的瞬间触发新动画
requestAnimationFrame
可补充 CSS 样式无法触及的渐变,如渐变 scrollTop 实现页面的平滑滚动。本质上讲:
- 实现
fps:60
(帧时隙1000ms/60 = 16.7ms)的动画,以保证重绘时间和浏览器刷新频率一致(过频繁浪费算力,过缓慢则不流畅)。所以从它的兼容性定义看,很像是一个 16.7ms 的 setTimeout 函数,需要递归的执行下去。 - 通俗来讲,每 16.7ms,我们修改一次数值,如果把数值的修改反应到DOM中去,就能像幻灯片一样播放起来
// 兼容性的函数定义
function requestAnimationFrame(callback) {
setTimeout(callback, 16.7)
}
动画的实现是通过递归调用,串联起来,形成连续帧的效果
function animate(){
// ... 跳出条件 + 每一帧的动作
requestAnimationFrame(animate) // 递归
}
animate() // 执行动画
案例:数字递增动效
这个效果在钱包类 APP 中常见
非常简洁的实现:
// DOM中获取元素
let account = document.querySelector('.account')
let number = parseInt(account.innerHTML)
// 在动画中操作元素内容
animate();
function animate() {
// 递归跳出条件
if(that.num > 300) return
// 数字递增显示
number += 1
account.innerHTML = number
// 递归
requestAnimationFrame(animate);
}
案例:平滑返回顶端
scrollTop 属性无法用 CSS3 动画来操控,我们用 requestAnimationFrame 来一帧一帧的实现:
document.querySelector('.getBack').onclick = function(){
if (document.documentElement.scrollTop > 0) {
document.documentElement.scrollTop -= 20
requestAnimationFrame (scrollBack)
}
}
在平滑滚回顶端的案例中,每帧的步长是20px,是一个一次线性函数。而 css3 动画默认的 ease 曲线效果不错。Tween.js 可以辅助我们计算过渡曲线在每一帧的采样值,让 requestAnimationFrame
也可以用到丰富的过渡时间曲线(不限于简单粗暴的线性递增)
我们看看用纯 JS 实现动画的案例,了解一下其使用:
<!--<script src="//cdn.bootcss.com/tween.js/r14/Tween.min.js"></script>-->
<div class='box' style='height:50px;width:50px;background:red'></div>
<script>
let box = document.querySelector('.box')
let coord = { x: 50, y: 50 }
// 创建 tween 对象来操控 coord 对象,coord 将在一段时间内被高频更新
let tween = new TWEEN.Tween(coord)
tween.to({ x: 150, y:150 }, 2000)
tween.onUpdate(function() {
box.style.height = this.x + 'px'
box.style.width = this.y + 'px'
})
tween.easing(TWEEN.Easing.Quadratic.Out) // 采用 ease 渐变,可选
tween.start()
// 周期性触发 onUpdate 中的回调(修改元素长宽)
function animate() {
if(TWEEN.update()) // 触发更新 + 返回值判断跳出递归
requestAnimationFrame(animate);
}
animate();
</script>
Tween 有以下的特点(以上面案例讲解):
- 在渐变过程中,直接操作到 coord 的地址,其中的数据会被高频更新
- onUpdate 传入的回调函数,会被强制绑定执行者为 coord,执行时 this === coord,通过 this.x 和 coord.x 访问的是同一地址
- tween 成员函数 return this,因此可以串联,如 tween.to().onUpdate().start()
- 执行 start() 之后,coord 对象的数据就开始持续更新了,在对应的时刻调用 TWEEN.update() 才能触发 onUpdate 回调中的动作,比如去调整 DOM
可能有这样的疑问:案例中的效果可以用 transition 很方便实现,何苦写这么多代码? 没错,尽量用 css 动画,在很多 css 动画无法涉足的领域中可以灵活的用 js 操作数据做补充
Vue 的官方教程中也有 Tween.js 过渡的 案例。其根本原理在是,在 onUpdate 的回调中,把渐变的数据赋值给响应式变量,以触发 DOM 更新。
不要试图直接将 Vue 的响应式的对象作为 TWEEN 的参数