滑动阻尼,惯性滚动列表,边界回弹,惯性回弹
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>