先看效果:
实现思路:获取页面中需要加载动画的节点,用元素的animate()
方法创建一个动画对象,并传入两个关键帧,接着使用IntersectionObserver
API创建观察对象,用于观察元素进入页面。当元素进入界面时,执行动画,最后取消元素的观察即可。
具体实现:
第一步:
<div class="container">
<div class="module">模块1</div>
<div class="module">模块2</div>
<div class="module">模块3</div>
<div class="module">模块4</div>
<div class="module">模块5</div>
<div class="module">模块6</div>
<div class="module">模块7</div>
<div class="module">模块8</div>
<div class="module">模块9</div>
<div class="module">模块10</div>
</div>
<script>
const DISTANCE = 100;
onload = function() {
// 页面加载完毕,获取到所有需要加载动画的节点
document.querySelectorAll('.module').forEach(module => {
// 循环调用每个模块的animate方法。用法:animate(keyframes, options)
// keyframes 关键帧对象数组,或一个关键帧对象(其属性为可迭代值的数组)
// options 代表动画持续时间的整数(以毫秒为单位),或者一个包含一个或多个时间属性的对象
let animation = module.animate([
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0
},
{
transform: 'translateY(0)',
opacity: 1
}
], {
duration: 1000,
easing: 'ease-in-out'
})
animation.pause() // 创建好animation对象后,首先暂停动画的执行,待会儿用监听器监听元素进入界面后开启动画
})
}
</script>
第二步:
// 创建一个WeakMap对象,用于存储节点及动画对象,WeakMap是为了防止内存泄漏,当元素消失时,元素会自动销毁在对象中的存储
let map = new WeakMap()
// 创建一个监视器
let observer = new IntersectionObserver((entries, observer) => {
// entries是获取到的所有监听对象
entries.forEach(entry => {
// 判断当元素进入到界面
if (entry.isIntersecting) {
// 获取每个元素上的动画对象
const animation = map.get(entry.target)
// 当动画存在时,执行动画,并取消元素进入界面的观察
if(animation) {
animation.play()
observer.unobserve(entry.target)
}
}
});
});
onload = function() {
document.querySelectorAll('.module').forEach(module => {
let animation = module.animate([
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0
},
{
transform: 'translateY(0)',
opacity: 1
}
], {
duration: 1000,
easing: 'ease-in-out'
})
animation.pause()
// 用上方创建的监听器 观察每一个模块节点
observer.observe(module)
// 将每个节点及动画存储对象中
map.set(module, animation)
})
}
最后一步:
解决关键性问题,当滚动条滚动到界面中间时,刷新界面后,不论是往上滚还是往下滚,再次进入界面的元素都会执行动画。当然我们所需要的是往下滚动时下面的元素进入界面需要加载动画,而上方的元素进入界面不需要动画。那么我们就可以这样写:
let map = new WeakMap()
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const animation = map.get(entry.target)
if(animation) {
animation.play()
observer.unobserve(entry.target)
}
}
});
});
const DISTANCE = 100
function isViewport(element) {
const rect = element.getBoundingClientRect();
return rect.top - DISTANCE > window.innerHeight
}
onload = function() {
document.querySelectorAll('.module').forEach(module => {
// 判断节点是否在视口内
if(!isViewport(module)) return;
let animation = module.animate([
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0
},
{
transform: 'translateY(0)',
opacity: 1
}
], {
duration: 1000,
easing: 'ease-in-out'
})
animation.pause()
observer.observe(module)
map.set(module, animation)
})
}
结尾:上面的示例是在html文件中完成的,当然你也可以在Vue中实现,在Vue中,你可以自定义指令,通过获取到的el
对象绑定动画、观察,甚至是通过绑定的指令传入动画的参数,来动态执行动画。
为什么需要使用animate方法,而不是在节点的style中添加动画?
因为我们是要封装公用指令、组件,如果我们继续使用style,向其添加动画,有可能和节点原有的动画冲突,为了不对元素本身的动画照成影响,我们可以使用Animation
API,它的好处就在于 不会改变元素的DOM树,不会改变元素本身的属性,这样就可以避免两者的冲突。