如何检测用户用JavaScript在网页上向某个方向滑动手指?

我想知道是否有一种解决方案可以同时适用于iPhone和Android手机上的网站。


当前回答

我重做了@ruben-martinez的答案,使用来自@givanse的惊人的解决方案,使用自定义反应钩子处理滑动事件。

import React, { useEffect, useRef, useState } from "react";

export default function useSwiper() {
  const [domRef, setDomRef] = useState<any>();

  const xDown: React.MutableRefObject<number | null> = useRef(null);
  const yDown: React.MutableRefObject<number | null> = useRef(null);

  useEffect(() => {
if (!domRef) return;

function getTouches(event: React.TouchEvent<HTMLDivElement>) {
  return event.touches;
}

function handleTouchStart(event: any) {
  const firstTouch = getTouches(event)[0];
  xDown.current = firstTouch.clientX;
  yDown.current = firstTouch.clientY;
}

function handleTouchMove(event: React.TouchEvent<HTMLDivElement>) {
  if (!xDown.current || !yDown.current) return;

  const firstTouch = getTouches(event)[0];
  const xUp = firstTouch.clientX;
  const yUp = firstTouch.clientY;

  const xDiff = xDown.current - xUp;
  const yDiff = yDown.current - yUp;

  if (Math.abs(xDiff) > Math.abs(yDiff)) {
    // handle horizontal swipes
    if (xDiff > 0) {
      // we swiped right
      console.log("right");
    } else {
      // we swiped left
      console.log("left");
    }
  } else {
    // handle vertical swipes
    if (yDiff > 0) {
      // we swiped down
      console.log("down");
    } else {
      // we swiped up
      console.log("up");
    }
  }
}

function handleTouchEnd(event: React.TouchEvent<HTMLDivElement>) {
  xDown.current = null;
  yDown.current = null;
}


  domRef.addEventListener("touchstart", handleTouchStart, false);
  domRef.addEventListener("touchmove", handleTouchMove, false);
  domRef.addEventListener("touchend", handleTouchEnd, false);

return () => {
    domRef.removeEventListener("touchstart", handleTouchStart, false);
    domRef.removeEventListener("touchmove", handleTouchMove, false);
    domRef.removeEventListener("touchend", handleTouchEnd, false);
};
  }, [domRef]);

  return (ref: any) => setDomRef(ref);
}

我在实现他的答案时遇到的主要挑战是不知道如何将swipe元素的ref绑定到自定义钩子中的ref。

基本上,所发生的是我们从自定义钩子返回一个函数。这个函数允许我们传入一个来自我们想要监听的滑动动作的元素的ref。自定义钩子接收到ref后,用元素的ref更新钩子状态,从而触发重新渲染,这样我们就有了实际的元素!

这种函数式的ref样式还允许我们对多个元素使用钩子。如下所示,我想使用它的项目列表,以启用滑动删除:)

import useSwiper from "./hooks/useSwipe";

const EntryCard = ({ entry, godMode, reload }: EntryProps) => {
const swiperRef = useSwiper();

const handleEntryClick =
(entry: Entry) => async (event: React.MouseEvent<HTMLDivElement>) => {
  if (!godMode) return;

  try {
    reload((state) => !state);
  } catch (err) {
    console.log("Error deleting entry: ", err);
  }
};

return (
  <div className="item" onClick={handleEntryClick(entry)} ref={swiperRef}>
    <div className="username">{entry.userName}</div>
    <div className="score">{entry.weekScore}</div>
  </div>
 );
};

PS:你可以把函数传递给钩子来接收滑动值。谢谢:)如果你喜欢,请投票:)

其他回答

首先使用鼠标事件原型实现它可能会更容易一些。

这里有很多答案,包括顶部,应该谨慎使用,因为它们不考虑边界情况,特别是在边界框周围。

See:

触摸事件 鼠标事件

你需要尝试捕捉边缘情况和行为,比如指针在结束前移动到元素之外。

滑动是一种非常基本的手势,它是一种更高级别的界面指针交互处理,大致介于处理原始事件和手写识别之间。

没有一种确切的方法来检测滑动或投掷,尽管几乎所有方法都遵循一个基本原则,即通过距离和速度或速度的阈值来检测元素的运动。你可以简单地说,如果在给定的时间内,在给定的方向上有65%的屏幕大小的移动,那么这就是一次滑动。具体在哪里划定界限以及如何计算取决于你自己。

有些人可能还会从某个方向的动量角度以及元素释放时它被推离屏幕的距离来看待它。这是更清晰的粘滞滑动,元素可以拖动,然后释放,要么反弹,要么飞出屏幕,就像弹性断裂。

最好是找到一个手势库,你可以移植或重用,通常用于一致性。这里的许多例子都过于简单,将滑动记录为任何方向上最轻微的触摸。

Android将是显而易见的选择,尽管它有相反的问题,它过于复杂。

许多人似乎把这个问题误解为某个方向的任何运动。滑动是一种面向单一方向的广泛且相对简短的运动(尽管可能是弧形的并具有一定的加速属性)。抛掷是类似的,但它是指在自身动量的作用下,随意地将物体推离一定距离。

这两者非常相似,以至于一些库可能只提供可以互换使用的fling或swipe。在平面屏幕上,很难真正区分这两种手势,一般来说,人们都在做这两种动作(滑动物理屏幕,但投掷屏幕上显示的UI元素)。

你最好的选择就是不要自己动手。目前已经有大量用于检测简单手势的JavaScript库。

我只想检测左右滑动,但只在触摸事件结束时触发动作,所以我稍微修改了@givanse的答案来实现这一点。

为什么要这么做?例如,在滑动时,用户注意到他最终不想滑动,他可以将手指移动到原来的位置(一个非常流行的“约会”手机应用程序可以做到这一点;)),然后“向右滑动”事件被取消。

因此,为了避免因为水平上有3px的差异而导致“向右滑动”事件,我添加了一个阈值,在该阈值下事件将被丢弃:为了有一个“向右滑动”事件,用户必须至少滑动浏览器宽度的1/3(当然,您可以修改这个)。

所有这些小细节都增强了用户体验。

请注意,目前,如果两个手指中的一个在缩放过程中做了一个大的水平移动,那么“触摸缩放”可能会被检测为滑动。

这是(Vanilla JS)代码:

var xDown = null, yDown = null, xUp = null, yUp = null;
document.addEventListener('touchstart', touchstart, false);        
document.addEventListener('touchmove', touchmove, false);
document.addEventListener('touchend', touchend, false);
function touchstart(evt) { const firstTouch = (evt.touches || evt.originalEvent.touches)[0]; xDown = firstTouch.clientX; yDown = firstTouch.clientY; }
function touchmove(evt) { if (!xDown || !yDown ) return; xUp = evt.touches[0].clientX; yUp = evt.touches[0].clientY; }
function touchend(evt) { 
    var xDiff = xUp - xDown, yDiff = yUp - yDown;
    if ((Math.abs(xDiff) > Math.abs(yDiff)) && (Math.abs(xDiff) > 0.33 * document.body.clientWidth)) { 
        if (xDiff < 0) 
            document.getElementById('leftnav').click();
        else
            document.getElementById('rightnav').click();
    } 
    xDown = null, yDown = null;
}

我之前使用的方法是,您必须检测mousedown事件,记录其x,y位置(任何相关的位置),然后检测mouseup事件,并减去两个值。

在这里的建议之上,我将跟踪手指号码,因为如果你同时用两个手指触摸,它将获得X位置,而不会导致一个奇怪的行为,而且,你可能想要设置一个“距离”最小值,这样用户在通过你的网站或应用程序触摸时就不会错误地触发滑动。

//Swipe 
let touchstartX = 0
let touchendX = 0
let fingerCount = 0
    
const checkDirection = () => {

  const distance = 50 //Minimum distance for the swipe to work

  //left
  if (touchendX < touchstartX && (touchstartX - touchendX) > distance ){

  //Do something cool
   
  } 
  //right
  if (touchendX > touchstartX && (touchendX - touchstartX) > distance){

    //Do something cooler
}

document.addEventListener('touchstart', e => {

    fingerCount = e.touches.length
    touchstartX = e.changedTouches[0].clientX  
     
})

document.addEventListener('touchend', e => {

    touchendX = e.changedTouches[0].clientX
    if(fingerCount === 1){ 
        checkDirection() 
    }

})

threshold, timeout swipe, swipeBlockElems添加。

document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchmove', handleTouchMove, false);
document.addEventListener('touchend', handleTouchEnd, false);     

const SWIPE_BLOCK_ELEMS = [
  'swipBlock',
  'handle',
  'drag-ruble'
]

let xDown = null;
let yDown = null; 
let xDiff = null;
let yDiff = null;
let timeDown = null;
const  TIME_THRESHOLD = 200;
const  DIFF_THRESHOLD = 130;

function handleTouchEnd() {

let timeDiff = Date.now() - timeDown; 
if (Math.abs(xDiff) > Math.abs(yDiff)) { /*most significant*/
  if (Math.abs(xDiff) > DIFF_THRESHOLD && timeDiff < TIME_THRESHOLD) {
    if (xDiff > 0) {
      // console.log(xDiff, TIME_THRESHOLD, DIFF_THRESHOLD)
      SWIPE_LEFT(LEFT) /* left swipe */
    } else {
      // console.log(xDiff)
      SWIPE_RIGHT(RIGHT) /* right swipe */
    }
  } else {
    console.log('swipeX trashhold')
  }
} else {
  if (Math.abs(yDiff) > DIFF_THRESHOLD && timeDiff < TIME_THRESHOLD) {
    if (yDiff > 0) {
      /* up swipe */
    } else {
      /* down swipe */
    }
  } else {
    console.log('swipeY trashhold')
  }
 }
 /* reset values */
 xDown = null;
 yDown = null;
 timeDown = null; 
}
function containsClassName (evntarget , classArr) {
 for (var i = classArr.length - 1; i >= 0; i--) {
   if( evntarget.classList.contains(classArr[i]) ) {
      return true;
    }
  }
}
function handleTouchStart(evt) {
  let touchStartTarget = evt.target;
  if( containsClassName(touchStartTarget, SWIPE_BLOCK_ELEMS) ) {
    return;
  }
  timeDown = Date.now()
  xDown = evt.touches[0].clientX;
  yDown = evt.touches[0].clientY;
  xDiff = 0;
  yDiff = 0;

}

function handleTouchMove(evt) {
  if (!xDown || !yDown) {
    return;
  }

  var xUp = evt.touches[0].clientX;
  var yUp = evt.touches[0].clientY;


  xDiff = xDown - xUp;
  yDiff = yDown - yUp;
}