ETJava Beta | Java    注册   登录
  • 搜索:
  • IntersectionObserver + scrollIntoView 实现电梯导航

    发表于      阅读(1)     博客类别:Crawler     转自:https://www.cnblogs.com/zsxblog/p/18346207
    如有侵权 请联系我们删除  (页面底部联系我们)  

    电梯导航也被称为锚点导航,当点击锚点元素时,页面内相应标记的元素滚动到视口。而且页面内元素滚动时相应锚点也会高亮。电梯导航一般把锚点放在左右两侧,类似电梯一样。常见的电梯导航效果如下,比如一些官方文档中:

    image

    image

    之前可能会用 getBoundingClientRect() 判断元素是否在视口中来实现类似效果,但现在有更方便的方法了,那就是 IntersectionObserver + scrollIntoView,轻松实现电梯导航。

    scrollIntoView() 介绍

    scrollIntoView() 方法会滚动元素的父容器,使元素出现在可视区域。默认是立即滚动,没有动画效果。

    如果要添加动画效果,可以这么做:

    scrollIntoView({
      behavior: 'smooth'  // instant 为立即滚动
    })
    

    它还有两个可选参数 blockinline

    block 表示元素出现在视口时垂直方向与父容器的对齐方式,inline 表示元素出现在视口时水平方向与父容器的对齐方式。

    他们同样都有四个值可选 startcenterend 、nearest。默认为 start;

    scrollIntoView({
      behavior: 'smooth',
      block:'center',
      inline:'center',
    })
    

    对于 block

    • start  将元素的顶部和滚动容器的顶部对齐。

    • center  将元素的中心和滚动容器的中心垂直对齐。

    • end  将元素的底部和滚动容器的底部对齐。

    对于 inline

    • start 将元素的左侧和滚动容器的左侧对齐。

    • center  将元素的中心和滚动容器的中心水平对齐。

    • end  将元素的右侧和容器的右侧对齐。

    nearest 不论是垂直方向还是水平方向,只要出现在视口任务就完成了。可以理解为以最小移动量让元素出现在视口,(慵懒移动)。如果元素已经完全出现在视口中,则不会发生变化。

    通过下面动图来感受这个变化,下面滚动容器中有四行五列,包含了从字母 AT。点击 出现在视口 的按钮会取三个下拉框的值作为参数来调用 scrollIntoView() 方法。

    image

    再来看看设置为 nearest 后的滚动情况

    image

    当字母 G 在视口内时,调用方法滚动容器不会发生变化。当 G 不完全在视口内,则会滚动到完全出现在视口内为止。

    在这里可以查看这个完整例子 scrollIntoView 可选项参数实践(codepen)

    而且 scrollIntoView 兼容性也很好

    image

    IntersectionObserver 介绍

    Intersection Observer API(交叉观察器 API) 提供了一种异步检测目标元素与祖先元素或顶级文档的视口相交情况变化的方法。也就是能判断元素是否在视口中,并且能监听元素在视口中出现的可见部分的比例,从而可以执行我们自定义的逻辑。

    由于是异步,也就不会阻塞主线程,性能自然比之前的频繁执行 getBoundingClientRect() 判断元素是否在视口中要好。

    创建一个 IntersectionObserver

    let options = {
      root: document.querySelector(selector),
      rootMargin: "0",
      threshold: 1.0,
    };
    
    let observer = new IntersectionObserver(callback, options);
    
    let target = document.querySelector(selector);
    observer.observe(target); //监听目标元素
    

    通过调用 IntersectionObserver 构造函数可以创建一个交叉观察器,构造函数接收两个参数,一个回调函数和一个可选项。上面例子中,当元素完全出现(100%)在视口中时会调用回调函数。

    可选项

    • root 用作视口的元素,必须是目标的祖先。默认为浏览器视口。

    • rootMargin 根周围的边距,也就是可以限制根元素检测视口的大小。值的方向大小和平常用的 margin 一样,例如 "10px 20px 30px 40px"(上、右、下、左)。只不过正数是增大根元素检测范围,负数是减小检测范围。

    比如设置一个可以滚动的 div 容器为根元素,宽高都为1000px。 此时设置 rootMargin:0 表示根元素检测视口大小就是当前根元素可视区域大小,也就是 1000px * 1000px。设置 rootMargin:25% 0 25% 0 表示上下边距为 25%,那么检测视口大小就是 1000px * 500px。

    • threshold 一个数字或一个数字数组,表示目标出现在视口中达到多少百分比时,观察器的回调就应该执行。如果只想在能见度超过 50% 时检测,可以使用 0.5 的值。如果希望每次能见度超过 25% 时都执行回调,则需要指定数组 [0, 0.25, 0.5, 0.75, 1]。默认值为 0(这意味着只要有一个像素可见,回调就会运行)。值为 1.0 意味着在每个像素都可见之前,阈值不会被认为已通过。

    回调函数

    当目标元素匹配了可选项中的配置后,会触发我们定义的回调函数

    let options = {
      root: document.querySelector(selector),
      rootMargin: "0",
      threshold: 1.0,
    };
    
    let observer = new IntersectionObserver(function (entries) {
          entries.forEach(entry => {
            
          })
        }, options);
    

    entries 表示被监听目标元素组成的数组,数组里面每个 entry 都有下列一些值

    • entry.boundingClientRect 返回目标元素的边界信息,值和 getBoundingClientRect() 形式一样。

    • entry.intersectionRatio 目标元素和根元素交叉的比例,也就是出现在检测区域的比例。

    • entry.intersectionRect 返回根和目标元素的相交区域的边界信息,值和 getBoundingClientRect() 形式一样。

    • entry.isIntersecting 返回true或者fasle,表示是否出现在根元素检测区域内

    • entry.rootBounds 返回根元素的边界信息,值和 getBoundingClientRect() 形式一样。

    • entry.target 返回出现在根元素检测区域内的目标元素

    • entry.time 返回从交叉观察器被创建到目标元素出现在检测区域内的时间戳

    比如,要检测目标元素有75%出现在检测区域中就可以这样做:

    entries.forEach(entry => {
        if(entry.isIntersecting && entry.intersectionRatio>0.75){
             
        }
    })
    

    监听目标元素

    创建一个观察器后,对一个或多个目标元素进行观察。

    let target = document.querySelector(selector);
    observer.observe(target);
    
    document.querySelectorAll('div').forEach(el => {
        observer.observe(el)
    })
    

    IntersectionObserver 的兼容性也很好:

    image

    掌握了 IntersectionObserver + scrollIntoView 的用法,实现电梯导航就简单了。

    简单写一个电梯导航的 htmlcss

    <div class="a" style="background:aqua;">第一章</div>
    <div class="b" style="background: blueviolet;">第二章</div>
    <div class="c" style="background: chartreuse;">第三章</div>
    <div class="d" style="background: darkgoldenrod;">第四章</div>
    <div class="e" style="background: firebrick;">第五章</div>
    <div class="f" style="background: gold;">第六章</div>
    <div class="g" style="background: hotpink;">第七章</div>
    <ul class="rightBox">
        <li class="aLi">第一章</li>
        <li class="bLi">第二章</li>
        <li class="cLi">第三章</li>
        <li class="dLi">第四章</li>
        <li class="eLi">第五章</li>
        <li class="fLi">第六章</li>
        <li class="gLi">第七章</li>
    </ul>
    
    html,
    body {
      width: 100%;
      height: 100%;
      background-color: #fff;
    }
    
    ul,li{list-style: none;}
    
    body {
      padding: 20px 0;
    }
    
    div{
      width: 60%;
      height: 70%;
      border-radius: 10px;
      margin-left: auto;
      margin-right: auto;
      opacity: 0.4;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 30px;
      font-weight: bold;
      color: #000;
    }
    
    div+div {
      margin-top: 20px;
    }
    
    .rightBox {
      position: fixed;
      right: 20px;
      top: 50%;
      color: teal;
      transform: translatey(-50%);
    }
    
    li {
      cursor: pointer;
      box-sizing: border-box;
      border: 1px solid #fff;
      border-radius: 4px;
      padding: 8px 12px;
    }
    
    li:hover {
      background: #f5d2c4;
    }
    
    .active {
      background: #f5d2c4;
    }
    

    预览如下:

    image


    第一步:点击右边的导航菜单,利用 scrollIntoView 方法使内容区域对应的元素出现在可视区域中。

        let rightBox = document.querySelector('.rightBox')
        rightBox.addEventListener('click', function (e) {
          let target = e.target || e.srcElement;
          if (target && !target.classList.contains('rightBox')) {
            document.querySelector('.' + target.className.replace('Li', '')).scrollIntoView({
              behavior: 'smooth',
              block: 'center'
            })
          }
        }, false)
    

    image


    第二步:页面容器滚动时,当目标元素出现在检测区域内则联动改变对应导航的样式。

    这里 threshold 被设置为 1,也就是当目标元素完全显示在可视区域时执行回调,改变导航菜单的样式。

    let observer = new IntersectionObserver(function (entries) {
      entries.forEach(entry => {
        let target = document.querySelector('.' + entry.target.className + 'Li')
        if (entry.isIntersecting) { // 出现在检测区域内
          document.querySelectorAll('li').forEach(el => {
            if(el.classList.contains('active')){
              el.classList.remove('active')
            }
          })
          if (!target.classList.contains('active')) {
            target.classList.add('active')
          }
        }
      })
    }, {
      threshold: 1
    })
    
    document.querySelectorAll('div').forEach(el => {
      observer.observe(el)
    })
    

    效果如下:

    image


    基本要求达到了,不过在滚动过程中,还有些问题。比如连续两个元素来回切换时,第二个元素比第一个元素在检测区域显示的比例更高,虽然没达到 100%,这时候导航菜单显示还是第一个元素的。见下图:

    image


    所以这里可以控制的更细,两个元素之间谁显示的比例更高时就高亮谁的导航菜单。

    let observer = new IntersectionObserver(function (entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting && entry.intersectionRatio > 0.65) {
                
            }
        })
    }, {
        threshold: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
    })
    

    这里设置了 threshold: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],当目标元素出现在检测区域的比例达到 20%,30%,40%,50%,60%,70%,80% 的时候会执行回调函数,在回调函数里,目标元素可见并且在检测区域显示的比例达到 65% 时高亮导航菜单。这样效果就好些了:

    image

    在这里可以查看这个完整例子 IntersectionObserver + scrollIntoView 实现电梯导航

    当然,具体还是看实际元素块大小和业务需求来定。

    如有帮助,帮忙点点赞,感谢~