移动端模拟滚动
当我们实现滚动列表的时候,我们往往会采用如下方案:外部一个固定高度的盒子,作为可视区域,内部则是高度由内容撑开的列表盒子,加上overflow:auto的配合,便实现了滚动效果。
但在最近一个项目中,我在移动端采用以上的方式实现滚动,安卓端表现正常,放到IOS端自带了橡皮筋效果,我看着这效果倒也能接受,奈何产品不能容忍这种现象,只能另辟蹊径。
首先,我尝试了在滚动到列表边界的时候在touchmove事件里阻止了默认行为,但由于项目的列表不止是上下滑动,还能左右滑动,导致一方到达边界后另一个方向也不能滑动。
然后,我又参考了网上推荐的inobounce这个库,但使用了之后依然不理想,看了它的代码实现后,发现它的实现和我之前的原理类似,也是边界判断,自然达不到要求。
在我尝试遍网上的方法后,我放弃了,做出了一个艰难的抉择:放弃浏览器的滚动,自己实现。由于看了inobounce的源码,发现里面很多东西可以复用,因此我打算在该库的基础上进行重构。 以下是我的实现:
class TouchMove {
constructor(el, options = {}) {
// 控制元素
this.el = el;
// 禁止滑动元素
this.banEl = options.banEl || [];
// 设置初始位置
this.lastTop = options.startTop || 0;
this.lastLeft = options.startLeft || 0;
this._startPoint = null;
this._handleStart = null;
this._handleMove = null;
this._handleEnd = null;
this.refreshX = this._refreshX;
this.refreshY = this._refreshY;
this.refresh = this._refresh;
this.setPosition = this._setPosition;
if (this._handleStart === null || this._handleMove === null || this._handleEnd === null) {
this._handleStart = this._touchstartEvent.bind(this, this);
this._handleMove = this._touchmoveEvent.bind(this, this, el);
this._handleEnd = this._touchendEvent.bind(this, this, el);
}
el.addEventListener('touchstart', this._handleStart);
window.addEventListener('touchmove', this._handleMove, {
passive: false,
});
window.addEventListener('touchend', this._handleEnd);
}
_getPoint(e) {
// 获取手指位置
return {
x: e.touches ? e.touches[0].pageX : e.clientX,
y: e.touches ? e.touches[0].pageY : e.clientY,
};
}
_preventDefault(e) {
// 在可以阻止的时候阻止滚动行为
e.preventDefault();
}
// 刷新X轴
_refreshX() {
this.lastLeft = 0;
}
// 刷新Y轴
_refreshY() {
this.lastTop = 0;
}
// 刷新所有
_refresh() {
this.lastLeft = 0;
this.lastTop = 0;
}
// 设置初始滑动weizhi
_setPosition(x, y) {
if (x) {
this.lastLeft = x;
}
if (y) {
this.lastTop = y;
}
}
// 判断元素是否为某元素的子元素或或是同一个元素
_isParent(obj, parentObj) {
if (obj == parentObj) {
return true;
}
while (obj != undefined && obj != null && obj.tagName.toUpperCase() != 'BODY') {
if (obj == parentObj) {
return true;
}
obj = obj.parentNode;
}
return false;
}
_touchstartEvent(context, e) {
context._startPoint = context._getPoint(e);
}
_touchmoveEvent(context, el, e) {
if (context.banEl.length > 0) {
// 如果触摸元素为被禁止元素的子元素或是同一个元素,则禁止滑动
for (var i = 0; i < context.banEl.length; i++) {
if (context._isParent(e.target, context.banEl[i])) {
return;
}
}
}
if (!context._startPoint) return void 0;
const curPoint = context._getPoint(e); // 当前点
const moveX = curPoint.x - context._startPoint.x; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
const moveY = curPoint.y - context._startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
el.scrollTop = context.lastTop - moveY;
el.scrollLeft = context.lastLeft - moveX;
}
_touchendEvent(context, el, e) {
context.lastTop = el.scrollTop;
context.lastLeft = el.scrollLeft;
context._startPoint = null;
}
}
export default TouchMove;
原理
监听触摸事件,调整元素位置。
- 特点:支持配置在滑动过程中禁止的元素;可设置初始位置
- 缺点:失去了原生滚动的惯性