一般在开发的过程中,很多时候都会有动画夹杂里面,不管是交互上还是体验,对使用者来说都是不错的,但是当我们真正的去写动画的时候又发现并不是那么简单的,涉及到的css 和 js 的计算等一些处理包括边界条件的处理都是比较麻烦,做出来了很多时候也会不尽如人意。有没有简单的不需要很复杂的实现动画的效果却又不是很麻烦的呢?可以借鉴使用一下现在很流行,至少前端也有必要去了解的动画 Flip 模式。
什么是Flip动画
其实可以拆解出来说,四个单词四个意思。
- F:表示
First
一个动画元素的最开始的位置。 - L:表示
Last
一个动画元素的最后结束的位置。 - I:表示
Invert
一个元素前后位置的翻转,或者可以说是前后位置的交换。 - P:表示
Play
元素最后完成的过渡动画执行,换句话说,当元素交换之后,两个位置的坐标差值进行transform
过渡。
简单的四个步骤,思路简单明了,换成代码就是下面的;
// 获取元素开始的位置
var first = el.getBoundingClientRect();
// 中间元素有位置变化的操作
// ...
// 获取元素变化完成后的位置
var last = el.getBoundingClientRect();
// 两个坐标位置差值
var invert = first.top - last.top;
// 趁浏览器还没来的及渲染,设置其位移距离
el.style.transform = `translateY(${invert}px)`;
// 然后再执行动画
requestAnimationFrame(function() {
el.style.transform = '';
});
如果还是没思路的话,可以接下来看几个小栗子? 来直观的感受下flip动画的魅力。
随机的滑块
做一个随机运动的滑块效果,在一个区域内,通过点击操作使滑块随机位置摆放。可以看一下简单的代码;
<div class="content">
<div class="box"></div>
</div>
<script>
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
function btnClick() {
const box = document.querySelector('.box')
const top = getRandomInt(0, 350)
const left = getRandomInt(0, 350)
box.style.top = `${top}px`
box.style.left = `${left}px`
}
</script>
样式代码太简单了就不写,可以看到如下的界面点击效果;
看起来还不错,至少可以随机的改变位置,但是变化的时候有点生硬不自然,因为这里的动画啥都没写,如果要加上强行加上 transition
过渡也不是不行,但是这次说的不是这个 css
就能处理的问题,可以用 Flip
的思路,通过js写一个动画效果。
可以稍微的改一下上面的点击函数;
function btnClick() {
const box = document.querySelector('.box')
// 获取元素的起始位置
const rect1 = box.getBoundingClientRect()
const top = getRandomInt(0, 350)
const left = getRandomInt(0, 350)
box.style.top = `${top}px`
box.style.left = `${left}px`
// 获取元素结束后的位置
const rect2 = box.getBoundingClientRect()
// 计算两个坐标位置差值
const diffLeft = rect1.left - rect2.left
const diffTop = rect1.top - rect2.top
// 趁浏览器还没来的及渲染,设置其位移距离
box.style.transform = `translate(${diffLeft}px, ${diffTop}px)`
// 执行动画
requestAnimationFrame(update)
const start = new Date().getTime()
function update() {
let time = (new Date().getTime() - start) / 300
time = Math.min(1, Math.max(0, time))
const l = diffLeft * (1 - time)
const t = diffTop * (1 - time)
box.style.transform = `translate(${l}px, ${t}px)`
if (time < 1) {
requestAnimationFrame(update)
} else {
box.style.transform = null
}
}
}
重新刷新一下代码再点击之后就可以看到滑块的过渡效果了,暂时没有借用 css
的 transition
过渡效果。 明白人肯定会说,用 css
实现,为啥还要用 js
去做动画呢?
答案肯定有必要去做的,在不同的场景下,多个元素的变化的时候,使用js 操作会好很多,举几个很常见的场景。
- 列表的拖拽排序动画
- 列表的增删动画
- 预览图片的放大缩小
等,操作列表类的都合适,再说一个 vue
里面的 transition-group
过渡动画效果也是借用这个思路写的。说了废话下面就来干货,做一个拖拽的列表。
说道拖拽列表的操作,很多情况下不会如想象的一般那么美好,像大部分列表都是不固定的高度的,在操作js时都要去计算当前项的高度和在列表的位置高度等去计算,还要去设置定位操作,过程可谓是一波三折,如果这样的麻烦话,一般来说都是借助第三方工具操作,但是很简单的项目都要引入大的文件的插件话,有点杀鸡焉用牛刀的意思了,不妨动手写个小几十行的插件即可。屁话不多说,开始写个拖拽的列表。
拖拽的列表
说道到拖拽可是使用场景非常多的一个功能了,移动端算是避免不了,如要要做列表的排序,那肯定是拖拽来的方便。可以用以前的老方法 touch
的监听方法,也不要忘记用新的 html5 API 的 draggable
。只要在需要拖拽的元素上加一个 draggable="true"
的属性,就可以完成元素的拖拽监听了。下面完成后的效果展示;
还算丝滑的,实现的代码也不多,就五十多行。
主要是用到了 ondragstart
,ondragenter
,ondragend
,三个事件监听,最多就是在enter 的时候做的一些处理,其实跟单个的flip
动画思路也是一样的,只不过多了一次循环的处理。
看一下代码实现;
<div class="container">
<div class="item" draggable="true" style="height: 30px;">1</div>
<div class="item" draggable="true">2</div>
<div class="item" draggable="true">3</div>
<div class="item" draggable="true">4</div>
<div class="item" draggable="true" style="height: 60px;">5</div>
<div class="item" draggable="true">6</div>
<div class="item" draggable="true" style="height: 70px;">7</div>
<div class="item" draggable="true">8</div>
<div class="item" draggable="true">9</div>
</div>
Html 部分没什么好说的,就是模拟列表的不同大小高度设置,需要注意的是,里面的item 元素下的所有子元素最好要禁用掉 pointer-events
事件,可以在 css 上直接设置 pointer-events: none;
即可。这样做的目的是为了在拖拽的时候进入到子元素产生不必要处理。
JS 部分可以分开来说
<script>
// 获取容器 dom 元素
const container = document.querySelector('.container')
let sourceNode = null
container.ondragstart = (e) => {
// 延迟加上移动的 class 类
setTimeout(() => {
e.target.classList.add('move')
}, 0);
// 保存正在拖拽的 dom 元素
sourceNode = e.target.closest('.item')
}
</script>
开始拖拽的事件简单,没啥好说的,主要是加上拖拽的样式和保存正在拖拽的元素,好让在下面拖拽到其他元素中做一个对比,因为加类名同步事件,导致拖出来的元素也是改变的,所以加一个异步的延迟。值得说的是,这里拖拽只针对了外部容器的监听,其实就是一个事件的委托,有兴趣可以了解一下。
container.ondragenter = (e) => {
e.preventDefault()
const currentNode = e.target.closest('.item')
if (e.target === container || currentNode === sourceNode) {
return
}
const children = Array.from(container.children)
const currentIndex = children.indexOf(currentNode)
const sourceIndex = children.indexOf(sourceNode)
// 保存初始的位置数据
children.forEach(child => {
child.rect = child.getBoundingClientRect()
})
// 通过判断索引来区分是 往上移动 还是向下移动
if (currentIndex > sourceIndex) {
container.insertBefore(currentNode, sourceNode)
} else {
container.insertBefore(sourceNode, currentNode)
}
// 获取结束后 item 的位置
children.forEach(child => {
const rect = child.getBoundingClientRect()
// 计算两个坐标位置差值
const diffLeft = child.rect.left - rect.left
const diffTop = child.rect.top - rect.top
// 趁浏览器还没来的及渲染,设置其位移距离
child.style.transform = `translate(${diffLeft}px, ${diffTop}px)`
// 执行动画
requestAnimationFrame(update)
const start = new Date().getTime()
function update() {
let time = (new Date().getTime() - start) / 300
time = Math.min(1, Math.max(0, time))
const l = diffLeft * (1 - time)
const t = diffTop * (1 - time)
child.style.transform = `translate(${l}px, ${t}px)`
if (time < 1) {
requestAnimationFrame(update)
} else {
child.style.transform = null
}
}
})
}
拖拽中处理的事情还是比较多,总体来说也并不复杂,需要考虑的主要有以下几点;
- 因为是委托事件,所以先要把自身和外部的父容器给过滤掉,保证不是自己的元素可以继续往下执行。
- 开始进入其他元素之前先保存上一个位置,换句话说就是保存当前的位置到
dom
自身。 - 然后通过判断索引来区分是 往上移动 还是向下移动,然后再使用
insertBefore
插入到对应的位置。 - 循环所有的元素来对比移动位置距离,并且赋值样式
transform
,这样元素看起来没有插入过去,或者说没有移动到指定位置,但实际上是transform
的影响。 - 最后就是执行动画,使用的是
requestAnimationFrame
提高性能。
跟上面说的动画思路是一样的,提前计算好位置,然后再执行动画。
container.ondragend = (e) => {
e.target.classList.remove('move')
}
最后一步就是拖拽后把元素的样式重置。
flip 动画思路其实可以给我们做到很多出乎意料的事情,如果想继续往下拓展的话可以做一个图片放大缩小的预览效果,拖拽九宫格排序,等等。