长按交互实现原理与移动端适配实战经验

宇文玲玲 移动 阅读 2,669
赞 33 收藏
二维码
手机扫码查看
反馈

为啥要折腾长按方案?

做移动端交互时,长按(long press)是个高频需求:比如聊天消息长按弹出菜单、图片长按保存、列表项长按进入编辑模式。但浏览器原生不支持 longpress 事件,得自己实现。我试过好几种方案,每种都有坑,今天就聊聊我踩过的雷和最终怎么选的。

长按交互实现原理与移动端适配实战经验

方案一:纯 JS + setTimeout(最糙但最稳)

这是最直白的思路:用户按下时启动计时器,超过阈值(比如 500ms)就触发长按;如果中途松开或移动,就清掉定时器。代码简单到几乎不会出错:

function useLongPress(element, callback, delay = 500) {
  let timer = null;
  let isLongPress = false;

  const start = (e) => {
    e.preventDefault(); // 防止 iOS 弹出菜单
    timer = setTimeout(() => {
      isLongPress = true;
      callback(e);
    }, delay);
  };

  const clear = () => {
    if (timer) clearTimeout(timer);
    isLongPress = false;
  };

  element.addEventListener('touchstart', start);
  element.addEventListener('touchend', clear);
  element.addEventListener('touchmove', clear); // 移动也算取消
  element.addEventListener('touchcancel', clear);
}

我比较喜欢这个方案,因为它不依赖任何框架,兼容性极好。在低端安卓机上跑得飞起,也没啥奇怪的副作用。唯一要注意的是 preventDefault()——不加的话,iOS 上长按会触发系统菜单(比如复制、粘贴),直接把你的逻辑干掉。这里我踩过好几次坑,调试半天才发现是系统行为干扰。

方案二:用 Pointer Events(现代但有兼容性雷区)

Pointer Events 是 W3C 标准,理论上能统一处理鼠标、触摸、笔输入。写起来确实清爽:

function useLongPressPointer(element, callback, delay = 500) {
  let timer = null;

  element.addEventListener('pointerdown', (e) => {
    e.preventDefault();
    timer = setTimeout(() => callback(e), delay);
  });

  ['pointerup', 'pointercancel', 'pointerleave'].forEach(event => {
    element.addEventListener(event, () => {
      if (timer) clearTimeout(timer);
    });
  });
}

但问题来了:部分安卓机(尤其是老款三星)对 pointer events 支持稀烂。我之前在一个项目里用了这套,结果 QA 报告说“长按没反应”,查了半天发现是设备根本不触发 pointerdown。后来不得不回退到 touch 事件。所以除非你确定目标用户全是较新设备(比如企业内网应用),否则别轻易上 Pointer Events。省事?可能更费事。

方案三:用第三方库(比如 Hammer.js)

有人会说:“何必重复造轮子?直接用 Hammer.js 的 press 事件不香吗?” 代码确实短:

// 需要先引入 Hammer.js
const mc = new Hammer(element);
mc.add(new Hammer.Press({ time: 500 }));
mc.on('press', callback);

但现实很骨感。Hammer.js 已经多年不更新,体积还大(gzip 后 7KB+),而你的需求可能就一个长按。更糟的是,它内部用的还是 touch 事件,但封装太深,出了问题不好 debug。我曾经为了调一个“长按后滚动失效”的问题,翻了 Hammer 的源码两小时,最后发现是它阻止了默认滚动行为。这种黑盒体验,除非项目里 already 在用 Hammer(比如还要做 pinch、swipe 等复杂手势),否则真没必要为一个长按引入整个库。

谁更灵活?谁更省事?

回到实际开发场景:

  • 如果你要做简单长按(比如弹个菜单),我 100% 选方案一(setTimeout + touch events)。代码不到 20 行,可控性高,改起来快。
  • 如果你的项目已经重度依赖 Pointer Events(比如用 Fabric.js 画布),那方案二可以考虑,但务必做好降级。
  • 方案三(第三方库)?除非你同时需要多种手势,否则别碰。省下的那几行代码,后期维护成本可能翻倍。

另外,有个细节很多人忽略:长按期间是否允许页面滚动? 方案一里,我在 touchstart 加了 preventDefault(),这会直接锁死滚动。但有些场景下,用户可能边滚动边长按(比如长按列表项时手滑了),这时候你可能只在确认是长按后才阻止默认行为。调整起来也很简单:

// 修改版:只在长按触发后阻止默认
const start = (e) => {
  timer = setTimeout(() => {
    isLongPress = true;
    e.preventDefault(); // 这里才阻止
    callback(e);
  }, delay);
};

不过要注意,iOS Safari 对动态 preventDefault() 有限制,必须在 touch 事件的同步回调里调用才有效。所以稳妥起见,我还是习惯在 touchstartpreventDefault(),牺牲一点滚动体验换稳定性。

我的选型逻辑

总结一下我的决策树:

  1. 需求是不是只有长按? → 是 → 用方案一(原生 touch + setTimeout)
  2. 要不要支持鼠标(桌面端)? → 要 → 在方案一基础上加 mouse 事件监听(mousedown/mouseup)
  3. 项目里是否已用 Hammer 或其他手势库? → 是 → 直接用库的 press 事件
  4. 目标用户全是高端机? → 是 → 可以试试方案二(Pointer Events),但留好 fallback

实际项目中,90% 的情况我都会选方案一。它就像一把瑞士军刀——不 fancy,但关键时刻从不掉链子。上周我刚重构了一个老项目,把里面用 Hammer 实现的长按全换成原生方案,bundle size 小了 8KB,bug 还少了一半。

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

  • iOS 系统菜单干扰:务必在 touchstartpreventDefault(),否则长按可能触发“拷贝”弹窗。
  • touchmove 的判定:手指稍微移动就算取消长按?还是允许小幅移动?我一般只要检测到 touchmove 就清定时器,避免误触。如果需要容错,可以记录初始坐标,移动超过 10px 再取消。
  • 多次触发问题:确保每次 touchstart 前清掉旧 timer,否则快速连点可能触发多次长按。我的代码里 clear() 函数就是干这个的。

以上是我个人对长按方案的完整踩坑总结,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如长按+拖拽组合),后续会继续分享这类实战博客。

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

暂无评论