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.

Raphael Jeger
  • 5,024
  • 13
  • 48
  • 79

5 Answers5

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.

TheCrazyProfessor
  • 919
  • 1
  • 15
  • 31
cocco
  • 16,442
  • 7
  • 62
  • 77
  • Unfortunately this is too complicated for me to understand :/ Another solution is using http://jgestures.codeplex.com/ where you can use event listeners on swipeleft, swiperight and so on – Jason Vasilev Nov 26 '13 at 14:15
  • 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 touchend – cocco Nov 26 '13 at 15:04
  • 2
    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 needed – Eric Fuller Feb 12 '14 at 02:48
  • 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. – cocco Feb 12 '14 at 09:11
  • http://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. – cocco Feb 12 '14 at 09:21
  • Awesome! Little code, no extra query mobile necessary. Thanks so much! – Garavani Sep 03 '14 at 09:01
  • @cocco 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? – Garavani Sep 23 '14 at 06:46
  • 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? – cocco Sep 23 '14 at 08:02
  • http://www.mobilexweb.com/blog/safari-ios8-iphone6-web-developers-designers 'Touch events inside iframes on home-screen apps are not being reported' – cocco Sep 23 '14 at 08:15
  • 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. – Nick Manning Sep 23 '14 at 15:19
  • 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 http://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. – Robin Luiten Sep 24 '14 at 06:15
  • @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? – Garavani Sep 24 '14 at 07:58
  • 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 – Garavani Sep 29 '14 at 12:59
  • Can someone test this... i increased the performance by removing some extra unnecessary stuff and caching more variables... MOBILE ONLY http://jsfiddle.net/zgc81Lb8/5/ – cocco Sep 30 '14 at 12:56
  • @cocco 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! – Garavani Oct 02 '14 at 11:22
  • Still found no solution for using it in my iframes on home screen iOS8. Anyone who found some workaround for that? – Garavani Oct 02 '14 at 11:24
  • @cocco Sorry, solved my problem. Your script works awesome anywhere. – Garavani Oct 02 '14 at 12:47
  • @cocco any insight why iphone4 (7.1) pinch to zoom in events propagate into your script as swr (and how to stop it) – ck_ Oct 02 '14 at 15:58
  • if(e.touches.length>1)return; add this at the top of the touchmove and touchstart... – cocco Oct 02 '14 at 16:31
  • @cocco was able to fold it a little smaller by making a recursive function for touches http://pastebin.com/raw.php?i=q3yLqVtM touchcancel could make `b.z=9` instead to cancel the end? – ck_ Oct 03 '14 at 00:56
  • @cocco 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. – ck_ Oct 03 '14 at 02:39
  • I may add here, also that iframes on iOS8 don’t handle any kind of touch events. Great bug. – Garavani Oct 03 '14 at 08:38
  • @ck_ 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. – cocco Oct 03 '14 at 12:10
  • 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. – cocco Oct 03 '14 at 12:14
  • 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... – cocco Oct 03 '14 at 12:17
  • i mean keep it simple/short/fast i think the final user is happier. – cocco Oct 03 '14 at 12:41
  • still no touch events in iframes in home screen use under iOS8.1 – Garavani Oct 21 '14 at 15:51
  • still no touch events in iframes in home screen even under iOS 8.1.1 – Garavani Nov 21 '14 at 15:37
  • 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? – Garavani Dec 12 '14 at 11:35
  • I openend a question for that. Thanks for help! Please don’t down vote again :-( [link](http://stackoverflow.com/questions/27480049/custom-swipe-event-indicated-by-touchmove-event) – Garavani Dec 16 '14 at 07:40
  • 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! – John M. May 14 '21 at 22:36
  • It's really helpful for me. thanks – KATHEESKUMAR Mar 11 '22 at 07:50
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/

EscapeNetscape
  • 2,892
  • 1
  • 33
  • 32
5

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

(function(d) {
    // based on original source: https://stackoverflow.com/a/17567696/334451
    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) { ... });
Mikko Rantalainen
  • 14,132
  • 10
  • 74
  • 112
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

Loops
  • 31
  • 2
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);
});
Nebojsa Sapic
  • 9,285
  • 1
  • 22
  • 23