随着互联网的持续发展,H5 页面作为与用户直接交互的表现层越来越复杂,呈现的形式也越来越丰富,从而也要求 H5 页面具有更多的花样性及动画效果。
我们在C端开发的时候难免会遇到动画开发的需求,我通常会用到 transform、animate,或者说用requestAnimationFrame方法来实现动画,大不了让设计出一版GIF图,直接挂到页面上也行。
今天换一种思路,通过vue加上一些css3属性,来实现一款九宫格打乱顺序的动画。具体功能:通过点击打乱按钮,对一个长度为10的数组,进行打乱操作,具体效果如下视频。
github: github.com/xinlong-che…
其实,这个打乱动画,只要知道格子的初始位置和最终位置就可以了,但是我们如果计算每一个格子的位置,然后手动的进行每个格子的移动工作,这样维护起来太过于繁琐了,不是什么好办法哈哈哈。
解决方案:
我们是知道的,当Dom元素的属性(比如left、right、transform这样的属性)改变的时候,浏览器是不会立即渲染的,而是推迟到浏览器的下一帧才进行渲染。 通过这个知识,我们就能够知道一个中间的时间点,这个时间点就是Dom的位置发生改变了,但是浏览器还未改变渲染,这样我们就能提前拿到最终位置Dom元素的属性。
拿到元素的属性,之后我们就可以通过transform等,对元素进行动画的操作了。
代码实现如下:
1.我们先来创建HTML和CSS
<template>
<button @click="handleShuffle">乱序</button><div class="SquareBox" ref="listRef">
<AnimateItem v-for="(num) in initData" :num="num" :key="num"></AnimateItem>
</div>
</template>
<style scoped>
.SquareBox {
display: flex;
flex-wrap: wrap;
}
</style>
AnimateItem.vue
<template>
<div class="SquareItem">
{{ num }}
</div>
</template>
<script>
export default {
props: {
num: {
type: Number
}
}
}
</script>
<style scoped>
.SquareItem {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: 1px solid #eee;
}
</style>
2.我们来实现一个产生createChildElementRectMap的函数,通过Map的数据结构,对每个格子进行位置的缓存,key为当前dom,value为当前dom的位置信息。
const createChildElementRectMap = (nodes) => {
if(!nodes) {
return new Map;
}
const elements = Array.from(nodes.childNodes);
return new Map(elements.map((node) => {
try {
return [node, node.getBoundingClientRect()]
} catch (error) {
return null
}
}).filter(Boolean));
}
3.首先点击打乱的按钮,对变更前的初始位置进行DOM信息的缓存,然后通过nextTick函数(onUpdated也行),等到DOM更新成功之后,取到最终DOM的信息。
<script>
import { nextTick, ref, watch } from 'vue';
import { shuffle } from 'lodash';
import AnimateItem from './Amimate-item.vue';
import { createChildElementRectMap } from './utils';
export default {
components: {
AnimateItem,
},
setup() {
const initData = ref([1,2,3,4,5,6,7,8,9,10]);
const listRef = ref(null);
const prevNodeList = ref(new Map());
const handleShuffle = () => {
initData.value = shuffle(initData.value);
prevNodeList.value = createChildElementRectMap(listRef.value)
}
watch(initData, () => {
nextTick(() => {
const currentNodeList = createChildElementRectMap(listRef.value);
console.log(prevNodeList, currentNodeList);
prevNodeList.value.forEach((prevRect, node) => {
const currentRect = currentNodeList.get(node);
const offset = {
left: prevRect.left - currentRect.left,
top: prevRect.top - currentRect.top,
};
const keyframes = [
{
transform: `translate(${offset.left}px, ${offset.top}px)`,
},
{ transform: "translate(0, 0)" },
];
node.animate(keyframes, {
duration: 1000,
easing: "cubic-bezier(0.25, 0.8, 0.25, 1)",
});
});
});
})
return {
initData,
listRef,
prevNodeList,
handleShuffle,
}
},
}
</script>
这样一个简单的动画就完成了~
通过代码实现完成之后,我们在把这些业务逻辑封装成组件吧,名字叫vue-simple-animater,以供日后直接使用
代码如下(github:github.com/xinlong-che…):
看一下如何使用vue-simple-animater组件吧!主要的组件,Animater组件负责监听数据变化以及实现动画,Animated组件主要负责每个子区域的Dom信息的收集工作。
具体的组件代码可以移步github查看~
<template>
<div>
<button @click="handleShuffle">乱序</button>
<button @click="handleRecover">重置</button>
<button @click="handleAdd">新增</button>
<Animater :data="initData" :duration="1000">
<div class="SquareBox" ref="listRef">
<Animated v-for="(num) in initData" :key="num">
<AnimateItem :num="num">{{ num }}</AnimateItem>
</Animated>
</div>
</Animater>
</div>
</template>
<script>
import { reactive, toRefs } from 'vue';
import Animater from '../../AnimateComp/Animater.vue';
import Animated from '../../AnimateComp/Animated.vue';
import AnimateItem from '../../components/Amimate-item.vue';
import { shuffle, range } from 'lodash';
export default {
components: {
Animater,
Animated,
AnimateItem,
},
setup() {
const initConfig = [1,2,3,4,5,6,7,8,9,10]
const state = reactive({
initData: initConfig,
});
const handleShuffle = () => {
state.initData = shuffle(state.initData);
};
const handleRecover = () => {
state.initData = initConfig;
}
const handleAdd = () => {
state.initData = range(state.initData.length, state.initData.length + 10).concat(state.initData)
}
return {
...toRefs(state),
handleShuffle,
handleRecover,
handleAdd,
}
},
}
</script>
<style scoped>
.SquareBox {
display: flex;
flex-wrap: wrap;
}
</style>
\
end~