微交互设计实战提升用户体验的细节优化

素玲 Dev 优化 阅读 1,047
赞 14 收藏
二维码
手机扫码查看
反馈

又踩坑了,touchmove滚动失效

今天在搞一个移动端的滑动卡片组件,加了个简单的微交互:手指滑动时,卡片跟着偏移,松手后判断滑动距离决定是回弹还是滑走。逻辑不复杂对吧?结果一上手就翻车了。

微交互设计实战提升用户体验的细节优化

问题出在安卓机上,iOS还好,但一到某些安卓浏览器(特别是微信内置的那种),整个页面的滚动直接卡死了,手指划下去没反应,上下滑都废了。我第一反应是:是不是我把 touchmove 阻止得太狠了?

查了一下代码,果然,我在 touchmove 里直接来了个 e.preventDefault(),想着防止默认行为干扰滑动效果。但这一下子就把整个页面的滚动也给干掉了。这里我踩了个坑:不是所有 touchmove 都该阻止,默认滚动得让人家滚。

三种方案对比,我选了最简单的

后来试了下发现,其实社区里早就有成熟的解法。常见的有三种:

  • 方案一:用 CSS touch-action: pan-y,让垂直方向保留原生滚动
  • 方案二:在 JS 中动态判断滑动方向,只在水平滑动时 preventDefault
  • 方案三:完全不用 touch 事件,改用 Pointer Events

我一开始想上方案三,觉得高大上,结果发现兼容性有点悬,尤其低端安卓机支持不好,还得加 polyfill,太重了。方案二听着靠谱,但写起来容易出 bug,比如刚开始滑动的时候方向判断不准,抖动一下就误判成水平滑了,然后页面就不能滚了,用户体验更糟。

最后我还是回到方案一,简单粗暴但有效。核心就是一句话:

.swipe-card {
  touch-action: pan-y;
}

这样一来,系统就知道这个元素你主要用来做水平滑动(pan-x),垂直方向请保留给页面滚动。完美避开手动阻止事件带来的副作用。

核心代码就这几行

当然光靠 CSS 不够,JS 还是要配合一下,不然滑动反馈还是不跟手。我的做法是监听 touchstarttouchmovetouchend,但在 move 阶段不再无脑阻止事件,而是让浏览器自己处理。

完整实现如下:

const card = document.querySelector('.swipe-card');
let isSwiping = false;
let startX, currentX;

card.addEventListener('touchstart', (e) => {
  startX = e.touches[0].clientX;
  isSwiping = true;
});

card.addEventListener('touchmove', (e) => {
  if (!isSwiping) return;

  currentX = e.touches[0].clientX;
  const diff = currentX - startX;

  // 只在水平位移明显时才应用 transform
  // 避免轻微抖动影响体验
  if (Math.abs(diff) > 5) {
    card.style.transform = translateX(${diff}px);
    card.style.transition = 'none'; // 关闭过渡,保持跟随
  }
});

card.addEventListener('touchend', () => {
  if (!isSwiping) return;

  const diff = currentX - startX;
  const threshold = 100; // 滑动阈值

  if (Math.abs(diff) > threshold) {
    card.style.transform = translateX(${diff > 0 ? 300 : -300}px);
    card.style.transition = 'transform 0.3s ease';
    
    // 动画结束后触发删除或跳转
    setTimeout(() => {
      card.remove();
    }, 300);
  } else {
    // 回弹
    card.style.transform = '';
    card.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
  }

  isSwiping = false;
});

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

折腾了半天发现,有几个细节特别容易出问题:

  • transition 别忘了关:在 touchmove 期间一定要把 transition 设为 none,否则每次位移都会有动画延迟,看起来卡顿不跟手。这点很多人忽略,包括我第一次写的时候。
  • 阈值判断要宽松点:一开始我把滑动判定设得太敏感,稍微一碰就算 swipe,结果用户想看文字时不小心划了一下卡片飞了,很恼火。后来改成必须超过 100px 才触发移除,体验好多了。
  • 别在 touchmove 里做太多计算:之前我在 move 里加了惯性速度估算,结果低端机直接掉帧。现在只做位移更新,其他放到 end 阶段再算,性能稳多了。

fetch 接口示例顺带写个

这个卡片滑走之后会标记为“已读”,所以需要调接口。虽然和微交互关系不大,但顺便贴一下请求部分怎么写的:

async function markAsRead(cardId) {
  try {
    const response = await fetch('https://jztheme.com/api/cards/read', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ id: cardId })
    });

    if (!response.ok) throw new Error('Network error');
  } catch (err) {
    console.warn('标记失败,稍后重试', err);
    // 失败也不阻塞 UI,可考虑本地缓存状态
  }
}

上面这个函数会在卡片滑出后调用,失败了也不会弹 Toast,太打扰用户。毕竟只是个微操作,别因为网络抖动破坏体验。

改完后还有一两个小问题,但无大碍

目前唯一的瑕疵是:在快速连续滑动几张卡片时,偶尔会出现某张卡没触发 remove 的情况。排查发现是因为 transition 没结束就执行了 remove,导致 DOM 被删了但动画卡住。临时方案是在 setTimeout 里加点冗余时间,比如 400ms,比动画时间多 100ms 容错。

更好的做法应该是监听 transitionend 事件,但要考虑取消滑动时的清理,逻辑更复杂。现阶段这个小概率问题可以接受,先上线观察。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流

说实话,这种交互看着简单,真要做丝滑还挺考验细节把控的。尤其是不同机型、不同浏览器之间的差异,有时候你以为 ok 的写法,在别人手机上就是不灵。

这次最大的收获是:别急着用 JS 把控制权全抢过来,先看看 CSS 能不能帮你解决问题。touch-action 这个属性以前一直忽略,现在发现它才是移动端手势处理的第一道防线。

另外,微交互的核心不是炫技,而是让用户感觉“这玩意儿本该如此”。做得好没人注意,做砸了立马被骂,真是吃力不讨好的活儿……但谁让我们是前端呢。

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

暂无评论