移动端模拟滚动

665 阅读2分钟

移动端模拟滚动

当我们实现滚动列表的时候,我们往往会采用如下方案:外部一个固定高度的盒子,作为可视区域,内部则是高度由内容撑开的列表盒子,加上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;

原理

监听触摸事件,调整元素位置。

  • 特点:支持配置在滑动过程中禁止的元素;可设置初始位置
  • 缺点:失去了原生滚动的惯性