给你的H5页面加上惯性滚动吧!
前端精髓
共 8588字,需浏览 18分钟
·
2024-04-11 17:47
在移动端,如果你使用过 overflow: scroll 生成一个滚动容器,会发现它的滚动是比较卡顿,呆滞的。为什么会出现这种情况呢?
因为我们早已习惯了目前的主流操作系统和浏览器视窗的滚动体验,比如滚动到边缘会有回弹,手指停止滑动以后还会按惯性继续滚动一会,手指快速滑动时页面也会快速滚动。而这种原生滚动容器却没有,就会让人感到卡顿。
首先,让我们来看一下它是怎样让滚动更流畅的吧。
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</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, // touchStart 手势 y 坐标
startY: 0, // touchStart 元素 y 偏移值
offsetY: 0, // 元素实时 y 偏移值
startTime: 0, // 惯性滑动范围内的 startTime
momentumStartY: 0, // 惯性滑动范围内的 startY
momentumTimeThreshold: 300, // 惯性滑动的启动 时间阈值
momentumYThreshold: 15, // 惯性滑动的启动 距离阈值
isStarted: false, // start锁
};
},
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;
// 回弹的最大限度
const maxOverflowY = this.wrapperHeight / 6;
let overflowY;
const distance = current - start;
const speed = (2 * Math.abs(distance)) / duration;
let destination =
current + (speed / deceleration) * (distance < 0 ? -1 : 1);
if (destination < this.minY) {
overflowY = this.minY - destination;
type =
overflowY > bounceThreshold ? "strongBounce" : "weekBounce";
destination = Math.max(
this.minY - maxOverflowY,
this.minY - overflowY / bounceRate
);
} else if (destination > this.maxY) {
overflowY = destination - this.maxY;
type =
overflowY > bounceThreshold ? "strongBounce" : "weekBounce";
destination = Math.min(
this.maxY + maxOverflowY,
this.maxY + overflowY / bounceRate
);
}
return {
destination,
duration: durationMap[type],
bezier: bezierMap[type],
};
},
// 超出边界时需要重置位置
isNeedReset() {
let offsetY;
if (this.offsetY < this.minY) {
offsetY = this.minY;
} else if (this.offsetY > this.maxY) {
offsetY = this.maxY;
}
if (typeof offsetY !== "undefined") {
this.offsetY = offsetY;
this.duration = 500;
this.bezier = "cubic-bezier(.165, .84, .44, 1)";
return true;
}
return false;
},
// 停止滚动
stop() {
const matrix = window
.getComputedStyle(this.$refs.scroller)
.getPropertyValue("transform");
this.offsetY = Math.round(+matrix.split(")")[0].split(", ")[5]);
},
},
});
</script>
</body>
</html>
可以发现,在增加惯性滚动,边缘回弹等效果之后,明显流畅、舒服了很多。那么,这些效果是怎么实现的呢?在用户滑动操作结束时,还会继续惯性滚动一段。首先看一下源码中的函数,这是 touchend 事件的处理函数,也就是用户滚动操作结束时的逻辑。
评论