From ce977e53645d22cd7f0699a0f9837366c8f3ca34 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 25 Sep 2012 17:27:17 +0200 Subject: [PATCH] Map browser event handling for touch devices By handling click, dblckick and drag sequences in MapBrowserEvent, we can overcome closure issues with touch events. In particular, these issues were double drag events (from mousemove and touchmove) and a missing dblclick event on touch devices. --- src/ol/interaction/dblclickzoom.js | 6 +- src/ol/interaction/drag.js | 6 +- src/ol/map.js | 58 +++---- src/ol/mapbrowserevent.js | 242 +++++++++++++++++++++++++++++ 4 files changed, 266 insertions(+), 46 deletions(-) diff --git a/src/ol/interaction/dblclickzoom.js b/src/ol/interaction/dblclickzoom.js index 587f776265..f01587843c 100644 --- a/src/ol/interaction/dblclickzoom.js +++ b/src/ol/interaction/dblclickzoom.js @@ -1,7 +1,7 @@ goog.provide('ol.interaction.DblClickZoom'); -goog.require('goog.events.EventType'); goog.require('ol.MapBrowserEvent'); +goog.require('ol.MapBrowserEvent.EventType'); goog.require('ol.interaction.Constraints'); goog.require('ol.interaction.Interaction'); @@ -24,8 +24,8 @@ goog.inherits(ol.interaction.DblClickZoom, ol.interaction.Interaction); ol.interaction.DblClickZoom.prototype.handleMapBrowserEvent = function(mapBrowserEvent) { var browserEvent = mapBrowserEvent.browserEvent; - if (browserEvent.type == goog.events.EventType.DBLCLICK && - browserEvent.isMouseActionButton()) { + if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DBLCLICK && + mapBrowserEvent.isMouseActionButton()) { var map = mapBrowserEvent.map; var resolution = map.getResolution(); var delta = mapBrowserEvent.browserEvent.shiftKey ? -1 : 1; diff --git a/src/ol/interaction/drag.js b/src/ol/interaction/drag.js index 2009e93364..fbe57dfd54 100644 --- a/src/ol/interaction/drag.js +++ b/src/ol/interaction/drag.js @@ -93,19 +93,19 @@ ol.interaction.Drag.prototype.handleMapBrowserEvent = } var browserEvent = mapBrowserEvent.browserEvent; if (this.dragging_) { - if (mapBrowserEvent.type == goog.fx.Dragger.EventType.DRAG) { + if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DRAG) { goog.asserts.assert(browserEvent instanceof goog.events.BrowserEvent); this.deltaX = browserEvent.clientX - this.startX; this.deltaY = browserEvent.clientY - this.startY; this.handleDrag(mapBrowserEvent); - } else if (mapBrowserEvent.type == goog.fx.Dragger.EventType.END) { + } else if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DRAGEND) { goog.asserts.assert(browserEvent instanceof goog.events.BrowserEvent); this.deltaX = browserEvent.clientX - this.startX; this.deltaY = browserEvent.clientY - this.startY; this.handleDragEnd(mapBrowserEvent); this.dragging_ = false; } - } else if (mapBrowserEvent.type == goog.fx.Dragger.EventType.START) { + } else if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DRAGSTART) { goog.asserts.assert(browserEvent instanceof goog.events.BrowserEvent); this.startX = browserEvent.clientX; this.startY = browserEvent.clientY; diff --git a/src/ol/map.js b/src/ol/map.js index 35d1650410..4372cbad2b 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -22,8 +22,6 @@ goog.require('goog.events.MouseWheelEvent'); goog.require('goog.events.MouseWheelHandler'); goog.require('goog.events.MouseWheelHandler.EventType'); goog.require('goog.functions'); -goog.require('goog.fx.DragEvent'); -goog.require('goog.fx.Dragger'); goog.require('goog.fx.anim'); goog.require('goog.fx.anim.Animated'); goog.require('goog.object'); @@ -98,12 +96,6 @@ ol.Map = function( this.logger = goog.debug.Logger.getLogger('ol.map.' + goog.getUid(this)); } - /** - * @type {boolean} Are we a touch device? - * @private - */ - this.isTouch_ = false; - /** * @type {ol.TransformFunction} * @private @@ -159,9 +151,15 @@ ol.Map = function( this.viewport_.style.zIndex = ol.MapPaneZIndex.VIEWPORT; goog.dom.appendChild(container, this.viewport_); - goog.events.listen(this.viewport_, [ - goog.events.EventType.DBLCLICK - ], this.handleBrowserEvent, false, this); + var mapBrowserEventHandler = new ol.MapBrowserEventHandler(this); + goog.events.listen(mapBrowserEventHandler, [ + ol.MapBrowserEvent.EventType.CLICK, + ol.MapBrowserEvent.EventType.DBLCLICK, + ol.MapBrowserEvent.EventType.DRAGSTART, + ol.MapBrowserEvent.EventType.DRAG, + ol.MapBrowserEvent.EventType.DRAGEND + ], this.handleMapBrowserEvent, false, this); + this.registerDisposable(mapBrowserEventHandler); // FIXME we probably shouldn't listen on document... var keyHandler = new goog.events.KeyHandler(document); @@ -175,17 +173,6 @@ ol.Map = function( this.handleBrowserEvent, false, this); this.registerDisposable(mouseWheelHandler); - var dragger = new goog.fx.Dragger(this.viewport_); - dragger.defaultAction = function() {}; - goog.events.listen(dragger, [ - goog.fx.Dragger.EventType.START, - goog.fx.Dragger.EventType.DRAG, - goog.fx.Dragger.EventType.END - ], this.handleDraggerEvent, false, this); - goog.events.listen(this.viewport_, goog.events.EventType.TOUCHSTART, - this.handleTouchstart, false, this); - this.registerDisposable(dragger); - /** * @type {ol.renderer.Map} * @private @@ -501,6 +488,14 @@ ol.Map.prototype.getViewport = function() { ol.Map.prototype.handleBrowserEvent = function(browserEvent, opt_type) { var type = opt_type || browserEvent.type; var mapBrowserEvent = new ol.MapBrowserEvent(type, this, browserEvent); + this.handleMapBrowserEvent(mapBrowserEvent); +}; + + +/** + * @param {ol.MapBrowserEvent} mapBrowserEvent The event to handle. + */ +ol.Map.prototype.handleMapBrowserEvent = function(mapBrowserEvent) { var interactions = this.getInteractions(); var interactionsArray = /** @type {Array.} */ interactions.getArray(); @@ -511,28 +506,11 @@ ol.Map.prototype.handleBrowserEvent = function(browserEvent, opt_type) { }; -/** - * @param {goog.events.Event} browserEvent Browser event. - */ -ol.Map.prototype.handleTouchstart = function(browserEvent) { - this.isTouch_ = true; - // now we know that we are a touch device - goog.events.unlisten(this.viewport_, goog.events.EventType.TOUCHSTART, - this.handleTouchstart, false, this); -}; - - /** * @param {goog.fx.DragEvent} dragEvent Drag event. */ ol.Map.prototype.handleDraggerEvent = function(dragEvent) { - var browserEvent = dragEvent.browserEvent; - // workaround for goog.fx.Dragger issue that causes both mousemove and - // touchmove to trigger a drag event on touch devices - if ((this.isTouch_ && browserEvent.type != goog.events.EventType.MOUSEMOVE) || - !this.isTouch_) { - this.handleBrowserEvent(browserEvent, dragEvent.type); - } + this.handleBrowserEvent(dragEvent.browserEvent, dragEvent.type); }; diff --git a/src/ol/mapbrowserevent.js b/src/ol/mapbrowserevent.js index 0fa253d952..53bd1ba68c 100644 --- a/src/ol/mapbrowserevent.js +++ b/src/ol/mapbrowserevent.js @@ -1,6 +1,11 @@ goog.provide('ol.MapBrowserEvent'); +goog.provide('ol.MapBrowserEvent.EventType'); +goog.provide('ol.MapBrowserEventHandler'); +goog.require('goog.asserts'); goog.require('goog.events.BrowserEvent'); +goog.require('goog.events.EventTarget'); +goog.require('goog.events.EventType'); goog.require('goog.style'); goog.require('ol.Coordinate'); goog.require('ol.MapEvent'); @@ -51,3 +56,240 @@ ol.MapBrowserEvent.prototype.getCoordinate = function() { return coordinate; } }; + + +/** + * @return {boolean} Do we have a left click? + */ +ol.MapBrowserEvent.prototype.isMouseActionButton = function() { + // always assume a left-click on touch devices + return ('ontouchstart' in document.documentElement) || + this.browserEvent.isMouseActionButton(); +}; + + + +/** + * @param {ol.Map} map The map with the viewport to listen to events on. + * @constructor + * @extends {goog.events.EventTarget} + */ +ol.MapBrowserEventHandler = function(map) { + + /** + * This is the element that we will listen to the real events on. + * @type {ol.Map} + * @private + */ + this.map_ = map; + + /** + * @type {Object} + * @private + */ + this.previous_ = null; + + /** + * @type {boolean} + * @private + */ + this.dragged_ = false; + + /** + * @type {number|undefined} + * @private + */ + this.timestamp_; + + /** + * @type {Array.} + * @private + */ + this.dragListenerKeys_ = null; + + /** + * @type {goog.events.BrowserEvent} + * @private + */ + this.down_ = null; + + /** + * @type {boolean} + * @private + */ + this.isTouch_ = document && ('ontouchstart' in document.documentElement); + + var element = this.map_.getViewport(); + goog.events.listen(element, + this.isTouch_ ? + goog.events.EventType.TOUCHSTART : + goog.events.EventType.MOUSEDOWN, + this.dragstart_, false, this); + goog.events.listen(element, + this.isTouch_ ? + goog.events.EventType.TOUCHEND : + goog.events.EventType.MOUSEUP, + this.dblclick_, false, this); + goog.events.listen(element, + goog.events.EventType.CLICK, this.click_, false, this); +}; +goog.inherits(ol.MapBrowserEventHandler, goog.events.EventTarget); + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @private + */ +ol.MapBrowserEventHandler.prototype.touchEnableBrowserEvent_ = + function(browserEvent) { + if (this.isTouch_) { + goog.asserts.assert(browserEvent instanceof goog.events.BrowserEvent); + var nativeEvent = browserEvent.getBrowserEvent(); + if (nativeEvent.touches && nativeEvent.touches.length) { + var nativeTouch = nativeEvent.touches[0]; + browserEvent.clientX = nativeTouch.clientX; + browserEvent.clientY = nativeTouch.clientY; + } + } +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @private + */ +ol.MapBrowserEventHandler.prototype.click_ = function(browserEvent) { + if (!this.dragged_) { + this.touchEnableBrowserEvent_(browserEvent); + var newEvent = new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.CLICK, this.map_, browserEvent); + this.down_ = null; + this.dispatchEvent(newEvent); + } +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @private + */ +ol.MapBrowserEventHandler.prototype.dblclick_ = function(browserEvent) { + if (!this.dragged_) { + var now = new Date().getTime(); + if (!this.timestamp_ || now - this.timestamp_ > 250) { + this.timestamp_ = now; + } else { + this.timestamp_ = undefined; + this.touchEnableBrowserEvent_(this.down_); + var newEvent = new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.DBLCLICK, this.map_, this.down_); + this.down_ = null; + this.dispatchEvent(newEvent); + } + } +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @private + */ +ol.MapBrowserEventHandler.prototype.dragstart_ = function(browserEvent) { + if (!this.previous_) { + this.touchEnableBrowserEvent_(browserEvent); + this.previous_ = { + clientX: browserEvent.clientX, + clientY: browserEvent.clientY + }; + this.down_ = browserEvent; + this.dragged_ = false; + if (this.isTouch_) { + this.dragListenerKeys_ = [ + goog.events.listen(document, + goog.events.EventType.TOUCHMOVE, this.drag_, false, this), + goog.events.listen(document, + goog.events.EventType.TOUCHEND, this.dragend_, false, this) + ]; + } else { + this.dragListenerKeys_ = [ + goog.events.listen(document, + goog.events.EventType.MOUSEMOVE, this.drag_, false, this), + goog.events.listen(document, + goog.events.EventType.MOUSEUP, this.dragend_, false, this) + ]; + } + var newEvent = new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.DRAGSTART, this.map_, browserEvent); + this.dispatchEvent(newEvent); + } +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @private + */ +ol.MapBrowserEventHandler.prototype.drag_ = function(browserEvent) { + this.dragged_ = true; + this.touchEnableBrowserEvent_(browserEvent); + this.previous_ = { + clientX: browserEvent.clientX, + clientY: browserEvent.clientY + }; + // prevent viewport dragging on touch devices + browserEvent.preventDefault(); + var newEvent = new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.DRAG, this.map_, browserEvent); + this.dispatchEvent(newEvent); +}; + + +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @private + */ +ol.MapBrowserEventHandler.prototype.dragend_ = function(browserEvent) { + if (this.previous_) { + goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey); + this.dragListenerKeys_ = null; + this.previous_ = null; + var newEvent = new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.DRAGEND, this.map_, browserEvent); + this.down_ = null; + this.dispatchEvent(newEvent); + } +}; + + +/** @override */ +ol.MapBrowserEventHandler.prototype.disposeInternal = function() { + var element = this.map_.getViewport(); + goog.events.unlisten(element, + this.isTouch_ ? + goog.events.EventType.TOUCHSTART : + goog.events.EventType.MOUSEDOWN, + this.dragstart_, false, this); + goog.events.unlisten(element, + this.isTouch_ ? + goog.events.EventType.TOUCHEND : + goog.events.EventType.MOUSEUP, + this.dblclick_, false, this); + goog.events.unlisten(element, + goog.events.EventType.CLICK, this.click_, false, this); + goog.asserts.assert(goog.isDef(this.dragListenerKeys_)); + goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey); + this.dragListenerKeys_ = null; +}; + + +/** + * Constants for event names. + * @enum {string} + */ +ol.MapBrowserEvent.EventType = { + CLICK: goog.events.EventType.CLICK, + DBLCLICK: goog.events.EventType.DBLCLICK, + DRAGSTART: 'dragstart', + DRAG: 'drag', + DRAGEND: 'dragend' +};