<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<div id="app">div>
<template id="tpl">
<div
class="wrapper"
ref="wrapper"
@touchstart.prevent="onStart"
@touchmove.prevent="onMove"
@touchend.prevent="onEnd"
@touchcancel.prevent="onEnd"
@mousedown.prevent="onStart"
@mousemove.prevent="onMove"
@mouseup.prevent="onEnd"
@mousecancel.prevent="onEnd"
@mouseleave.prevent="onEnd"
@transitionend="onTransitionEnd"
>
<ul class="list" ref="scroller" :style="scrollerStyle">
<li class="list-item" v-for="item in list">{{item}}li>
ul>
div>
template>
<style>
body,
ul {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.wrapper {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.list {
background-color: #70f3b7;
}
.list-item {
height: 40px;
line-height: 40px;
width: 100%;
text-align: center;
border-bottom: 1px solid #ccc;
}
style>
<script src="https://cdn.jsdelivr.net/npm/vue@2">script>
<script>
new Vue({
el: "#app",
template: "#tpl",
computed: {
list() {
let list = [];
for (let i = 0; i < 100; i++) {
list.push(i);
}
return list;
},
scrollerStyle() {
return {
transform: `translate3d(0, ${this.offsetY}px, 0)`,
"transition-duration": `${this.duration}ms`,
"transition-timing-function": this.bezier,
};
},
},
data() {
return {
minY: 0,
maxY: 0,
wrapperHeight: 0,
duration: 0,
bezier: "linear",
pointY: 0,
startY: 0,
offsetY: 0,
startTime: 0,
momentumStartY: 0,
momentumTimeThreshold: 300,
momentumYThreshold: 15,
isStarted: false,
};
},
mounted() {
this.$nextTick(() => {
this.wrapperHeight =
this.$refs.wrapper.getBoundingClientRect().height;
this.minY =
this.wrapperHeight -
this.$refs.scroller.getBoundingClientRect().height;
});
},
methods: {
onStart(e) {
const point = e.touches ? e.touches[0] : e;
this.isStarted = true;
this.duration = 0;
this.stop();
this.pointY = point.pageY;
this.momentumStartY = this.startY = this.offsetY;
this.startTime = new Date().getTime();
},
onMove(e) {
if (!this.isStarted) return;
const point = e.touches ? e.touches[0] : e;
const deltaY = point.pageY - this.pointY;
this.offsetY = Math.round(this.startY + deltaY);
const now = new Date().getTime();
if (now - this.startTime > this.momentumTimeThreshold) {
this.momentumStartY = this.offsetY;
this.startTime = now;
}
},
onEnd(e) {
if (!this.isStarted) return;
this.isStarted = false;
if (this.isNeedReset()) return;
const absDeltaY = Math.abs(this.offsetY - this.momentumStartY);
const duration = new Date().getTime() - this.startTime;
if (
duration < this.momentumTimeThreshold &&
absDeltaY > this.momentumYThreshold
) {
const momentum = this.momentum(
this.offsetY,
this.momentumStartY,
duration
);
this.offsetY = Math.round(momentum.destination);
this.duration = momentum.duration;
this.bezier = momentum.bezier;
}
},
onTransitionEnd() {
this.isNeedReset();
},
momentum(current, start, duration) {
const durationMap = {
noBounce: 2500,
weekBounce: 800,
strongBounce: 400,
};
const bezierMap = {
noBounce: "cubic-bezier(.17, .89, .45, 1)",
weekBounce: "cubic-bezier(.25, .46, .45, .94)",
strongBounce: "cubic-bezier(.25, .46, .45, .94)",
};
let type = "noBounce";
const deceleration = 0.003;
const bounceRate = 10;
const bounceThreshold = 300