Detect left/right-swipe on touch-devices, but allow up/down-scrolling
Asked Answered
H

5

31

I need to detect and react to left/right-swipes, but want to give the user the ability to scroll on the same element, so as long as he moves his finger only left/right with a maximum up/down movement of X pixels, it should not scroll, but when he exceeds X, it should scroll.

So what I did is:

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();
    }

But I doesn't restore the ability to scroll in the "if" case...

Thanks for any tips.

Heehaw answered 10/7, 2013 at 9:41 Comment(2)
@ Raphael: Do you declare them anywhere?Gayl
yes, added it on top @T.J.CrowderHeehaw
C
53

I wrote my own touch handler events.maybe this helps you

it checks for:

fast click : 'fc'

swipe left : 'swl'

swipe right : 'swr'

swipe up : 'swu'

swipe down : 'swd'

each check initializes it's correspondent event.but you can scroll and do whatever else you do normally. you just have some new events.

you need swl swr, I aslo suggest to use fc (fastclick) for click events... it's much faster than normal click.

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);
}

in this case h is my handler for every type of event and i add the handlers to the body.

for what i understand your question you just have to write

YOURELEMENT.addEventListener('swr',YOURSWIPERIGHTFUNCTION,false);
YOURELEMENT.addEventListener('swl',YOURSWIPELEFTFUNCTION,false);

to handle multiple elements and the same function... just add one handler.

so if you have

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

you do:

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

same for fc & swr

there is a bug in ios: don't use alert() .. it will execute 2 times.

Christianly answered 10/7, 2013 at 9:59 Comment(32)
Unfortunately this is too complicated for me to understand :/ Another solution is using jgestures.codeplex.com where you can use event listeners on swipeleft, swiperight and so onHorsefly
it's specifically designed for low cpu devices as it prevents to many checks and calculations and uses only the short code you see without the need to add third pary libraries and also does not overwrite the native scrolling system of the device which probably makes my solution the less lagging one.The functions itself checks on touchend if you moved up,down,left or right with the coordinates x,y or if you didn't moved(coordinates are the same on touchstart and touchend).in each case it creates a new custom event (the direction or a tap if not moved).all calculations are done at the touchendChristianly
This is a great little snippet you've posted, cocco, thanks so much! I did run into one problem with Android Chrome: if you don't preventDefault(), Chrome fires touchcancel almost immediately after a touchmove event starts. To fix this without killing native touchmove functionality, I check distance traversed on the x and y axes. To do this, update touchmove in your code to: 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();} Tweak distances as neededHenequen
the touchmove function should not have complex calculations to prevent performance loss on old devices.what happens if you remove the whole touchcancel function? i don't have android.Christianly
jsfiddle.net/uRFcN <- here is a newwer function i wrote ... works also with mouse.play with that and tell me if you get any erorrs.Christianly
Awesome! Little code, no extra query mobile necessary. Thanks so much!Audio
@Christianly thanks again for your great snippet. Do you use it yourself also? I ask because I lately recognized that it doesn’t work any longer on ios8 (ipad) in home screen mode. (in browser it does!) Someone with an idea why this happens?Audio
i haven't yet update my ipads... but i heard ios8 has even more bug then the previous verions regarding webapps(also fileuploading has several issues..)... anyway atm i can't test... you have a chance to get the console errors?Christianly
mobilexweb.com/blog/… 'Touch events inside iframes on home-screen apps are not being reported'Christianly
Cool I'm gonna use this...I'm gonna set bubbling to false on swipe events though. I can't imagine I would need bubbling for a swipe.Xenocryst
I took the above fiddle from cocco 'uRFcN' reformatted it and got rid of the (clever) hard to read comma expressions and renamed the variables jsfiddle.net/s2gb4jt3 thought it might be of interest to others. This version also works in Firefox the previous one accessed fields that ".x" and ".y" that do not exist on Firefox for mouse events. Note people should check Loops answer as well.Alienage
@cocco: Thanks! Would be so great to hear about this when you will have installed iOS8! Touch events seem not be reported also on my index page. :-( PS: no errors in the console log, it is just ignored completely. I frankly wonder why anyone may use home screen apps without touch events?Audio
Swipes with the method of cocco seem to work now after iOS8 update to 8.0.2 (at least on my home screen app). Maybe also due to some problems with cache refreshing, known difficulty on iOS <iOS8.0.2. Hope this is fixed now, also. Yet, as cocco mentioned still no swipe report from iframes!Audio
Can someone test this... i increased the performance by removing some extra unnecessary stuff and caching more variables... MOBILE ONLY jsfiddle.net/zgc81Lb8/5Christianly
@Christianly works very fine on my site! Thanks! Yet I have one situation where it doesn’t. But must be something with my code. Because also with with your version before it worked only on parts of the screen (strangely diagonal bordered). Now in this particual situation it does not work any longer at all. But for the rest brillant!Audio
Still found no solution for using it in my iframes on home screen iOS8. Anyone who found some workaround for that?Audio
@Christianly Sorry, solved my problem. Your script works awesome anywhere.Audio
@Christianly any insight why iphone4 (7.1) pinch to zoom in events propagate into your script as swr (and how to stop it)Andorra
if(e.touches.length>1)return; add this at the top of the touchmove and touchstart...Christianly
@Christianly was able to fold it a little smaller by making a recursive function for touches pastebin.com/raw.php?i=q3yLqVtM touchcancel could make b.z=9 instead to cancel the end?Andorra
@Christianly I've found an inherit flaw with this approach. Because all events are captured for touch on the elements specified, you cannot selectively cancel propagation during a move. This means for example on iphone if a page is zoomed in a bit and you swipe left or right, the action is passed to the page below right after the event is handled otherwise and it will pan. No easy way to solve this and it is a big problem.Andorra
I may add here, also that iframes on iOS8 don’t handle any kind of touch events. Great bug.Audio
@Andorra i made this for my ipad1 wich is very slow... and for other android devices that are slow. i made mine(func) not much smaller because of the touchmove function. touchmove is executed many times per second and where the lag could start. so i do the calculations outside touchmove.Also a function inside the touchmove slows it down a little. But for the rest it's ok.again this function is created for singlefinger adding multitouch is a little more complicated.so if you pinch i wouldn't add a swipe handler.btw stop the propagation or count the touches.Christianly
Same for mouse you could use one function to handle all touches & clicks, but that means alot of calculations/checks inside touchmove/mousemove... thats why i separate every functions. Adding support for multitouch means you need to use touchids and not touches[0] else you have no real control.And adding mouseevents would be much harder as that is no multitouch. this function is made for simple iteractions on different devices.Christianly
for modern multicore devices with good hardware acceleration you can do the checks inside mousemove/touchmove and create nice elastic/magnetic realtime swipes or just use iScroll...Christianly
i mean keep it simple/short/fast i think the final user is happier.Christianly
still no touch events in iframes in home screen use under iOS8.1Audio
still no touch events in iframes in home screen even under iOS 8.1.1Audio
Hi cocco! Still there? I use your brillant little swipe event plugIn. Thanks again and again. I wondered if it would be possible to integrate or add an let’s say “intermediate” event that will be fired while the user touches and moves before he executes the final swipe by leaving the finger (touchend) Only an idea. If I would try it, certainly it will become a mess. What do you think of the idea?Audio
I openend a question for that. Thanks for help! Please don’t down vote again :-( linkAudio
I gotta say this is still the very best answer from all custom solutions even in 2021, I was stuck with this for days. Thank you million times!Peso
It's really helpful for me. thanksJuanitajuanne
E
9

There is a "bug" in the accepted answer. If you don't use Chrome on Android but the build in browser or a "webview" (For a html5-hybrid-app) for example, then the swipe is not being detected.

I found out that the event doesn't fire, because of the normal scroll behavior. So adding "e.preventDefault();" in touchmove would fix it or the fix from Eric Fuller in the accepted answer.

It's a nice snipped but in a mobile WebApp or Website this could result in a bad scroll stuttering, because the touch-events are observed the whole time.

So I decided to build something new. It's not as comfortable like to have new event listeners, but it's comfortable enough for my needs and it's performat.

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");
}

To use the function just use it like

detectswipe('an_element_id',myfunction);

detectswipe('an_other_element_id',my_other_function);

If a swipe is detected the function "myfunction" is called with parameter element-id and "l,r,u,d" (left,right,up,down).

Example: http://jsfiddle.net/rvuayqeo/1/

Equinoctial answered 24/11, 2014 at 21:44 Comment(0)
E
5

Inspired by @cocco I created a better (non-minimized) version:

(function(d) {
    // based on original source: https://mcmap.net/q/460796/-detect-left-right-swipe-on-touch-devices-but-allow-up-down-scrolling
    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);

Important changes compared to original version by @cocco:

  • use event.touches[0].screenX/screenY as the major source of information. The pageX/pageY properties do not correctly represent the movement of touches on screen because if some piece of page scrolls with the touch, it affects the pageX/pageY values, too.
  • add minimum gesture length setting
  • add tolerance setting for ignoring near diagonal gestures
  • ignore the gesture if page content has scrolled with the gesture (inspect difference in pageX/pageY before triggering gesture)
  • abort gesture if multiple touches are done during the gesture

Things that would need to be done in the future:

  • use CustomEvent() function interface instead of createEvent() method.
  • add MSIE compatibility
  • maybe configure minimum gesture length for pageX/pageY separate from screenX/screenY?
  • It seems that Chrome's threaded scrolling still causes some problems with scrolling detection if touch movement is too fast. Perhaps wait for next frame before deciding where scrolling has gone before deciding if event should be triggered?

Usage is as follows:

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

or jquery style

$("article").on("gesture-down", function (e) { ... });
Eveevection answered 21/11, 2018 at 14:9 Comment(1)
Upvoted for the efforts and crispness of the outcome , Great work.Discern
T
3

All of these codes need improvement (like most of the codes that you can find on touch manipulation).

When playing with touch event, keep in mind that user have more than one finger, that a touch has an identifier and that touches list represent all current touches on the surface, even touches that have not moved.

So the process is relatively simple:

  1. ontouchstart: get the first changed touch (not event.originalEvent.touches property, but event.originalEvent.changedTouches one). Register its identifier with event.originalEvent.changedTouches[0].identifier and touch properties to look for (pageX/pageY or clientX/clientY that are pretty usefull in combination with DOMElement.getBoundingClientRect() method);

  2. ontouchmove: make sure that the current touch is in the changedTouches list with event.originalEvent.changedTouches.identifiedTouch( identifier ). If it return nothing, that means that the user has moved another touch (not the one you are looking for). Also register touch properties to look for and do whatever you want with.

  3. ontouchend: again, you must be sure the current touch is in changedTouches list. Do the job with touch properties and finally discard your current touch identifier.

If you want to do it stronger, consider multiple touches (not only one) to observe.

More information about TouchEvent, TouchList and Touch on: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Touch_events

Torchbearer answered 17/1, 2014 at 9:29 Comment(0)
T
1

Detecting left and right while touch is still moving.

This is done with saving last position and using timeout for erasing last position after touchmove stop.

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);
});
Trey answered 24/4, 2017 at 3:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.