动画的简单实现思路

226 阅读2分钟

随着互联网的持续发展,H5 页面作为与用户直接交互的表现层越来越复杂,呈现的形式也越来越丰富,从而也要求 H5 页面具有更多的花样性及动画效果。

我们在C端开发的时候难免会遇到动画开发的需求,我通常会用到 transform、animate,或者说用requestAnimationFrame方法来实现动画,大不了让设计出一版GIF图,直接挂到页面上也行。

今天换一种思路,通过vue加上一些css3属性,来实现一款九宫格打乱顺序的动画。具体功能:通过点击打乱按钮,对一个长度为10的数组,进行打乱操作,具体效果如下视频。

github: github.com/xinlong-che…

9152187227128130_2022-06-06_10.42.54.gif

其实,这个打乱动画,只要知道格子的初始位置和最终位置就可以了,但是我们如果计算每一个格子的位置,然后手动的进行每个格子的移动工作,这样维护起来太过于繁琐了,不是什么好办法哈哈哈。

解决方案:

我们是知道的,当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~