在触屏设备上检测左滑或右滑,但允许上下滚动。

31

我需要检测并响应左/右滑动手势,但希望用户能够在同一元素上进行滚动,因此只要用户仅左/右移动手指且上/下最大移动距离不超过X像素,就不会发生滚动,但当超过X时,会发生滚动。

所以我做的是:

var startX, startY, $this = $(this);
function touchmove(event) {
        var touches = event.originalEvent.touches;
        if (touches && touches.length) {
            var deltaX = touches[0].pageX - startX;
            var deltaY = touches[0].pageY - startY;
            if (Math.abs(deltaY) > 50) {
                $this.html('X: ' + deltaX + '<br> Y: ' + deltaY + '<br>TRUE');
                $this.unbind('touchmove', touchmove);
                return true;
            } else {
                $this.html('X: ' + deltaX + '<br> Y: ' + deltaY);
                event.preventDefault();
            }
        }
    }

    function touchstart(event) {
        var touches = event.originalEvent.touches;
        if (touches && touches.length) {
            startX = touches[0].pageX;
            startY = touches[0].pageY;
            $this.bind('touchmove', touchmove);
        }
        //event.preventDefault();
    }

但是在"if"的情况下,它并没有恢复滚动的能力...

谢谢任何建议。


@ Raphael:你在任何地方声明它们吗? - T.J. Crowder
1
是的,在顶部添加了@T.J.Crowder。 - Raphael Jeger
5个回答

53

我编写了自己的触摸处理程序,也许能帮到你。

它检查以下内容:

快速点击:'fc'

向左滑动:'swl'

向右滑动:'swr'

向上滑动:'swu'

向下滑动:'swd'

每个检查都会初始化其相应的事件。但是您可以像平常一样滚动并执行其他任何操作。您只是拥有了一些新的事件。

如果您需要swl swr,则我还建议使用fc(快速单击)来进行单击事件… 它比普通单击快得多。

window.onload = function() {
    (function(d) {
        var
            ce = function(e, n) {
                var a = document.createEvent("CustomEvent");
                a.initCustomEvent(n, true, true, e.target);
                e.target.dispatchEvent(a);
                a = null;
                return false
            },
            nm = true,
            sp = {
                x: 0,
                y: 0
            },
            ep = {
                x: 0,
                y: 0
            },
            touch = {
                touchstart: function(e) {
                    sp = {
                        x: e.touches[0].pageX,
                        y: e.touches[0].pageY
                    }
                },
                touchmove: function(e) {
                    nm = false;
                    ep = {
                        x: e.touches[0].pageX,
                        y: e.touches[0].pageY
                    }
                },
                touchend: function(e) {
                    if (nm) {
                        ce(e, 'fc')
                    } else {
                        var x = ep.x - sp.x,
                            xr = Math.abs(x),
                            y = ep.y - sp.y,
                            yr = Math.abs(y);
                        if (Math.max(xr, yr) > 20) {
                            ce(e, (xr > yr ? (x < 0 ? 'swl' : 'swr') : (y < 0 ? 'swu' : 'swd')))
                        }
                    };
                    nm = true
                },
                touchcancel: function(e) {
                    nm = false
                }
            };
        for (var a in touch) {
            d.addEventListener(a, touch[a], false);
        }
    })(document);
    //EXAMPLE OF USE
    var h = function(e) {
        console.log(e.type, e)
    };
    document.body.addEventListener('fc', h, false); // 0-50ms vs 500ms with normal click
    document.body.addEventListener('swl', h, false);
    document.body.addEventListener('swr', h, false);
    document.body.addEventListener('swu', h, false);
    document.body.addEventListener('swd', h, false);
}
在这种情况下,h是我的处理程序,用于处理各种事件,并将处理程序添加到body中。
根据我理解您的问题,您只需要编写。
YOURELEMENT.addEventListener('swr',YOURSWIPERIGHTFUNCTION,false);
YOURELEMENT.addEventListener('swl',YOURSWIPELEFTFUNCTION,false);

只需添加一个处理程序即可处理多个元素和相同功能...

因此,如果您有

<ul id="ul"><li>1</li><li>2</li><li>3</li></ul>

你做什么:

var deleteli=function(e){
    var li=e.target;
    console.log('deleting '+li.textContent);
}
document.getElementById('ul').addEventListener('swl',deleteli,false);

fc和swr相同

iOS存在一个bug:不要使用alert(),否则它会执行两次。


很遗憾,这对我来说太复杂了 :/ 另一个解决方案是使用http://jgestures.codeplex.com/,在那里您可以在swipeleft、swiperight等上使用事件监听器。 - Jason Vasilev
它专门为低CPU设备设计,因为它防止了太多的检查和计算,并且仅使用您看到的短代码,无需添加第三方库,也不会覆盖设备的本机滚动系统,这可能使我的解决方案是最不卡顿的。函数本身在touchend上检查您是否向上、向下、向左或向右移动了x、y坐标,或者如果您没有移动(坐标在touchstart和touchend上相同)。在每种情况下,它都会创建一个新的自定义事件(方向或轻击,如果未移动)。所有计算都在touchend上完成。 - cocco
2
这是一个很棒的小片段,cocco,非常感谢!我在Android Chrome上遇到了一个问题:如果你不使用preventDefault(),Chrome会在touchmove事件开始后几乎立即触发touchcancel。为了解决这个问题而不破坏本地的touchmove功能,我检查了x和y轴上的移动距离。为此,请将您的代码中的touchmove更新为:touchmove:function(e){nm=false;ep={x:e.touches[0].pageX,y:e.touches[0].pageY};if(Math.abs(ep.x - sp.x) > 10 && Math.abs(ep.y - sp.y) < 20) e.preventDefault();}根据需要调整距离。 - Eric Fuller
touchmove函数不应该进行复杂的计算,以防止旧设备性能损失。如果删除整个touchcancel函数会发生什么?我没有安卓设备。 - cocco
http://jsfiddle.net/uRFcN/ <- 这里是我写的一个更新的函数...也可以用鼠标。试着玩一下,告诉我有没有错误。 - cocco
显示剩余27条评论

9
在被接受的答案中有一个“漏洞”。如果在Android上不使用Chrome浏览器而使用内置浏览器或“webview”(例如用于html5-hybrid-app),则无法检测到轻扫操作。
我发现事件没有触发,因为正常的滚动行为。因此,在touchmove中添加“e.preventDefault();”将修复它,或者采用Eric Fuller在被接受的答案中提供的修复方法。
这是一个不错的片段,但在移动WebApp或网站中,这可能会导致非常糟糕的滚动卡顿,因为始终会监视触摸事件。
因此,我决定构建一些新东西。它不像具有新事件侦听器那样舒适,但对我的需求来说足够舒适且性能良好。
function detectswipe(el,func) {
  swipe_det = new Object();
  swipe_det.sX = 0;
  swipe_det.sY = 0;
  swipe_det.eX = 0;
  swipe_det.eY = 0;
  var min_x = 20;  //min x swipe for horizontal swipe
  var max_x = 40;  //max x difference for vertical swipe
  var min_y = 40;  //min y swipe for vertical swipe
  var max_y = 50;  //max y difference for horizontal swipe
  var direc = "";
  ele = document.getElementById(el);
  ele.addEventListener('touchstart',function(e){
    var t = e.touches[0];
    swipe_det.sX = t.screenX; 
    swipe_det.sY = t.screenY;
  },false);
  ele.addEventListener('touchmove',function(e){
    e.preventDefault();
    var t = e.touches[0];
    swipe_det.eX = t.screenX; 
    swipe_det.eY = t.screenY;    
  },false);
  ele.addEventListener('touchend',function(e){
    //horizontal detection
    if ((((swipe_det.eX - min_x > swipe_det.sX) || (swipe_det.eX + min_x < swipe_det.sX)) && ((swipe_det.eY < swipe_det.sY + max_y) && (swipe_det.sY > swipe_det.eY - max_y)))) {
      if(swipe_det.eX > swipe_det.sX) direc = "r";
      else direc = "l";
    }
    //vertical detection
    if ((((swipe_det.eY - min_y > swipe_det.sY) || (swipe_det.eY + min_y < swipe_det.sY)) && ((swipe_det.eX < swipe_det.sX + max_x) && (swipe_det.sX > swipe_det.eX - max_x)))) {
      if(swipe_det.eY > swipe_det.sY) direc = "d";
      else direc = "u";
    }

    if (direc != "") {
      if(typeof func == 'function') func(el,direc);
    }
    direc = "";
  },false);  
}

myfunction(el,d) {
  alert("you swiped on element with id '"+el+"' to "+d+" direction");
}

要使用这个函数,只需像这样使用它:

detectswipe('an_element_id',myfunction);

detectswipe('an_other_element_id',my_other_function);

如果检测到滑动操作,将调用函数"myfunction"并传入参数element-id和"l,r,u,d"(left,right,up,down)。
示例: http://jsfiddle.net/rvuayqeo/1/

5
受 @cocco 启发,我创建了一个更好的(非最小化)版本:
(function(d) {
    // based on original source: https://dev59.com/yGMm5IYBdhLWcg3wfvHd#17567696
    var newEvent = function(e, name) {
        // This style is already deprecated but very well supported in real world: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
        // in future we want to use CustomEvent function: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
        var a = document.createEvent("CustomEvent");
        a.initCustomEvent(name, true, true, e.target);
        e.target.dispatchEvent(a);
        a = null;
        return false
    };
    var debug = false; // emit info to JS console for all touch events?
    var active = false; // flag to tell if touchend should complete the gesture
    var min_gesture_length = 20; // minimum gesture length in pixels
    var tolerance = 0.3; // value 0 means pixel perfect movement up or down/left or right is required, 0.5 or more means any diagonal will do, values between can be tweaked

    var sp = { x: 0, y: 0, px: 0, py: 0 }; // start point
    var ep = { x: 0, y: 0, px: 0, py: 0 }; // end point
    var touch = {
        touchstart: function(e) {
            active = true;
            t = e.touches[0];
            sp = { x: t.screenX, y: t.screenY, px: t.pageX, py: t.pageY };
            ep = sp; // make sure we have a sensible end poin in case next event is touchend
            debug && console.log("start", sp);
        },
        touchmove: function(e) {
            if (e.touches.length > 1) {
                active = false;
                debug && console.log("aborting gesture because multiple touches detected");
                return;
            }
            t = e.touches[0];
            ep = { x: t.screenX, y: t.screenY, px: t.pageX, py: t.pageY };
            debug && console.log("move", ep, sp);
        },
        touchend: function(e) {
            if (!active)
                return;
            debug && console.log("end", ep, sp);
            var dx = Math.abs(ep.x - sp.x);
            var dy = Math.abs(ep.y - sp.y);

            if (Math.max(dx, dy) < min_gesture_length) {
                debug && console.log("ignoring short gesture");
                return; // too short gesture, ignore
            }

            if (dy > dx && dx/dy < tolerance && Math.abs(sp.py - ep.py) > min_gesture_length) { // up or down, ignore if page scrolled with touch
                newEvent(e, (ep.y - sp.y < 0 ? 'gesture-up' : 'gesture-down'));
                //e.cancelable && e.preventDefault();
            }
            else if (dx > dy && dy/dx < tolerance && Math.abs(sp.px - ep.px) > min_gesture_length) { // left or right, ignore if page scrolled with touch
                newEvent(e, (ep.x - sp.x < 0 ? 'gesture-left' : 'gesture-right'));
                //e.cancelable && e.preventDefault();
            }
            else {
                debug && console.log("ignoring diagonal gesture or scrolled content");
            }
            active = false;
        },
        touchcancel: function(e) {
            debug && console.log("cancelling gesture");
            active = false;
        }
    };
    for (var a in touch) {
        d.addEventListener(a, touch[a], false);
        // TODO: MSIE touch support: https://github.com/CamHenlin/TouchPolyfill
    }
})(window.document);

与@cocco原始版本相比,重要更改如下:

  • 使用 event.touches[0].screenX/screenY 作为主要信息来源。因为如果页面的某些部分随着触摸滚动,它也会影响到 pageX/pageY 的值,所以 pageX/pageY 属性不正确地表示触摸在屏幕上的移动。
  • 添加最小手势长度设置
  • 添加容差设置以忽略近似对角线手势
  • 如果页面内容随着手势滚动,则忽略手势(在触发手势之前检查 pageX/pageY 中的差异)
  • 如果在手势期间进行多次触摸,则终止手势

未来需要完成的事项:

  • 使用 CustomEvent() 函数接口 替换 createEvent() 方法。
  • 添加 MSIE 兼容性
  • 也许可以将 pageX/pageY 的最小手势长度配置单独设置为 screenX/screenY
  • 如果触摸移动过快,Chrome 的线程滚动仍然会导致一些滚动检测问题。在决定是否触发事件之前,或许可以等待下一帧再决定滚动的位置?

使用方法如下:

document.body.addEventListener('gesture-right', function (e) {  ... });

或者jQuery风格
$("article").on("gesture-down", function (e) { ... });

赞成您的努力和成果的简洁性,做得很好。 - Shyam Tayal

3
所有这些代码都需要改进(就像你可以在触摸操作中找到的大多数代码一样)。当使用触摸事件时,请记住用户可能会用多个手指,触摸有一个标识符,而touches列表表示表面上所有当前的触摸,甚至是未移动的触摸。因此,流程相对简单:
  1. ontouchstart:获取第一个更改的触摸(不是event.originalEvent.touches属性,而是event.originalEvent.changedTouches属性)。使用event.originalEvent.changedTouches[0].identifier注册其标识符,并注册要查找的触摸属性(pageX/pageYclientX/clientY,这些属性与DOMElement.getBoundingClientRect()方法结合使用非常有用);

  2. ontouchmove: 确保当前触摸在changedTouches列表中与event.originalEvent.changedTouches.identifiedTouch(identifier)一起使用。如果它没有返回任何内容,则意味着用户移动了另一个触摸(不是您正在查找的那一个)。还要注册要查找的触摸属性并执行所需操作。

  3. ontouchend: 再次,您必须确保当前触摸在changedTouches列表中。执行要完成的操作,最后丢弃当前触摸标识符。

如果您想要更加强大,可以考虑观察多个触摸而不仅仅是一个。有关TouchEvent、TouchList和Touch的更多信息,请参见: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Touch_events

1
在触摸仍在移动时检测左右方向。 这是通过保存上一个位置并使用超时来擦除触摸移动停止后的最后一个位置来完成的。
var currentX;
var lastX = 0;
var lastT;
$(document).bind('touchmove', function(e) {
    // If still moving clear last setTimeout
    clearTimeout(lastT);

    currentX = e.originalEvent.touches[0].clientX;

    // After stoping or first moving
    if(lastX == 0) {
        lastX = currentX;
    }

    if(currentX < lastX) {
        // Left
    } else if(currentX > lastX){
        // Right
    }

    // Save last position
    lastX = currentX;

    // Check if moving is done
    lastT = setTimeout(function() {
        lastX = 0;
    }, 100);
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接