实现滑块拖动遇到的问题?

230 阅读2分钟

前言

最近修改了一个用户登录验证的组件,这个组件是一个滑块拼图组件,在 ios 移动端使用时发现滑动时整个页面会产生回弹效果,因此对这个组件进行了内部修改。

1.png

问题

在实现这类滑块时,一般会经历按下,移动和释放三个步骤,在组件的内部代码中通过 start(), move(), end() 来实现,并通过对应的事件进行绑定。组件是使用了 touch 和 mouse 事件来绑定这些函数,这样可以同时在 PC 和移动端使用。

比较奇怪的是组件绑定 move(), end() 这两个函数是绑定在 window 对象上的,这也解析了为什么会有页面的回弹效果,如果要取消回弹只需要在 body 元素上设置 touch-action: none 就可以了。

不过这样就有新的问题,为什么组件作者需要把事件绑定在 window 对象上呢?

移除 window 绑定

如果把 window 绑定移到滑块上,就会发现在移动端没有问题,但是在 PC 端当鼠标指针移出滑块时滑块就不再滑动了,很明显组件作者就是为了解决这个问题。但是这种解决方案其实不太好,一来会产生页面滑动的默认行为,而且假如有多个滑块还会产生冲突。

像这种移动目标脱离的问题,其实本身就有专门的 API 来解决,例如 input range 元素在 PC 端就可以脱离后滑动, 只需要使用元素的 setPointerCapture(pointerId) 就可以对特定的指针进行捕获,捕获后只要不释放那么事件的目标对象就不会变化,即使指针离开了目标元素。pointerId 来源于 PointerEvent 事件对象,PointerEvent 事件还有一个好处就是可以兼容移动端和 PC 端,这样就不用使用 touch 和 mouse 事件来分别定义。

demo

因为组件的业务代码太多了,所以我写了一个 demo 来简单展示滑块拖动的效果

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0, maximum-scale=2.0, minimum-scale=0.5">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta name="renderer" content="webkit">
  <title>index</title>
  <style>
    .wrapper {
      border: 1px solid #000;
    }
    body {
      margin: 0;
    }
    .bar {
      margin: auto;
      width: 300px;
      height: 30px;
      outline: 1px black solid;
      margin-top: 100px;
    }
    .block {
      background-color: red;
      height: 100%;
      aspect-ratio: 1;
      cursor: pointer;
      transform: translateX(var(--left));
      touch-action: none;
    }
  </style>
</head>
<body>
  <div class="bar">
    <div style="--left: 0;" onpointerdown="pointerdown(event)" onpointerup="pointerup(event)" class="block"></div>
  </div>
  <script>
    let prevClientX = 0
    const barEle = document.querySelector('.bar')
    const blockEle = document.querySelector('.block')
    function move(event) {
      const clientX = event.clientX
      const diff = clientX - prevClientX
      const left = parseInt(blockEle.style.getPropertyValue('--left'))
      const { width: barWidth } = barEle.getBoundingClientRect()
      const { width: blockWidth } = blockEle.getBoundingClientRect()
      let newLeft = left + diff
      if (newLeft < 0) {
        newLeft = 0
      }
      if (newLeft > (barWidth - blockWidth)) {
        newLeft = barWidth - blockWidth
      }
      blockEle.style.setProperty('--left', newLeft + 'px')
      prevClientX = clientX
    }
    function pointerdown (event) {
      prevClientX = event.clientX
      blockEle.addEventListener('pointermove', move)
      event.target.setPointerCapture(event.pointerId)
    }

    function pointerup(event) {
      blockEle.removeEventListener('pointermove', move)
      prevClientX = 0
      event.target.releasePointerCapture(event.pointerId)
    }
  </script>
</body>
</html>

这里还需要注意要特别设置滑块的样式为 touch-action: none; 不然移动端会触发 pointercancel 来强制取消滑块移动的监听,使用 touch-action 可以明确告知浏览器这个触摸行为的意图。具体原因可以查看这里 developer.mozilla.org/en-US/docs/…