【问题标题】:Determine how much of the viewport is covered by element (IntersectionObserver)确定元素覆盖了多少视口(IntersectionObserver)
【发布时间】:2019-09-04 10:03:25
【问题描述】:

当元素进入视口时,我使用IntersectionObserver 向元素添加和删除类。

我不想说“当 X% 的元素可见时 - 添加此类”我想说“当 X% 的元素可见时当 X% 的视口被覆盖时按元素 - 添加此类”。

我假设这是不可能的?如果是这样,我认为IntersectionObserver 有点缺陷,因为如果您有一个比视口高 10 倍的元素,除非您将阈值设置为 10% 或更低,否则它永远不会被视为可见。当你有可变高度的元素时,尤其是在响应式设计中,你必须将阈值设置为 0.1% 之类的东西,以“确定”元素将接收类(但你永远无法真正确定)。

编辑:回应摩西的回复。

Edit2:更新了几个阈值,以强制它更频繁地计算 percentOfViewport。仍然不理想。

var observer = new IntersectionObserver(function (entries) {
	entries.forEach(function (entry) {
		var entryBCR = entry.target.getBoundingClientRect();
		var percentOfViewport = ((entryBCR.width * entryBCR.height) * entry.intersectionRatio) / ((window.innerWidth * window.innerHeight) / 100);

		console.log(entry.target.id + ' covers ' + percentOfViewport + '% of the viewport and is ' + (entry.intersectionRatio * 100) + '% visible');

		if (entry.intersectionRatio > 0.25) {
			entry.target.style.background = 'red';
		}
		else if (percentOfViewport > 50) {
			entry.target.style.background = 'green';
		}
		else {
			entry.target.style.background = 'lightgray';
		}
	});
}, {threshold: [0.025, 0.05, 0.075, 0.1, 0.25]});

document.querySelectorAll('#header, #tall-content').forEach(function (el) {
	observer.observe(el);
});
#header {background: lightgray; min-height: 200px}
#tall-content {background: lightgray; min-height: 2000px}
<header id="header"><h1>Site header</h1></header>
<section id="tall-content">I'm a super tall section. Depending on your resolution the IntersectionObserver will never consider this element visible and thus the percentOfViewport isn't re-calculated.</section>

【问题讨论】:

    标签: javascript intersection-observer


    【解决方案1】:

    你需要做的是给每个元素一个不同的阈值。如果元素短于默认阈值(相对于窗口),则默认阈值可以正常工作,但如果它更高,则需要该元素的唯一阈值。

    假设您要触发以下任一元素:

    1. 50% 可见或
    2. 覆盖 50% 的屏幕

    那你需要检查一下:

    1. 如果元素短于窗口的 50%,您可以使用选项 1
    2. 如果元素高于窗口的 50%,您需要给它一个阈值,即窗口的高度除以元素的高度乘以阈值 (50%):
    function doTheThing (el) {
        el.classList.add('in-view');
    }
    
    const threshold = 0.5;
    
    document.querySelectorAll('section').forEach(el => {
        const elHeight = el.getBoundingClientRect().height;
        var th = threshold;
    
        // The element is too tall to ever hit the threshold - change threshold
        if (elHeight > (window.innerHeight * threshold)) {
            th = ((window.innerHeight * threshold) / elHeight) * threshold;
        }
    
        new IntersectionObserver(iEls => iEls.forEach(iEl => doTheThing(iEl)), {threshold: th}).observe(el);
    });
    

    【讨论】:

      【解决方案2】:
      let optionsViewPort = {
        root: document.querySelector('#viewport'), // assuming the viewport has an id "viewport"
        rootMargin: '0px',
        threshold: 1.0
      }
      
      let observerViewport = new IntersectionObserver(callback, optionsViewPort);
      observerViewPort.observe(target);
      

      在回调中,给定视口的大小,给定元素的大小,给定重叠的百分比,就可以计算出视口中的重叠百分比:

        const percentViewPort = viewPortSquarePixel/100;
        const percentOverlapped = (targetSquarePixel * percent ) / percentViewPort;
      

      示例:

      const target = document.querySelector('#target');
      const viewport = document.querySelector('#viewport');
      const optionsViewPort = {
        root: viewport, // assuming the viewport has an id "viewport"
        rootMargin: '0px',
        threshold: 1.0
      }
      
      let callback = (entries, observer) => { 
        entries.forEach(entry => {  
          const percentViewPort = (parseInt(getComputedStyle(viewport).width) * parseInt(getComputedStyle(viewport).height))/100;    
          const percentOverlapped = ((parseInt(getComputedStyle(target).width) * parseInt(getComputedStyle(viewport).height)) * entry.intersectionRatio) / percentViewPort;
          console.log("% viewport overlapped", percentOverlapped);
          console.log("% of element in viewport", entry.intersectionRatio*100);
          // Put here the code to evaluate percentOverlapped and target visibility to apply the desired class
        });
          
      };
      
      let observerViewport = new IntersectionObserver(callback, optionsViewPort);
      observerViewport.observe(target);
      #viewport {
        width: 900px;
        height: 900px;
        background: yellow;
        position: relative;
      }
      
      #target {
        position: absolute;
        left: 860px;
        width: 100px;
        height: 100px;
        z-index: 99;
        background-color: red;
      }
      <div id="viewport">
        <div id="target" />
      </div>

      使用 getBoundingClientRect() 计算重叠区域/目标百分比的替代数学

      const target = document.querySelector('#target');
      const viewport = document.querySelector('#viewport');
      
      const rect1 = viewport.getBoundingClientRect();
      const rect2 = target.getBoundingClientRect();
      
      const rect1Area = rect1.width * rect1.height;
      const rect2Area = rect2.width * rect2.height;
      
      const x_overlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));
      const y_overlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));
      
      const overlapArea = x_overlap * y_overlap;
      const overlapPercentOfTarget = overlapArea/(rect2Area/100);
      
      console.log("OVERLAP AREA", overlapArea);
      console.log("TARGET VISIBILITY %", overlapPercentOfTarget);
      #viewport {
        width: 900px;
        height: 900px;
        background: yellow;
        position: relative;
      }
      
      #target {
        position: absolute;
        left: 860px;
        width: 100px;
        height: 100px;
        z-index: 99;
        background-color: red;
      }
      <div id="viewport">
        <div id="target" />
      </div>

      【讨论】:

      • 这个问题是 IntersectionObserver 仅在元素被视为可见时触发一次,而在不可见时触发一次。因此,滚动时不会更新“覆盖视口的百分比”值。因此,如果我的阈值是 25%(这是我想要的)并且我的超高元素从未达到阈值,则永远不会重新计算视口的百分比......所以我 仍然需要将阈值设置得如此之低,以便相对确定它总是会触发,即使它确实触发了元素也可能不会覆盖大部分视口。
      • 如果你用我的 sn-p 减小视口的大小,你会注意到元素不 可见(你必须向右滚动),尽管交叉点是检测到。如果您可以在 sn-p 中重现您的问题,我会更准确。
      • 我刚刚意识到,通过传入几个阈值,我可以让它更频繁地触发。它远非完美,因为它仍然是一个在某些情况下可能会失败的任意数字,所以我对解决方案不满意。
      • 为什么不传0所以一直触发,然后在回调中移动逻辑?
      • 因为 0 它仍然只会触发一次,只要元素的单个像素可见。
      【解决方案3】:

      这是一个使用ResizeObserver 的解决方案,它在元素> 可见比例或完全覆盖视口时触发回调

      /**
       * Detect when an element is > ratio visible, or when it fully
       * covers the viewport.
       * Callback is called only once.
       * Only works for height / y scrolling.
       */
      export function onIntersection(
        elem: HTMLElement,
        ratio: number = 0.5,
        callback: () => void
      ) {
        // This helper is needed because IntersectionObserver doesn't have 
        // an easy mode for when the elem is taller than the viewport. 
        // It uses ResizeObserver to re-observe intersection when needed.
      
        const maxRatio = window.innerHeight / elem.getBoundingClientRect().height;
      
        const threshold = maxRatio < ratio ? 0.99 * maxRatio : ratio;
        const intersectionObserver = new IntersectionObserver(
          (entries) => {
            const entry = entries[0];
            if (entry.isIntersecting && entry.intersectionRatio >= threshold) {
              disconnect();
              callback();
            }
          },
          { threshold: [threshold] }
        );
      
        const resizeObserver = new ResizeObserver(() => {
          const diff =
            maxRatio - window.innerHeight / elem.getBoundingClientRect().height;
          if (Math.abs(diff) > 0.0001) {
            disconnect();
            onIntersection(elem, ratio, callback);
          }
        });
      
        const disconnect = () => {
          intersectionObserver.disconnect();
          resizeObserver.disconnect();
        };
      
        resizeObserver.observe(elem);
        intersectionObserver.observe(elem);
      }
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-13
        • 2022-09-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多