Reworking click and pinch handling to get better behavior on multi-touch devices. r=ahocevar +testing from others (closes #3133)

git-svn-id: http://svn.openlayers.org/trunk/openlayers@11695 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
Tim Schaub
2011-03-11 01:53:26 +00:00
parent 3ce239208f
commit 5b7d530461
6 changed files with 488 additions and 184 deletions

View File

@@ -51,13 +51,21 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* constructed.
*/
pixelTolerance: 0,
/**
* APIProperty: dblclickTolerance
* {Number} Maximum distance in pixels between clicks for a sequence of
* events to be considered a double click. Default is 13. If the
* distance between two clicks is greater than this value, a double-
* click will not be fired.
*/
dblclickTolerance: 13,
/**
* APIProperty: stopSingle
* {Boolean} Stop other listeners from being notified of clicks. Default
* is false. If true, any click listeners registered before this one
* will not be notified of *any* click event (associated with double
* or single clicks).
* is false. If true, any listeners registered before this one for
* click or rightclick events will not be notified.
*/
stopSingle: false,
@@ -82,6 +90,13 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* {Number} The id of the timeout waiting to clear the <delayedCall>.
*/
timerId: null,
/**
* Property: touch
* {Boolean} When a touchstart event is fired, touch will be true and all
* mouse related listeners will do nothing.
*/
touch: false,
/**
* Property: down
@@ -92,15 +107,22 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/
down: null,
/**
/**
* Property: last
* {Object} Object that store relevant information about the last
* touchmove. Its 'xy' OpenLayers.Pixel property gives
* mousemove or touchmove. Its 'xy' OpenLayers.Pixel property gives
* the average location of the mouse/touch event. Its 'touches'
* property records clientX/clientY of each touches.
*/
last: null,
/**
* Property: first
* {Object} When waiting for double clicks, this object will store
* information about the first click in a two click sequence.
*/
first: null,
/**
* Property: rightclickTimerId
* {Number} The id of the right mouse timeout waiting to clear the
@@ -126,25 +148,8 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/
initialize: function(control, callbacks, options) {
OpenLayers.Handler.prototype.initialize.apply(this, arguments);
// optionally register for mousedown
if(this.pixelTolerance != null) {
this.mousedown = function(evt) {
this.down = this.getEventInfo(evt);
return true;
};
}
},
/**
* Method: mousedown
* Handle mousedown. Only registered as a listener if pixelTolerance is
* a non-zero value at construction.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
mousedown: null,
/**
* Method: touchstart
* Handle touchstart.
@@ -153,8 +158,67 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Continue propagating this event.
*/
touchstart: function(evt) {
if (!this.touch) {
this.unregisterMouseListeners();
this.touch = true;
}
this.down = this.getEventInfo(evt);
this.last = null;
this.last = this.getEventInfo(evt);
return true;
},
/**
* Method: touchmove
* Store position of last move, because touchend event can have
* an empty "touches" property.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
touchmove: function(evt) {
this.last = this.getEventInfo(evt);
return true;
},
/**
* Method: touchend
* Correctly set event xy property, and add lastTouches to have
* touches property from last touchstart or touchmove
*/
touchend: function(evt) {
// touchstart may not have been allowed to propagate
if (this.down) {
evt.xy = this.last.xy;
evt.lastTouches = this.last.touches;
this.handleSingle(evt);
}
return true;
},
/**
* Method: unregisterMouseListeners
* In a touch environment, we don't want to handle mouse events.
*/
unregisterMouseListeners: function() {
this.map.events.un({
mousedown: this.mousedown,
mouseup: this.mouseup,
click: this.click,
dblclick: this.dblclick,
scope: this
});
},
/**
* Method: mousedown
* Handle mousedown.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
mousedown: function(evt) {
this.down = this.getEventInfo(evt);
this.last = this.getEventInfo(evt);
return true;
},
@@ -171,8 +235,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
// Collect right mouse clicks from the mouseup
// IE - ignores the second right click in mousedown so using
// mouseup instead
if(this.checkModifiers(evt) &&
this.control.handleRightClicks &&
if (this.checkModifiers(evt) && this.control.handleRightClicks &&
OpenLayers.Event.isRightClick(evt)) {
propagate = this.rightclick(evt);
}
@@ -194,7 +257,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if(this.rightclickTimerId != null) {
//Second click received before timeout this must be
// a double click
this.clearTimer();
this.clearTimer();
this.callback('dblrightclick', [evt]);
return !this.stopDouble;
} else {
@@ -227,93 +290,93 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if (evt) {
this.callback('rightclick', [evt]);
}
return !this.stopSingle;
},
/**
* Method: dblclick
* Handle dblclick. For a dblclick, we get two clicks in some browsers
* (FF) and one in others (IE). So we need to always register for
* dblclick to properly handle single clicks.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
dblclick: function(evt) {
// for touch devices trigger dblclick only for
// "one finger" touch
var last = this.down || this.last;
if (this.passesTolerance(evt) &&
(!last || !last.touches || last.touches.length == 1)) {
if(this["double"]) {
this.callback('dblclick', [evt]);
}
this.clearTimer();
}
return !this.stopDouble;
},
/**
* Method: touchmove
* Store position of last move, because touchend event can have
* an empty "touches" property.
*/
touchmove: function(evt) {
this.last = this.getEventInfo(evt);
},
/**
* Method: touchend
* Correctly set event xy property, and add lastTouches to have
* touches property from last touchstart or touchmove
*/
touchend: function(evt) {
var last = this.last || this.down;
if (!evt || !last) {
return false;
}
evt.xy = last.xy;
evt.lastTouches = last.touches;
return evt.xy ? this.click(evt) : false;
},
/**
* Method: click
* Handle click.
* Handle click events from the browser. This is registered as a listener
* for click events and should not be called from other events in this
* handler.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
click: function(evt) {
// Sencha Touch emulates click events, see ticket 3079 for more info
if (this.down && this.down.touches && evt.type === "click") {
return !this.stopSingle;
if (!this.last) {
this.last = this.getEventInfo(evt);
}
if(this.passesTolerance(evt)) {
if(this.timerId != null) {
this.handleSingle(evt);
return !this.stopSingle;
},
/**
* Method: dblclick
* Handle dblclick. For a dblclick, we get two clicks in some browsers
* (FF) and one in others (IE). So we need to always register for
* dblclick to properly handle single clicks. This method is registered
* as a listener for the dblclick browser event. It should *not* be
* called by other methods in this handler.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
dblclick: function(evt) {
this.handleDouble(evt);
return !this.stopDouble;
},
/**
* Method: handleDouble
* Handle double-click sequence.
*/
handleDouble: function(evt) {
if (this["double"] && this.passesDblclickTolerance(evt)) {
this.callback("dblclick", [evt]);
}
},
/**
* Method: handleSingle
* Handle single click sequence.
*/
handleSingle: function(evt) {
if (this.passesTolerance(evt)) {
if (this.timerId != null) {
// already received a click
var last = this.down || this.last;
if (last && last.touches && last.touches.length > 0) {
// touch device - we may trigger dblclick
this.dblclick(evt);
} else {
if (this.last.touches && this.last.touches.length === 1) {
// touch device, no dblclick event - this may be a double
this.handleDouble(evt);
}
// if we're not in a touch environment we clear the click timer
// if we've got a second touch, we'll get two touchend events
if (!this.last.touches || this.last.touches.length !== 2) {
this.clearTimer();
}
} else {
// remember the first click info so we can compare to the second
this.first = this.getEventInfo(evt);
// set the timer, send evt only if single is true
//use a clone of the event object because it will no longer
//be a valid event object in IE in the timer callback
var clickEvent = this.single ?
OpenLayers.Util.extend({}, evt) : null;
this.timerId = window.setTimeout(
OpenLayers.Function.bind(this.delayedCall, this, clickEvent),
this.delay
);
this.queuePotentialClick(clickEvent);
}
}
return !this.stopSingle;
},
/**
* Method: queuePotentialClick
* This method is separated out largely to make testing easier (so we
* don't have to override window.setTimeout)
*/
queuePotentialClick: function(evt) {
this.timerId = window.setTimeout(
OpenLayers.Function.bind(this.delayedCall, this, evt),
this.delay
);
},
/**
* Method: passesTolerance
* Determine whether the event is within the optional pixel tolerance. Note
@@ -327,28 +390,67 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/
passesTolerance: function(evt) {
var passes = true;
if(this.pixelTolerance != null && this.down && this.down.xy) {
var dpx = Math.sqrt(
Math.pow(this.down.xy.x - evt.xy.x, 2) +
Math.pow(this.down.xy.y - evt.xy.y, 2)
);
if(dpx > this.pixelTolerance) {
passes = false;
if (this.pixelTolerance != null && this.down && this.down.xy) {
passes = this.pixelTolerance >= this.down.xy.distanceTo(evt.xy);
// for touch environments, we also enforce that all touches
// start and end within the given tolerance to be considered a click
if (passes && this.touch &&
this.down.touches.length === this.last.touches.length) {
// the touchend event doesn't come with touches, so we check
// down and last
for (var i=0, ii=this.down.touches.length; i<ii; ++i) {
if (this.getTouchDistance(
this.down.touches[i],
this.last.touches[i]
) > this.pixelTolerance) {
passes = false;
break;
}
}
}
}
return passes;
},
/**
* Method: getTouchDistance
*
* Returns:
* {Boolean} The pixel displacement between two touches.
*/
getTouchDistance: function(from, to) {
return Math.sqrt(
Math.pow(from.clientX - to.clientX, 2) +
Math.pow(from.clientY - to.clientY, 2)
);
},
/**
* Method: passesDblclickTolerance
* Determine whether the event is within the optional double-cick pixel
* tolerance.
*
* Returns:
* {Boolean} The click is within the double-click pixel tolerance.
*/
passesDblclickTolerance: function(evt) {
var passes = true;
if (this.down && this.first) {
passes = this.down.xy.distanceTo(this.first.xy) <= this.dblclickTolerance;
}
return passes;
},
/**
* Method: clearTimer
* Clear the timer and set <timerId> to null.
*/
clearTimer: function() {
if(this.timerId != null) {
if (this.timerId != null) {
window.clearTimeout(this.timerId);
this.timerId = null;
}
if(this.rightclickTimerId != null) {
if (this.rightclickTimerId != null) {
window.clearTimeout(this.rightclickTimerId);
this.rightclickTimerId = null;
}
@@ -361,8 +463,8 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/
delayedCall: function(evt) {
this.timerId = null;
if(evt) {
this.callback('click', [evt]);
if (evt) {
this.callback("click", [evt]);
}
},
@@ -407,7 +509,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) {
this.clearTimer();
this.down = null;
this.last = null;
this.first = null;
deactivated = true;
}
return deactivated;

View File

@@ -37,9 +37,9 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
/**
* Property: stopDown
* {Boolean} Stop propagation of touchstart events from getting to
* listeners on the same element. Default is true.
* listeners on the same element. Default is false.
*/
stopDown: true,
stopDown: false,
/**
* Property: pinching
@@ -105,6 +105,7 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
this.start = null;
this.last = null;
}
// prevent document dragging
OpenLayers.Event.stop(evt);
return propagate;
},
@@ -120,15 +121,15 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Let the event propagate.
*/
touchmove: function(evt) {
var propagate = true;
if (this.started && OpenLayers.Event.isMultiTouch(evt)) {
this.pinching = true;
var current = this.getPinchData(evt);
this.callback("move", [evt, current]);
this.last = current;
propagate = false;
// prevent document dragging
OpenLayers.Event.stop(evt);
}
return propagate;
return true;
},
/**
@@ -142,16 +143,14 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Let the event propagate.
*/
touchend: function(evt) {
var propagate = true;
if (this.started) {
this.started = false;
this.pinching = false;
this.callback("done", [evt, this.start, this.last]);
this.start = null;
this.last = null;
propagate = false;
}
return propagate;
return true;
},
/**