vue / js scrollIntoView的使用和替代方法(无jquery的滚动动画效果)

scrollIntoView: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollIntoView

背景

笔者想要实现一个页面,该页面包括如下功能:

  1. 顶部Tab
    -需要置顶;
    -超出则左右可滑动;
    -点击时将选中Tab高亮,且自动居中
  2. 内容滚动区域
    -需和顶部Tab联动,即点击Tab,内容滚动至该Tab对应的锚点处;相应的,滚动内容,如到达该锚点时,对应的Tab也需要切换为高亮选中状态

模块、功能拆分:

  1. 页面容器内容,分为两个模块:
    tab等置顶模块;
    滚动区域模块
  2. tab模块实现切换高亮和选中时滚动居中功能
  3. 内容滚动区域,实现与顶部tab的联动

功能实现设计:

  1. 顶部Tab置顶,内容滚动区域选择
    方案一: 严格区分置顶区域和滚动区域,使用overflow:hidden 固定页面容器,将滚动区域的overflow设置为可滚动(注意此时的滚动容器)
    方案二: 直接使用position: fixed固定顶部Tab,滚动区域不做安排,直接使用window容器滚动
  2. 点击Tab时滚动内容导到指定锚点处:
    方案一: 使用scrollIntoView
    方案二: 使用基础的scrollTop赋值的方式
  3. 滚动内容时,如到达该埋点时,自动切换Tab
    方案: 监听scroll(根据功能1的方案确定滚动容器)

注意: 其实滚动容器的选择就决定了功能2、功能3的方案选择

方案比较:

  1. 滚动容器选择方案1,即部分区域可滚动
    缺点1: 需要配置好、区分好可滚动区域,且外部需要设置 overflow: hidden 阻止滚动(注意外部不可有滚动区域)
    缺点2: 后续的scroll监听需要依赖于选择的滚动容器节点
    缺点3: scrollIntoView的使用兼容性问题(smooth平滑动画无效果)
    vue / js scrollIntoView的使用和替代方法(无jquery的滚动动画效果)

  2. 滚动容器选用方案2,即使用window容器进行滚动
    缺点1: scrollIntoView的效果,是将节点滚动到可滚动容器的顶端,而我们是有置顶内容的

所以:
最终选择: 1. 使用window作为滚动区域,不使用scrollIntoView,而是直接通过scrollTop赋值来实现滚动效果

方案一CSS代码: 可实现 顶部Tab 和 滚动区域的严格区分

.page-container {
  position: relative;
  overflow: hidden;
  height: 100%;
  display: flex;
  flex-direction: column;

  header {
    // 顶部固定模块
    width: 100%;
    height: 1.8rem;
  }

  .scroll-container {
    // 滚动容器
    flex: 1;
    overflow: auto;

    .scroll-content {
      // 滚动内容
      .first {}
      .second {}
      .third {}
      .fourth {}
      .fifth {}
    }
  }
}

方案一JS代码: 可实现 点击Tab 滚动到指定锚点

methods: {
    setActiveIndex(index) {
      // 滚动tab到可视区域中间
      // 使用 smooth 或者 center 有问题?
      this.$refs[`topTab${index}`][0]?.scrollIntoView({ inline: 'center' });
      if (this.activeIndex === index) {
        return;
      }
      this.activeIndex = index;
      // 滚动到指定位置
      this.$refs[`scrollView${index}`]?.scrollIntoView();
    },
}

scrollIntoView 平滑滚动
element.scrollIntoView({behavior: "smooth"})

最终实现方案:

切换Tab实现内容滚动到指定锚点:

methods: {
   // 滚动到指定位置
    scrollToElePosition(index) {
      const { first, second, third, fourth, fifth } = this.$refs;
      // 记录当前滚动状态
      this.isSmoothScrolling = true;
      // 动画时间
      const SCROLL_DURATION = 500;
      if (index === TAB_INDEX_ZERO) {
        scrollToY(first.offsetTop, SCROLL_DURATION);
      } else if (index === TAB_INDEX_ONE) {
        scrollToY(second.offsetTop, SCROLL_DURATION);
      } else if (index === TAB_INDEX_TWO) {
        scrollToY(third.offsetTop, SCROLL_DURATION);
      } else if (index === TAB_INDEX_THREE) {
        scrollToY(fourth.offsetTop, SCROLL_DURATION);
      } else if (index === TAB_INDEX_FOUR) {
        scrollToY(fifth.offsetTop, SCROLL_DURATION);
      }
    },
}

使用scrollTop赋值实现滚动动画:

/*
 * y: the y coordinate to scroll, 0 = top
 * duration: scroll duration in milliseconds; default is 0 (no transition)
 * element: the html element that should be scrolled ; default is the main scrolling element
 */
const scrollToY = function (y, duration = 0, element = document.scrollingElement) {
  // cancel if already on target position
  if (element.scrollTop === y) {
    return;
  }

  const NUMBER_TWO = 2;
  const cosParameter = (element.scrollTop - y) / NUMBER_TWO;
  let scrollCount = 0;
  let oldTimestamp = null;

  const step = function (newTimestamp) {
    if (oldTimestamp !== null) {
      // if duration is 0 scrollCount will be Infinity
      scrollCount += Math.PI * (newTimestamp - oldTimestamp) / duration;
      if (scrollCount >= Math.PI) {
        element.scrollTop = y;
        return;
      }
      element.scrollTop = cosParameter + y + cosParameter * Math.cos(scrollCount);
    }
    oldTimestamp = newTimestamp;
    window.requestAnimationFrame(step);
  };
  window.requestAnimationFrame(step);
};

监听内容滚动,切换Tab

实现方式: 主要是通过scroll监听来实现,通过获取当前的scrollTop 来和 每个锚点模块的offsetTop 进行比较
注意点:

  1. 注意throttle节流的使用
  2. 需要使用 isSmoothScrolling 来避免 和 切换Tab时滚动到指定锚点的功能相互影响
mounted() {
  // 基准offset - first模块
    const { offsetTop: firstOffsetTop } = first;
    // 节流时间间隔
    const THROTTLE_INTERVAL = 200;
    window.addEventListener(
      'scroll',
      !this.isSmoothScrolling &&
        throttle(() => {
          // 正在平滑滚动,无需监听
          if (this.isSmoothScrolling) return;
          // 当前的滚动距离
          const scrollTop =
            window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
          // 各个临界值,可能会变
          const secondOffsetTop = second.offsetTop;
          const thirdOffsetTop = third.offsetTop;
          const fourthOffsetTop = fourth.offsetTop;
          const fifthOffsetTop = fifth.offsetTop;
          // scroll计算当前的activeTab
          this.scrollCalcCurrentActiveTab({
            scrollTop,
            firstOffsetTop,
            secondOffsetTop,
            thirdOffsetTop,
            fourthOffsetTop,
            fifthOffsetTop,
          });
        }, THROTTLE_INTERVAL)
    );
},
methods: {
  // scroll计算当前的activeTab
    scrollCalcCurrentActiveTab({
      scrollTop,
      firstOffsetTop,
      secondOffsetTop,
      thirdOffsetTop,
      fourthOffsetTop,
      fifthOffsetTop,
    }) {
      // 此处向上取整,因为offsetTop获取的是整数,这会导致滚动时tab位置错误
      const currentOffsetTop = firstOffsetTop + Math.ceil(scrollTop);
      if (currentOffsetTop < secondOffsetTop) {
        this.activeIndex = 0;
      } else if (currentOffsetTop >= secondOffsetTop && currentOffsetTop < thirdOffsetTop) {
        this.activeIndex = 1;
      } else if (currentOffsetTop >= thirdOffsetTop && currentOffsetTop < fourthOffsetTop) {
        this.activeIndex = 2;
      } else if (currentOffsetTop >= fourthOffsetTop && currentOffsetTop < fifthOffsetTop) {
        this.activeIndex = 3;
      } else if (currentOffsetTop >= fifthOffsetTop) {
        this.activeIndex = 4;
      }
    },
}

最后:

  1. 需要注意 动画滚动和 window.addEventListener('scroll') 监听相互干扰的情况,需要使用变量 isSmoothScrolling 来规避
    实现:
    在切换tab,内容滚动至锚点时 this.isSmoothScrolling = true;
    监听touchmove事件,将 this.isSmoothScrolling = false;
    scroll监听是通过判断isSmoothScrolling,来确定是否执行scroll监听的回调方法
mounted() {
    // 此处通过 
    window.addEventListener(
      'touchmove',
      throttle(() => {
        if (this.isSmoothScrolling) {
          this.isSmoothScrolling = false;
        }
      })
    );
}
  1. 注意使用节流来节省性能

最后的最后:

无jquery 的 滚动动画效果:
https://*.com/questions/21474678/scrolltop-animation-without-jquery
https://github.com/Robbendebiene/Sliding-Scroll/blob/master/sliding-scroll.js

上一篇:门面模式 与 装饰器模式(3)


下一篇:Ubuntu16.04 中PHP7.0 安装pdo_mysql 扩展