H5模拟移动端惯性滚动(惯性滑动)边界回弹效果

74 阅读1分钟
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Inertia Scrolling with Bounce</title>
  <style>
    #scrollContainer {
      width: 300px;
      height: 500px;
      overflow: hidden;
      border: 1px solid black;
      position: relative;
    }
    #content {
      min-height: 1000px;
      background: linear-gradient(to bottom, #ffcc00, #ff6600);
      display: grid;
      grid-gap: 10px;
      grid-template-columns: repeat(2, 1fr);
      justify-items: center;
      /* overflow-x: hidden; */
    }
    .item {
      height: 100px;
      width: 100px;
      background: white;
    }
  </style>
</head>
<body>
  <div id="scrollContainer">
    <div id="content">
      
    </div>
  </div>

  <script>
    let c = ``;
    for(let i = 0; i < 100; i++) {
      c += `<div class="item">${i+1}</div>`;
    }
    document.querySelector('#content').innerHTML = c;


    const container = document.getElementById('scrollContainer');
    const content = document.getElementById('content');
    const contentHeight = content.offsetHeight;
    let isDragging = false;
    let startY = 0;
    let scrollY = 0;
    let velocity = 0;
    let lastMoveY = 0;
    let lastTimestamp = 0;
    let animationFrame;

    container.addEventListener('touchstart', (e) => {
      e.preventDefault();
      const touch = e.touches[0];
      isDragging = true;
      startY = touch.clientY;
      // scrollY = container.scrollTop;
      scrollY = getTranslateY(content);
      velocity = 0;
      lastMoveY = touch.clientY;
      lastTimestamp = e.timeStamp;
      cancelAnimationFrame(animationFrame);
    }, { passive: false });

    document.addEventListener('touchmove', (e) => {
      if (!isDragging) return;
      e.preventDefault();
      const touch = e.touches[0];
      const dy = touch.clientY - startY;
      // container.scrollTop = scrollY - dy;
      
      content.style.transform = `translateY(${scrollY + dy}px)`;
      const now = e.timeStamp;
      const dt = now - lastTimestamp;
      velocity = (touch.clientY - lastMoveY) / dt * 1000;
      lastMoveY = touch.clientY;
      lastTimestamp = now;
    }, { passive: false });

    document.addEventListener('touchend', (e) => {
      if (!isDragging) {return}
      e.preventDefault();
      isDragging = false;
      animateInertia();
    }, { passive: false });

    function animateInertia() {
      // const friction = 0.05;
      // let lastTime = Date.now();

      function step() {
        // let timestamp = Date.now();
        // const dt = (timestamp - lastTime) / 1000; // 转换为秒

        //速度小于60就停止动画
        if(Math.abs(velocity) < 60) {
          return
        }

        // 每一帧移动的像素数
        const deltaY = velocity / 60;

        // 顶部回弹
        content.style.transform = `translateY(${ getTranslateY(content) + deltaY }px)`;
        let y = getTranslateY(content);
        if(y >= 60) {
          animate(500, easeOutQuad, (value) => {
            content.style.transform = `translateY(${ y - y * value }px)`;
          })
          return;
        }

        // 底部回弹
        if(y < 0 && Math.abs(y) > contentHeight - container.offsetHeight +60) {
          const h = contentHeight - container.offsetHeight;
          y = Math.abs(y) - (contentHeight- container.offsetHeight);
          animate(500, easeOutQuad, (value) => {
            content.style.transform = `translateY(-${ h + (y - y * value) }px)`;
          })
          return;
        }


        // container.scrollTop -= deltaY;

        // velocity = velocity * Math.pow(Math.E, -friction * dt)
        velocity = velocity * (1- 0.05)

        animationFrame = requestAnimationFrame(step);
      }

      requestAnimationFrame(step);
    }

    function getTranslateY(element) {
      const style = window.getComputedStyle(element);
      const transform = style.transform || style.webkitTransform || style.mozTransform;

      if (transform && transform !== 'none') {
        // transform值通常是 "matrix(a, b, c, d, e, f)",其中e和f是translateX和translateY的值
        const match = transform.match(/matrix\(([^,]+),[^,]+,[^,]+,([^,]+),([^,]+),([^,]+)\)/);
        if (match) {
          return parseFloat(match[4]);
        }
      }
      return 0; // 如果没有translateY值,默认返回0
    }


    function easeOutBack(x) {
      const c1 = 1.70158;
      const c3 = c1 + 1;
      return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
    }

    function easeOutQuad(x) {
      return 1 - (1 - x) * (1 - x);
    }

    function easeOutCirc(x) {
      return Math.sqrt(1 - Math.pow(x - 1, 2));
    }

    function animate(duration = 500, easeFunction, framer) {
      let startTime = null;
      function step() {
        time = Date.now();
        if (!startTime) startTime = time;
        let progress = (time - startTime) / duration;
        if (progress > 1) progress = 1;
        let value = easeFunction(progress);

        framer(value);

        if (progress < 1) {
          requestAnimationFrame(step);
        }
      }
      requestAnimationFrame(step);
    }
    
  </script>
</body>
</html>