touch事件,tap,click,press,slide,swipe
轉自http://blog.mobiscroll.com/working-with-touch-events/
Working with touch events
- Posted on April 16, 2014
Last month I was attending the
jQuery Europe conference in Vienna with the Mobiscroll team.
There was a session called Getting
touchywhich gave an insight into touch events and talked about why we need them.
There is a lot of ground that the presentation covers, so make sure to check out the slides. I would like to build on top of it and share my experience on the topic.
So why do we need touch events?
We don’t always need to use touch events in our apps or websites. Turns out that mostly we can get away of using the regular click events because mobile browsers emulate the mouse events rather well.
While emulation works in many cases, the functionality is limited.
The usual problems are:
- Delayed event dispatch: mouse events are usually fired with a delay, which makes the app feel unresponsive.
- Mousemove doesn’t track, only a single emulated mousemove event is fired. This makes impossible to make complex UI interfaces with gestures.
There are a ton of resources on the web targeting these issues, so I’m not reinventing the wheel here, I will just share my own personal experience on how I combined and extended different solutions to match our needs while building Mobiscroll.
The click delay
As you probably know, there is a delay between the actual tap and the firing of the click event. I’m not going into details on why this happens, you can read about it in the slidesmentioned before.
A common technique to prevent the delay is to bind the handler to both touchend
and click
events.
The challenge here is to prevent the so called “phantom click”, meaning that if your handler was called on touch end, don’t execute it again when the click event is emulated by the browser. The proposed solution here is to call e.preventDefault()
either
on touchstart
ortouchend
which
will prevent the firing of emulated mouse events.
However I find this problematic because:
-
Calling it on
touchstart
will kill native page scroll -
Calling it on
touchend
will kill momentum scroll on some devices - It does not prevent at all emulated events on Android 4.x stock browsers
The solution we are using in Mobiscroll does the following:
- Attach
touchstart
,touchend
andclick
events - Remember start and end coordinates
- Call the handler
e.preventDefault()
ontouchend
, but only if movement was less then 20px in any direction (so the user did not have the intention to scroll) - Set a flag to prevent executing the handler again in the emulated click event
- Start a timeout which will clear the flag in case when the click event was not emulated
Putting everything together:
var startX,
startY,
tap;
function getCoord(e, c) {
return /touch/.test(e.type) ? (e.originalEvent || e).changedTouches[0]['page' + c] : e['page' + c];
}
function setTap() {
tap = true;
setTimeout(function () {
tap = false;
}, 500);
}
element.on('touchstart', function (ev) {
startX = getCoord(ev, 'X');
startY = getCoord(ev, 'Y');
}).on('touchend', function (ev) {
// If movement is less than 20px, execute the handler
if (Math.abs(getCoord(ev, 'X') - startX) < 20 && Math.abs(getCoord(ev, 'Y') - startY) < 20) {
// Prevent emulated mouse events
ev.preventDefault();
handler.call(this, ev);
}
setTap();
}).on('click', function (ev) {
if (!tap) {
// If handler was not called on touchend, call it on click;
handler.call(this, ev);
}
ev.preventDefault();
});
Working with gestures
When we started building the Listview, we imagined a widget with heavy gesture support. And this is where it shines.
We needed support of following:
- Use native vertical scrolling
- Handle left and right swipe gestures, disable page scroll during swipe gestures
- Handle tap
- Handle tap and hold (activate sort mode)
Native scroll
If we want to keep using native touch scrolling, this involves that we cannot calle.preventDefault()
on any of the touch events
unconditionally.
Horizontal swipe
We need to listen to the touchmove
event and decide on the fly whether it will be a vertical or horizontal swipe. If it’s horizontal
swipe, kill the page scroll with callinge.preventDefault()
. The following thresholds are used:
- If the horizontal movement is more than 7px, we consider it a swipe
- If the vertical movement is more than 10px, we consider it a scroll
Tap
We need to decide if the user is tapping on a list item or intends to swipe or scroll. If movement was less than 5px in any direction tap is being honored. We also need to take care of the duplicate firing of the events, so a flag is being set – similar to the one we used for the click delay.
Tap & hold
On touchstart
we will start a timer which activates the “sort/reorder” mode after a delay. This timer is being reset in case of
a scroll, swipe, or touchend
.
Let’s take a look at our event handlers. Also note that we attach the touch events and themousedown
to the element itself, while
the mousemove
and mouseup
events
are attached to the document element dynamically and removed at the end. That is because they behave differently: the touchmove
and touchend
will
still be fired if the finger leaves the element, while themousemove
and mouseend
event
will not fire once the mouse pointer has left the element.
var touch,
action,
diffX,
diffY,
endX,
endY,
scroll,
sort,
startX,
startY,
swipe,
function testTouch(e) {
if (e.type == 'touchstart') {
touch = true;
} else if (touch) {
touch = false;
return false;
}
return true;
}
function onStart(ev) {
if (testTouch(ev) && !action) {
action = true;
startX = getCoord(ev, 'X');
startY = getCoord(ev, 'Y');
diffX = 0;
diffY = 0;
sortTimer = setTimeout(function () {
sort = true;
}, 200);
if (ev.type == 'mousedown') {
$(document).on('mousemove', onMove).on('mouseup', onEnd);
}
}
}
function onMove(ev) {
if (action) {
endX = getCoord(ev, 'X');
endY = getCoord(ev, 'Y');
diffX = endX - startX;
diffY = endY - startY;
if (!sort && !swipe && !scroll) {
if (Math.abs(diffY) > 10) { // It's a scroll
scroll = true;
// Android 4.0 will not fire touchend event
$(this).trigger('touchend');
} else if (Math.abs(diffX) > 7) { // It's a swipe
swipe = true;
}
}
if (swipe) {
ev.preventDefault(); // Kill page scroll
// Handle swipe
// ...
}
if (sort) {
ev.preventDefault(); // Kill page scroll
// Handle sort
// ....
}
if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) {
clearTimeout(sortTimer);
}
}
}
function onEnd(ev) {
if (action) {
action = false;
if (swipe) {
// Handle swipe end
// ...
} else if (sort) {
// Handle sort end
// ...
} else if (!scroll && Math.abs(diffX) < 5 && Math.abs(diffY) < 5) { // Tap
if (ev.type === 'touchend') { // Prevent phantom clicks
ev.preventDefault();
}
// Handle tap
// ...
}
swipe = false;
sort = false;
scroll = false;
clearTimeout(sortTimer);
if (ev.type == 'mouseup') {
$(document).off('mousemove', onMove).off('mouseup', onEnd);
}
}
}
element
.on('touchstart mousedown', onStart)
.on('touchmove', onMove)
.on('touchend touchcancel', onEnd)
Problems that are still unsolved
Android ICS
On Android ICS if no preventDefault
is called on touchstart
or
the first touchmove
, furthertouchmove
events
and the touchend
will not be fired. As a workaround we need to decide in the first touchmove
if
this is a scroll (so we don’t call preventDefault
) and then manually trigger touchend
–
see the code above.
Windows Phone
In WP8 there is no way to prevent native scroll on the fly. To be able to listen to touch events, you need to set the following css property:
touch-action: none;
However this will kill all default behavior, like native page scroll. Fortunately this can be fine tuned, so for the Listview we use the following:
touch-action: pan-y;
Which tells to browser that vertical swipe will be handled by the browser, while our code will take care of the horizontal swipe. Unfortunately sorting won’t be working, because it will not prevent native scroll while dragging an element. So in WP8 the only
way to implement sorting is to use a “sort handle” element which has the touch-action: none;
rule applied. So when the user “picks”
up an item from the sort handle, we know he intends to reorder.
Firefox Mobile
In Firefox Mobile the native scroll can be killed only if preventDefault() is called on thetouchstart
event. Unfortunately at touchstart
we
don’t really know if we want scroll or not. This has two consequences:
-
sort will work with the above mentioned sort handle only (by calling
preventDefault()
ontouchstart
if dragged by the handle) - while swiping an item left or right vertical scroll will still work
These issues can easily disappear in upcoming browser updates or releases, until then we need to come up with workarounds.
What are your hacks and workarounds for dealing with complex gestures? Let us know in the comment section below.