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

    

作者:前端精髓

https://blog.csdn.net/wu_xianqiang/article/details/136777572

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


推荐阅读  点击标题可跳转

1、前端项目路径别名终极解决方案

2、重磅更新!ECMAScript 2024 全新特性全面解析

3、纯前端怎么实现检测版本更新,请看这篇!

相关推荐

  • 不用 JS,轻松锁定页面滚动!
  • RAG中的Query改写思路之查询-文档对齐评分优化:兼看昨日大模型进展总结回顾
  • 统计学入门:时间序列分析基础知识详解
  • 李飞飞创业:3 个月估值破 10 亿美元
  • CVPR 2024 录用数据出炉!这几个方向爆火 。。。
  • 假开源真噱头?Meta再陷「开源」争议,LeCun被炮轰Meta只是开放模型
  • 清华提出时间序列大模型:面向通用时序分析的生成式Transformer | ICML 2024
  • xAI创立未足年,创始工程师Kosic离职重返老东家OpenAI,巨头人才之战热度升级
  • 「数据墙」迫近?苹果OpenAI等巨头走投无路,被迫「偷师」YouTube视频!
  • 奥特曼深夜发动价格战,GPT-4o mini暴跌99%!清华同济校友立功,GPT-3.5退役
  • 13个漂亮的登录页面,附源代码地址
  • 30s到0.8s,记录一次接口优化成功案例!
  • 45K*16薪,这波跳槽不亏。。。
  • 大模型知识机理与编辑专场 | 7月23日 19:00直播
  • 公理训练让LLM学会因果推理:6700万参数模型比肩万亿参数级GPT-4
  • 15 年功臣、英伟达首席科学家在股价巅峰期黯然辞职:手握大笔财富,但我为我的工作感到遗憾
  • 经五轮面试终于拿到微信的offer,却只能无奈放弃
  • Vue 组件管理的新趋势!以后可能不再需要组件库了?
  • 重磅推荐:一个开源的即时通讯应用 Tailchat
  • 两个各有特长的即时通讯开源项目