给你的H5页面加上惯性滚动吧!

前端精髓

共 8588字,需浏览 18分钟

 · 2024-04-11


在移动端,如果你使用过 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 事件的处理函数,也就是用户滚动操作结束时的逻辑。


浏览 7
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报