js 实现滚动动画到指定元素位置

3,254 阅读2分钟

总流程图

滚动动画总流程.png

主要步骤是获取需要滚动到位置元素的顶部距离,然后使用 ELement 的 scrollTo 方法滚动到指定位置。

详细流程图

滑动流程图详细.png

(1)首先对滚动容器进行获取,没有传入滚动元素 scrollerid 或获取不到 scrollerid 的 ELement 节点则使用 body 作为滚动的容器进行滚动。

(2)获取滚动容器的 scrollTop 和元素的 offsetTop 进行大小对比,判断是向上还是向下滚动。

(3)window.requestAnimationFrame 设置的滚动动画每一帧都会传入当前的时间戳,根据(和第一帧时间戳的差值 / 动画的时间)* |scrollTop - offsetTop|来获取每一帧的滚动位置。

(4)判断和第一帧时间戳的差值 - 动画的时间 >= 0来判断动画时间是不是已经到了,否则动画会一直执行。

(5)当判断到动画时间结束,需要补上最后一帧的动画,原因是当使用和第一帧时间戳的差值 - 动画的时间 >= 0判断最后一帧的动画,可能出现最后一帧和第一帧差值 !== 动画的时间 && 下一帧和第一帧的差值 〉 动画的时间,就会导致其实有几毫秒的距离是没有滚动的。这个时候需要补上真正的最后一帧的滚动动画

具体代码

export const scrollTo = (params = {}) => {
  const baseConfig = {
    duration: 500, // 滚动的动画的时间
    scroller: '', // 滚动容器的 id ,默认为 body
    id: '', // 滚动到指定位置的元素 id
  };
  const config = {
    ...baseConfig,
    ...params,
  };
  const { id, duration, scroller } = config;
  const ele = document.getElementById(id);
  if (ele) {
    const { scrollTop, scrollerOffsetTop } = getScrollTop(scroller);
    // 需要减去滚动容器到顶部的距离,默认滚动容器是 body ,scrollerOffsetTop 的值为 0
    const offsetTop  = ele.offsetTop - scrollerOffsetTop;
    let start;
    let timeOff = false;
    const scrollFrame = (timestramp) => {
      if (start === undefined) {
        start = timestramp;
      }
      const elapsed = timestramp - start;
      const offset = getOffset({
        scrollTop,
        offsetTop,
        elapsed,
        duration,
      });
      if (duration - elapsed >= 0) {
        window.requestAnimationFrame(scrollFrame);
        scolling(scroller, offset);
      } else {
        // 最后一帧因为时间判断可能不会执行,补上最后一帧
        if (!timeOff) {
          scolling(scroller, offsetTop);
          timeOff = true;
        }
      }
    };
    window.requestAnimationFrame(scrollFrame);
  }
};

const getScrollTop = (scrollerTag) =>  {
  let scrollTop;
  let scrollerOffsetTop = 0; // 滚动容器距离 body 顶部的距离
  const ele = document.getElementById(scrollerTag);
  if (ele) {
    scrollTop = ele.scrollTop;
    scrollerOffsetTop = ele.offsetTop;
  } else {
    scrollTop
      = document.documentElement.scrollTop
      || window.pageYOffset
      || document.body.scrollTop;
  }
  return { scrollTop, scrollerOffsetTop };
};

const getOffset = (data) => {
  const { scrollTop, offsetTop, elapsed, duration } = data;
  let offset;
  // 判断是向下还是向上滚动
  if (scrollTop > offsetTop) {
    offset = scrollTop - (elapsed / duration) * (scrollTop - offsetTop);
  } else {
    offset = scrollTop + (elapsed / duration) * (offsetTop - scrollTop);
  }
  return offset;
};

const scolling = (scrollerTag, offset) => {
  const scroller = getScroller(scrollerTag);
  scroller.scrollTo(0, offset);
};

const getScroller = scrollerTag => document.getElementById(scrollerTag) || window;

示例 vue 文件

<template>
  <div>
    <div class="select-list">
      <div
        class="select-item"
        v-for="item in selectList"
        :key="item"
        @click="handleScroll(item)"
      >
        {{ item }}
      </div>
    </div>
    <div class="scroller" id="scroller">
      <div
        class="scroller-child"
        v-for="(item, index) in selectList"
        :key="item"
        :id="baseId + item"
        :style="{ background: colorList[index] }"
      >
        {{ item }}
      </div>
    </div>
  </div>
</template>

<script>
import { scrollTo } from './secoll';
export default {
  data() {
    return {
      selectList: ['1', '2', '3', '4'],
      colorList: ['black', 'white', 'blue', 'green'],
      baseId: 'scroller-child_',
    };
  },
  methods: {
    handleScroll(item) {
      scrollTo({
        id: this.baseId + item,
        // 注释掉不是 body 为滚动容器
        // scroller: 'scroller',
      });
    },
  },
};
</script>

<style lang="less" scoped>
.select-list {
  display: flex;
  justify-content: space-around;
  flex-direction: row;
  font-size: 24px;
  position: fixed;
  top: 0;
  width: 100%;
}

.scroller {
  margin-top: 40px;
  // 注释掉不是 body 为滚动容器
  // max-height: 600px;
  // overflow-y: scroll;
}

.scroller-child {
  margin: 0 20px 20px 20px;
  border: 1px solid olivedrab;
  height: 1550px;
}
</style>