滑动阻尼,惯性滚动列表,边界回弹,惯性回弹

https://juejin.cn/post/7426280686695759882

<!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>
    #container {
      width: 300px;
      height: 500px;
      overflow: hidden;
      border: 1px solid black;
      position: relative;
      box-sizing: border-box;
    }
    .inertia-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="container"></div>
  <script>
    class Inertia {
      options = {
        max: 20
      };
      data = {
        container: null,
        content: null,
        containerHeight: 0,
        contentHeight: 0,
        scrollableHeight: 0,
        maxDistance: () => this.data.containerHeight * 0.9,
        isDragging: false,
        startY: 0,
        scrollY: 0,
        velocity: 0,
        lastMoveY: 0,
        lastTimestamp: 0,
        animationFrame: null,
        direction: 0,
      };
      static utils = {
        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
        },
        easeOutQuad(x) {
          return 1 - (1 - x) * (1 - x);
        },
        easeInOutCubic(x) {
          return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
        },
        easeOutQuint(x) {
          return 1 - Math.pow(1 - x, 5);
        },
        iosEase(x) {
          return Math.sqrt(1 - Math.pow(1 - 2*x, 2));
        },
        deci(number) {
          return Math.round(number * 10) / 10
        }
      };
      constructor(container, options) {
        this.data.container = document.querySelector(container);
        Object.assign(this.options, options);
        this.init();
      }
      async init() {
        this.createdInertiaContentEl();
        await this.renderContent();
        this.bindEvents();
      }
      createdInertiaContentEl() {
        this.data.container.insertAdjacentHTML('afterbegin', `<div class="inertia-content"></div>`);
        this.data.content = container.querySelector('.inertia-content');
      }
      async renderContent() {
        const { container, content } = this.data;
        const data = await this.options.rendered();
        container.querySelector('.inertia-content').innerHTML = data;
        this.data.containerHeight = container.offsetHeight;
        this.data.contentHeight = content.offsetHeight;
        this.data.scrollableHeight = this.data.contentHeight - this.data.containerHeight;
      }
      bindEvents() {
        container.addEventListener('touchstart', (e) => {
          e.preventDefault();
          const { content, animationFrame } = this.data;
          const touch = e.touches[0];
          Object.assign(this.data, {
            isDragging: true,
            startY: touch.clientY,
            scrollY: Inertia.utils.getTranslateY(content),
            velocity: 0,
            lastMoveY: touch.clientY,
            lastTimestamp: e.timeStamp,
          });
          cancelAnimationFrame(animationFrame);
        }, { passive: false });

        document.addEventListener('touchmove', (e) => {
          e.preventDefault();
          let { isDragging, content, startY, maxDistance, direction, lastTimestamp, lastMoveY, scrollY, scrollableHeight } = this.data;
          if (!isDragging) return;
          const { deci, easeOutQuad } = Inertia.utils;
          const touch = e.touches[0];
          // 偏移量
          const dy = touch.clientY - startY;
          // 实时滚动距离
          const y = scrollY + dy;
          // 1 向下滚动, -1 向上滚动
          direction = this.data.direction = Math.abs(dy)/dy;
          // 当接触到边界时,增加阻尼感 
          if(y > 0 || (y < -scrollableHeight && direction == -1)) {
            //下拉置顶或者上拉置底时,滑动距离
            const disY = direction == 1 ? Math.abs(dy) : Math.abs(y) - scrollableHeight;
            // 0.2是阻尼系数
            const realDisY = direction * easeOutQuad(disY * 0.1 / maxDistance()) * maxDistance();
            // 从哪里开始,会有阻尼感的距离滑动
            const originY = direction == 1 ? 0 : -scrollableHeight;
            content.style.transform = `translateY(${ originY + realDisY }px)`;
          }else {
            content.style.transform = `translateY(${this.data.scrollY + dy}px)`;
          }
          const now = e.timeStamp;
          const dt = now - lastTimestamp;
          this.data.velocity = (touch.clientY - lastMoveY) / dt * 1000;
          this.data.lastMoveY = touch.clientY;
          this.data.lastTimestamp = now;
        }, { passive: false });

        document.addEventListener('touchend', (e) => {
          e.preventDefault();
          const { lastTimestamp, content, isDragging } = this.data;
          const { max } = this.options;
          const { getTranslateY, easeInOutCubic } = Inertia.utils;
          if (!isDragging) {return}
          this.data.isDragging = false;
          const y = this.data.scrollY = getTranslateY(content);

          // 下拉距离小于max 或者 停止时间超过300ms 或者 速度小于60 ,则自动回弹到顶部
          // if((y > 0 && y < max) || e.timeStamp - lastTimestamp >= 300 || Math.abs(this.data.velocity) < 1) {
          //   y > 0 && this.transition(400, easeInOutCubic, (value) => {
          //     content.style.transform = `translateY(${ y - y * value }px)`;
          //   })
          //   return
          // }
          console.log('开启惯性滚动')
          this.animateInertia();
        }, { passive: false })


      }
      animateInertia() {
        const { getTranslateY, deci, easeInOutCubic, iosEase } = Inertia.utils;
        const self = this;
        const { content, scrollableHeight } = this.data;
        const { max } = this.options;
        function step() {
          //速度小于60就停止动画
          if(Math.abs(self.data.velocity) < 1) { 
            self.data.scrollY = getTranslateY(content)
            return
          }
          // 每一帧移动的像素数
          const deltaY = deci(self.data.velocity / 60);
          let y = getTranslateY(content);
          // 顶部回弹
          if(y > max) {
            self.transition(400, easeInOutCubic, (value) => {
              content.style.transform = `translateY(${ y - y * value }px)`;
            })
            return;
          }
          // 底部回弹
          if(y < 0 && Math.abs(y) - scrollableHeight > max) {
            y = Math.abs(y) - scrollableHeight;
            self.transition(500, easeInOutCubic, (value) => {
              content.style.transform = `translateY(-${ scrollableHeight + (y - y * value) }px)`;
            })
            return;
          }
          content.style.transform = `translateY(${ y + deltaY }px)`;
          self.data.velocity *= (1- 0.03);
          self.data.animationFrame = requestAnimationFrame(step);
        }
        requestAnimationFrame(step);
      }
      transition(duration = 500, easeFunction, framer) {
        let startTime = null;
        function step() {
          let 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);
      }

    }

    // 使用
    new Inertia('#container', {
      rendered() {
        return new Promise((resolve) => {
          let c = ``;
          for(let i = 0; i < 1000; i++) {
            c += `<div class="item" style="${ i < 2 && 'margin-top:10px' }">${i+1}</div>`;
          }
          resolve(c);
        })
      }
    });

  </script>
</body>
</html>

 

posted @ 2024-10-17 15:07  littleboyck  阅读(17)  评论(0编辑  收藏  举报