利用交叉观察器,实现页面滚动元素入场动画

1,234 阅读4分钟

需求

先看一下需求,实现效果如下图,就是用户滑动页面的时候实现元素这样按顺序“蹭蹭蹭”的冒出来。

cb8327fcccf783cdcdec741abdfc6ddb.gif

效果并不复杂,要实现这个效果我最开始想到的,就是根据用户滚动页面的距离和元素位置去计算。这样的确能实现,但是一想到那冗余的计算和冗长的计算代码我内心有点拒绝。所以我想,有没有方式去监听元素在视窗上是否可见,这样我可以先隐藏这些动画元素,在它的位置进入视窗的时候我再加个动画让它“跳”出来。然后就想起了IntersectionObserver API,顺带总结一下。

IntersectionObserver API 使用

IntersectionObserver API 可以监听目标元素是否离开或进入指定父元素的内部,也叫“交叉观察器”。

用法

let observer = new IntersectionObserver(entries => {
    console.log(entries);
 }, options);

接受两个参数:callback是发生交叉时的回调函数,option是配置对象(非必填)。

实例对象原型上的常用方法

名称说明参数
observe开始监听一个目标元素目标元素节
unobserve停止监听一个目标元素目标元素节
takeRecords返回所有监听的目标元素集
disconnect停止所有监听

callback

callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象。如果同时有两个被观察的对象与指定父元素发生了交叉行为,entries数组就会有两个成员。 如下图所示,两张美女图是几乎同时进入视口的,所以callback函数中打印出来的entries就有两项:

b3ca09eabae5aed1a3a691578524fbe8.gif

其中IntersectionObserverEntry对象的常见属性及其对应含义如下表:

属性名称含义
rootBounds根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有指定根元素(即直接相对于视窗滚动),则返回null
boundingClientRect目标元素的区域信息
intersectionRect目标元素与视口(或指定根元素)的交叉区域的信息
target被观察的目标元素,是一个 DOM 节点对象
time可见性发生变化的时间戳,单位为毫秒
isIntersecting目标元素是否正在交叉,可用做判断元素是否可见
intersectionRatio即目标元素与根元素交叉区域/目标元素区域,完全可见时为1,完全不可见时小于等于0

option 配置对象

属性及其对应含义如下表:

属性名称含义
root指定根元素,默认为视窗
rootMargin触发交叉的偏移值,默认为"0px 0px 0px 0px"(上左下右,正数为向外扩散,负数则向内收缩)
threshold触发回调函数时机,默认为[0],即交叉比例达到0时触发回调函数。

因为在本次需求中,需要元素“跳”入视窗中,所以元素的入场动画不能再元素刚好或者将要发生交叉行为的时候再触发,这样为时过早,当rootMargin为默认值"0px 0px 0px 0px"效果如下:

d48e91a1c09495aa3a6f9d572befc7cc.gif

这里可以发现,当用户缓慢划动页面时,元素很有可能在不太显眼的地方把入场动画给完成了,那我花时间写的动画只能给用户看个寂寞了。所以调整rootMargin为"0px 0px -50px 0px":

const observer = new IntersectionObserver((entries, observer) => {
    // ...do something
 }, {
    rootMargin: '0px 0px -50px 0px',
 });

画张图示意下:

97687a31e321fd3f5ace553a43bfdae1.png

就是目标元素已经进入视窗50px左右,元素的入场动画才开始执行,这也是开头我想要先隐藏这些动画元素的原因。调整后如下:

69d57f6642f86711013090f847b2097d.gif

假设UI想要随着元素进入或移除视窗的区域比占变化来做透明度的变化也是可以的,这里可以利用threshold配置,threshold为[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,触发回调函数。控制台中打印目标元素的intersectionRatio:

const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach((item = {}) => {
       // ...do something
        console.log(item.intersectionRatio.toFixed(2));
      });
 }, {
    threshold: [0, 0.25, 0.5, 0.75, 1]
 });

效果如下:

2909741d98ea7cc644652f131f0d76df.gif

2909741d98ea7cc644652f131f0d76df.gif

实现代码

根据需求我这里还有通过自定义属性给图片添加不同类型的入场动画,下面代码中没有体现出来,自由发挥。

// html
<div className={classPrefix}>
  <div className={cx('contain')}>
    <div className={cx('first')}>
      <div className={cx('download')}>立即下载体验</div>
    </div>
    <div className={cx('second bounceInUpPage')}>
      <img src={SECOND_TITLE} className='title' alt='' />
      <img src={SECOND_IMG_LEFT} className='left' dataclass='left' alt='' />
      <img src={SECOND_IMG_RIGHT} className='right' dataclass='right' alt='' />
      <img src={CALL} className='call' dataclass='flot' alt='' />
      <img src={VEDIO} className='vedio' dataclass='right' alt='' />
      <img src={VOICE} className='voice' dataclass='right' alt='' />
      <img src={TEXT} className='text' dataclass='right' alt='' />
    </div>
  </div>
</div>

在元素入场后便停止监听了:

// js
const images = document.querySelectorAll('.bounceInUpPage')
const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((item = {}) => {
    if (item.isIntersecting) {
      try {
        item.target.classList.add('bounceInUp')
        observer.unobserve(item.target); // 停止监听
      } catch (err) {
        console.log(err);
        observer.unobserve(item.target); // 停止监听
      }
    }
  });
}, {
  rootMargin: '0px 0px -50px 0px'
});
images.forEach((item = {}) => {
  const childrenNode = item.children || []
  childrenNode.forEach(child => observer.observe(child))
});

其他使用场景

利用交叉观察者还可以实现图片懒加载、触底、吸顶等效果,参考:

  1. 利用"交叉观察者"这个小宝贝儿,轻松实现懒加载、吸顶、触底
  2. IntersectionObserver API 使用教程