移动端手势冲突的那些坑我帮你踩过了

迷人的颖萓 优化 阅读 1,386
赞 21 收藏
二维码
手机扫码查看
反馈

说说手势冲突这事

最近在做一个移动端H5页面,碰到了经典的手势冲突问题。滑动列表的时候偶尔会触发底部轮播图的左右滑动,用户体验简直灾难。这个问题其实挺常见的,今天就来对比一下几种常用的解决思路。

移动端手势冲突的那些坑我帮你踩过了

我比较喜欢用阻止默认行为的方式,简单粗暴,但确实有效。不过有些场景下还是得考虑兼容性和体验,所以这里把几种主流方案都列出来,顺便说说各自的坑。

阻止事件冒泡和默认行为

这是最直接的方案,核心思想就是在某个手势操作期间,阻止其他元素的手势事件。比如列表滚动的时候,阻止父级容器的touchmove事件。

// 列表容器
const listContainer = document.querySelector('.list-container');

let isScrolling = false;

listContainer.addEventListener('touchstart', (e) => {
    isScrolling = true;
});

listContainer.addEventListener('touchmove', (e) => {
    if (isScrolling) {
        e.preventDefault(); // 阻止默认滚动行为
        e.stopPropagation(); // 阻止事件冒泡
    }
});

listContainer.addEventListener('touchend', (e) => {
    isScrolling = false;
});

这个方案的优点很明显:简单直接,控制粒度细。但是缺点也明显,就是可能会干扰正常的交互,比如快速滚动的时候可能会卡顿。而且如果页面层级复杂,event.stopPropagation()的控制范围很难把握。

防抖和节流策略

这种方式是从时间维度来避免冲突。核心思路是,在某个时间段内只允许一种手势生效。我之前在做视频播放器的手势控制时经常用这种方法。

let scrollTimer = null;
let lastTouchTime = 0;

function handleListScroll(e) {
    const now = Date.now();
    
    // 如果距离上次触摸时间太短,忽略
    if (now - lastTouchTime < 150) {
        return;
    }
    
    lastTouchTime = now;
    
    // 清除之前的定时器
    if (scrollTimer) {
        clearTimeout(scrollTimer);
    }
    
    // 标记当前正在滚动
    listContainer.classList.add('scrolling');
    
    // 延迟移除标记,给手势一个稳定期
    scrollTimer = setTimeout(() => {
        listContainer.classList.remove('scrolling');
    }, 200);
}

// CSS配合
// .list-container.scrolling + .carousel {
//     pointer-events: none; /* 暂时禁用轮播图的触摸事件 */
// }

这种方式的好处是比较温和,不会完全阻断用户操作。但是需要注意的是,时间参数的设置很关键,太短了起不到作用,太长了会影响用户体验。我在实际项目中一般设为150-200ms,这个值可以根据具体场景调整。

方向识别判断

这是我觉得相对优雅的方案,通过判断手势的方向来决定优先级。比如垂直滑动优先处理列表滚动,水平滑动优先处理轮播图切换。

class GestureHandler {
    constructor(element) {
        this.element = element;
        this.startX = 0;
        this.startY = 0;
        this.isVerticalGesture = false;
        this.isHorizontalGesture = false;
        
        this.init();
    }
    
    init() {
        this.element.addEventListener('touchstart', this.handleTouchStart.bind(this));
        this.element.addEventListener('touchmove', this.handleTouchMove.bind(this));
        this.element.addEventListener('touchend', this.handleTouchEnd.bind(this));
    }
    
    handleTouchStart(e) {
        const touch = e.touches[0];
        this.startX = touch.clientX;
        this.startY = touch.clientY;
        this.isVerticalGesture = false;
        this.isHorizontalGesture = false;
    }
    
    handleTouchMove(e) {
        if (e.touches.length > 1) return; // 多指操作不处理
        
        const touch = e.touches[0];
        const deltaX = Math.abs(touch.clientX - this.startX);
        const deltaY = Math.abs(touch.clientY - this.startY);
        
        // 位移量达到阈值才确定手势方向
        if (!this.isVerticalGesture && !this.isHorizontalGesture) {
            if (deltaX > 10 && deltaX > deltaY) {
                this.isHorizontalGesture = true;
                // 触发水平手势锁定
                this.lockHorizontalGesture();
            } else if (deltaY > 10 && deltaY > deltaX) {
                this.isVerticalGesture = true;
                // 触发垂直手势锁定
                this.lockVerticalGesture();
            }
        }
        
        // 根据手势方向决定是否阻止事件
        if (this.isVerticalGesture) {
            e.preventDefault(); // 列表滚动,阻止轮播图手势
        } else if (this.isHorizontalGesture) {
            // 轮播图手势,可能需要阻止列表滚动
            // 这里可以根据具体需求决定
        }
    }
    
    handleTouchEnd(e) {
        this.isVerticalGesture = false;
        this.isHorizontalGesture = false;
        this.unlockGestures();
    }
    
    lockVerticalGesture() {
        this.element.style.touchAction = 'pan-y'; // 只允许垂直滚动
    }
    
    lockHorizontalGesture() {
        this.element.style.touchAction = 'pan-x'; // 只允许水平滚动
    }
    
    unlockGestures() {
        this.element.style.touchAction = 'auto';
    }
}

这里用了一个类来封装手势处理逻辑,主要是为了方便复用。这个方案的关键在于阈值的设置,我一般用10px作为初始判断阈值,这样可以避免轻微的误触导致的手势判断错误。

CSS touch-action 属性

这个方案相对简单,通过CSS属性来控制触摸行为。现代浏览器支持还不错,不过要考虑兼容性问题。

/* 列表容器,只允许垂直滚动 */
.list-container {
    touch-action: pan-y;
}

/* 轮播图容器,只允许水平滚动 */
.carousel-container {
    touch-action: pan-x;
}

/* 完全禁用触摸行为 */
.no-touch {
    touch-action: none;
}

/* 默认触摸行为 */
.default-touch {
    touch-action: auto;
}

这个方案的优点是纯CSS控制,性能比较好。但是缺点也很明显,就是控制粒度不够细,无法动态改变触摸行为。而且在一些老版本Android浏览器上支持不好。

谁更灵活?谁更省事?

从我的实践经验来看,方向识别判断是最推荐的方案。虽然代码稍微复杂一点,但是用户体验最好,也不会完全阻断用户的手势操作。特别是对于复杂的交互场景,这种方式最灵活。

如果你的项目时间紧,或者交互比较简单,直接用阻止默认行为的方式也能解决问题。但是我建议至少要加个节流机制,避免频繁触发。

至于CSS touch-action,这个更适合静态场景,比如整个页面只需要单一方向滚动的时候用。动态切换触摸行为的需求,还是得靠JS来控制。

踩坑提醒:这三点一定注意

第一点,event.preventDefault()一定要谨慎使用。特别是在iOS Safari上,滥用会导致整个页面都无法滚动的问题。我之前踩过这个坑,调试了好久才发现。

第二点,手势方向判断的阈值不要设置得太小,否则用户轻微的手指偏移就会导致手势判断错误。10px是个不错的起始值,可以根据实际情况微调。

第三点,touchAction属性的动态修改在某些设备上会有延迟,建议结合CSS类名的方式来控制,而不是直接修改style属性。

我的选型逻辑

简单场景直接用CSS touch-action,比如只需要控制页面整体的滚动方向。复杂交互场景我一般选择方向识别判断的方案,虽然代码多点,但是可控性强,用户体验也好。

对于临时性的快速修复,我会考虑阻止默认行为的方式,但是一定要配合节流机制,并且测试好各种边界情况。

以上是我个人对这个手势冲突问题的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论