diff --git a/CHANGES.md b/CHANGES.md index 684205004fc8..a2dfb123e38c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Change Log ##### Additions :tada: * `Resource.fetchImage` now has a `flipY` option to vertically flip an image during fetch & decode. It is only valid when `ImageBitmapOptions` is supported by the browser. [#7579](https://github.com/AnalyticalGraphicsInc/cesium/pull/7579) * Added `backFaceCulling` and `normalShading` options to `PointCloudShading`. Both options are only applicable for point clouds containing normals. [#7399](https://github.com/AnalyticalGraphicsInc/cesium/pull/7399) +* Added support for touch and hold gesture. The touch and hold delay can be customized by updating `ScreenSpaceEventHandler.touchHoldDelayMilliseconds`. [#7286](https://github.com/AnalyticalGraphicsInc/cesium/pull/7286) ##### Fixes :wrench: * Fixed the value for `BlendFunction.ONE_MINUS_CONSTANT_COLOR`. [#7624](https://github.com/AnalyticalGraphicsInc/cesium/pull/7624) diff --git a/Source/Core/ScreenSpaceEventHandler.js b/Source/Core/ScreenSpaceEventHandler.js index 91d3272de5a5..2b539365173a 100644 --- a/Source/Core/ScreenSpaceEventHandler.js +++ b/Source/Core/ScreenSpaceEventHandler.js @@ -133,6 +133,14 @@ define([ return (getTimestamp() - screenSpaceEventHandler._lastSeenTouchEvent) > ScreenSpaceEventHandler.mouseEmulationIgnoreMilliseconds; } + function checkPixelTolerance(startPosition, endPosition, pixelTolerance) { + var xDiff = startPosition.x - endPosition.x; + var yDiff = startPosition.y - endPosition.y; + var totalPixels = Math.sqrt(xDiff * xDiff + yDiff * yDiff); + + return totalPixels < pixelTolerance; + } + function handleMouseDown(screenSpaceEventHandler, event) { if (!canProcessMouseEvent(screenSpaceEventHandler)) { return; @@ -193,11 +201,7 @@ define([ if (defined(clickAction)) { var startPosition = screenSpaceEventHandler._primaryStartPosition; - var xDiff = startPosition.x - position.x; - var yDiff = startPosition.y - position.y; - var totalPixels = Math.sqrt(xDiff * xDiff + yDiff * yDiff); - - if (totalPixels < screenSpaceEventHandler._clickPixelTolerance) { + if (checkPixelTolerance(startPosition, position, screenSpaceEventHandler._clickPixelTolerance)) { Cartesian2.clone(position, mouseClickEvent.position); clickAction(mouseClickEvent); @@ -398,11 +402,13 @@ define([ var touchClickEvent = { position : new Cartesian2() }; + var touchHoldEvent = { + position : new Cartesian2() + }; function fireTouchEvents(screenSpaceEventHandler, event) { var modifier = getModifier(event); var positions = screenSpaceEventHandler._positions; - var previousPositions = screenSpaceEventHandler._previousPositions; var numberOfTouches = positions.length; var action; var clickAction; @@ -411,6 +417,12 @@ define([ if (numberOfTouches !== 1 && screenSpaceEventHandler._buttonDown[MouseButton.LEFT]) { // transitioning from single touch, trigger UP and might trigger CLICK screenSpaceEventHandler._buttonDown[MouseButton.LEFT] = false; + + if(defined(screenSpaceEventHandler._touchHoldTimer)) { + clearTimeout(screenSpaceEventHandler._touchHoldTimer); + screenSpaceEventHandler._touchHoldTimer = undefined; + } + action = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_UP, modifier); if (defined(action)) { @@ -419,18 +431,14 @@ define([ action(touchEndEvent); } - if (numberOfTouches === 0) { + if (numberOfTouches === 0 && !screenSpaceEventHandler._isTouchHolding) { // releasing single touch, check for CLICK clickAction = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.LEFT_CLICK, modifier); if (defined(clickAction)) { var startPosition = screenSpaceEventHandler._primaryStartPosition; - var endPosition = previousPositions.values[0]; - var xDiff = startPosition.x - endPosition.x; - var yDiff = startPosition.y - endPosition.y; - var totalPixels = Math.sqrt(xDiff * xDiff + yDiff * yDiff); - - if (totalPixels < screenSpaceEventHandler._clickPixelTolerance) { + var endPosition = screenSpaceEventHandler._previousPositions.values[0]; + if(checkPixelTolerance(startPosition, endPosition, screenSpaceEventHandler._clickPixelTolerance)) { Cartesian2.clone(screenSpaceEventHandler._primaryPosition, touchClickEvent.position); clickAction(touchClickEvent); @@ -438,6 +446,8 @@ define([ } } + screenSpaceEventHandler._isTouchHolding = false; + // Otherwise don't trigger CLICK, because we are adding more touches. } @@ -469,6 +479,25 @@ define([ action(touchStartEvent); } + screenSpaceEventHandler._touchHoldTimer = setTimeout(function() { + if(!screenSpaceEventHandler.isDestroyed()) { + screenSpaceEventHandler._touchHoldTimer = undefined; + screenSpaceEventHandler._isTouchHolding = true; + + clickAction = screenSpaceEventHandler.getInputAction(ScreenSpaceEventType.RIGHT_CLICK, modifier); + + if (defined(clickAction)) { + var startPosition = screenSpaceEventHandler._primaryStartPosition; + var endPosition = screenSpaceEventHandler._previousPositions.values[0]; + if(checkPixelTolerance(startPosition, endPosition, screenSpaceEventHandler._holdPixelTolerance)) { + Cartesian2.clone(screenSpaceEventHandler._primaryPosition, touchHoldEvent.position); + + clickAction(touchHoldEvent); + } + } + } + }, ScreenSpaceEventHandler.touchHoldDelayMilliseconds); + event.preventDefault(); } @@ -669,6 +698,7 @@ define([ RIGHT: false }; this._isPinching = false; + this._isTouchHolding = false; this._lastSeenTouchEvent = -ScreenSpaceEventHandler.mouseEmulationIgnoreMilliseconds; this._primaryStartPosition = new Cartesian2(); @@ -680,9 +710,12 @@ define([ this._removalFunctions = []; + this._touchHoldTimer = undefined; + // TODO: Revisit when doing mobile development. May need to be configurable // or determined based on the platform? this._clickPixelTolerance = 5; + this._holdPixelTolerance = 25; this._element = defaultValue(element, document); @@ -799,5 +832,13 @@ define([ */ ScreenSpaceEventHandler.mouseEmulationIgnoreMilliseconds = 800; + /** + * The amount of time, in milliseconds, before a touch on the screen becomes a + * touch and hold. + * @type {Number} + * @default 1500 + */ + ScreenSpaceEventHandler.touchHoldDelayMilliseconds = 1500; + return ScreenSpaceEventHandler; }); diff --git a/Specs/Core/ScreenSpaceEventHandlerSpec.js b/Specs/Core/ScreenSpaceEventHandlerSpec.js index 3b5766bf58c9..ebaeb29c6799 100644 --- a/Specs/Core/ScreenSpaceEventHandlerSpec.js +++ b/Specs/Core/ScreenSpaceEventHandlerSpec.js @@ -1207,6 +1207,89 @@ defineSuite([ expect(action).not.toHaveBeenCalled(); }); + it('handles touch and hold gesture', function() { + jasmine.clock().install(); + + var delay = ScreenSpaceEventHandler.touchHoldDelayMilliseconds; + + var eventType = ScreenSpaceEventType.RIGHT_CLICK; + + var action = createCloningSpy('action'); + handler.setInputAction(action, eventType); + + expect(handler.getInputAction(eventType)).toEqual(action); + + // start, then end + function simulateInput(timeout) { + var touchStartPosition = { + clientX : 1, + clientY : 2 + }; + var touchEndPosition = { + clientX : 1, + clientY : 2 + }; + + if (usePointerEvents) { + DomEventSimulator.firePointerDown(element, combine({ + pointerType : 'touch', + pointerId : 1 + }, touchStartPosition)); + jasmine.clock().tick(timeout); + DomEventSimulator.firePointerUp(element, combine({ + pointerType : 'touch', + pointerId : 1 + }, touchEndPosition)); + } else { + DomEventSimulator.fireTouchStart(element, { + changedTouches : [combine({ + identifier : 0 + }, touchStartPosition)] + }); + jasmine.clock().tick(timeout); + DomEventSimulator.fireTouchEnd(element, { + changedTouches : [combine({ + identifier : 0 + }, touchEndPosition)] + }); + } + } + + simulateInput(delay + 1); + + expect(action.calls.count()).toEqual(1); + expect(action).toHaveBeenCalledWith({ + position : new Cartesian2(1, 2) + }); + + // Should not be fired if hold delay is less than touchHoldDelayMilliseconds. + action.calls.reset(); + + simulateInput(delay - 1); + + expect(action).not.toHaveBeenCalled(); + + // Should not be fired after removal. + action.calls.reset(); + + handler.removeInputAction(eventType); + + simulateInput(delay + 1); + + expect(action).not.toHaveBeenCalled(); + + // Should not fire click action if touch and hold is triggered. + eventType = ScreenSpaceEventType.LEFT_CLICK; + + handler.setInputAction(action, eventType); + + simulateInput(delay + 1); + + expect(action).not.toHaveBeenCalled(); + + jasmine.clock().uninstall(); + }); + it('treats touch cancel as touch end for touch clicks', function() { var eventType = ScreenSpaceEventType.LEFT_CLICK;