<!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;
}
.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,
lastMoveY: 0,
originY: 0,
velocity: 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') {
const match = transform.match(/matrix\(([^,]+),[^,]+,[^,]+,([^,]+),([^,]+),([^,]+)\)/);
if (match) {
return parseFloat(match[4]);
}
}
return 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),
originY: 0,
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, originY, scrollableHeight } = this.data;
const { deci, easeOutQuad } = Inertia.utils;
const touch = e.touches[0];
if (!isDragging || lastMoveY - touch.clientY == 0) return;
const dy = touch.clientY - startY;
const y = scrollY + dy;
direction = this.data.direction = Math.abs(dy)/dy;
originY = this.data.originY = direction == 1 ? 0 : scrollableHeight;
if(y > 0 || (y < -scrollableHeight && direction == -1)) {
const disY = direction == 1 ? dy : Math.abs(y) - scrollableHeight;
const realDisY = easeOutQuad(Math.abs(disY * 0.1) / maxDistance()) * maxDistance();
content.style.transform = `translateY(${ direction * (originY + realDisY) }px)`;
}else {
content.style.transform = `translateY(${ y }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, scrollableHeight, direction, originY } = this.data;
const { max } = this.options;
const { getTranslateY, easeOutQuad } = Inertia.utils;
if (!isDragging) {return}
this.data.isDragging = false;
let y = this.data.scrollY = getTranslateY(content);
if(e.timeStamp - lastTimestamp >= 300) {
if(y > 0 || y < -scrollableHeight) {
y < 0 && (y = Math.abs(y) - scrollableHeight);
this.transition(400, easeOutQuad, (value) => {
content.style.transform = `translateY(${direction * (originY + (y - y * value)) }px)`;
})
}
return;
}
console.log('开启惯性滚动')
this.animateInertia();
}, { passive: false })
}
animateInertia() {
const { getTranslateY, deci, easeInOutCubic } = Inertia.utils;
const self = this;
const { content, scrollableHeight, direction, originY } = this.data;
const { max } = this.options;
function step() {
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(400, easeInOutCubic, (value) => {
console.log('originY', originY)
content.style.transform = `translateY(${direction * (originY + (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 < 100; i++) {
c += `<div class="item" style="${ i < 2 && 'margin-top:10px' }">${i+1}</div>`;
}
resolve(c);
})
}
});
</script>
</body>
</html>
滑动阻尼案例