Commit b5339433 authored by hirono@chromium.org's avatar hirono@chromium.org

Gallery.app: Add touch handlers for the zoom/scroll feature.

Previously touch operations are handled by SwipeOverlay class. Overlay is a
common way to obtain mouse/touch events in Gallery, but it does not support
touch specific capability (e.g. multi fingers). The CL newly adds TouchHandlers
class to the SlideMode class and let it call the zoom/scroll feature of the
slide mode depending on user's touch gestures.

BUG=245926
TEST=on link

Review URL: https://codereview.chromium.org/411853002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@284926 0039d316-1c4b-4281-b951-d872f2087c98
parent 2fda997e
...@@ -747,8 +747,10 @@ ImageEditor.MouseControl.prototype.onTouchStart = function(e) { ...@@ -747,8 +747,10 @@ ImageEditor.MouseControl.prototype.onTouchStart = function(e) {
* @param {TouchEvent} e Event. * @param {TouchEvent} e Event.
*/ */
ImageEditor.MouseControl.prototype.onTouchEnd = function(e) { ImageEditor.MouseControl.prototype.onTouchEnd = function(e) {
if (!this.dragHappened_ && Date.now() - this.touchStartInfo_.time <= if (!this.dragHappened_ &&
ImageEditor.MouseControl.MAX_TAP_DURATION_) { this.touchStartInfo_ &&
Date.now() - this.touchStartInfo_.time <=
ImageEditor.MouseControl.MAX_TAP_DURATION_) {
this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y); this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y);
if (this.previousTouchStartInfo_ && if (this.previousTouchStartInfo_ &&
Date.now() - this.previousTouchStartInfo_.time < Date.now() - this.previousTouchStartInfo_.time <
......
...@@ -52,13 +52,6 @@ function Viewport() { ...@@ -52,13 +52,6 @@ function Viewport() {
*/ */
this.scale_ = 1; this.scale_ = 1;
/**
* Index of zoom ratio. 0 is "zoom ratio = 1".
* @type {number}
* @private
*/
this.zoomIndex_ = 0;
/** /**
* Zoom ratio specified by user operations. * Zoom ratio specified by user operations.
* @type {number} * @type {number}
...@@ -93,18 +86,10 @@ function Viewport() { ...@@ -93,18 +86,10 @@ function Viewport() {
/** /**
* Zoom ratios. * Zoom ratios.
* *
* @type {Object.<string, number>} * @type {Array.<number>}
* @const * @const
*/ */
Viewport.ZOOM_RATIOS = Object.freeze({ Viewport.ZOOM_RATIOS = Object.freeze([1, 1.5, 2, 3]);
'3': 3,
'2': 2,
'1': 1.5,
'0': 1,
'-1': 0.75,
'-2': 0.5,
'-3': 0.25
});
/** /**
* @param {number} width Image width. * @param {number} width Image width.
...@@ -125,32 +110,57 @@ Viewport.prototype.setScreenSize = function(width, height) { ...@@ -125,32 +110,57 @@ Viewport.prototype.setScreenSize = function(width, height) {
}; };
/** /**
* Sets the new zoom ratio. * Sets zoom value directly.
* @param {number} zoomIndex New zoom index. * @param {number} zoom New zoom value.
*/ */
Viewport.prototype.setZoomIndex = function(zoomIndex) { Viewport.prototype.setZoom = function(zoom) {
// Ignore the invalid zoomIndex. var zoomMin = Viewport.ZOOM_RATIOS[0];
if (!Viewport.ZOOM_RATIOS[zoomIndex.toString()]) var zoomMax = Viewport.ZOOM_RATIOS[Viewport.ZOOM_RATIOS.length - 1];
return; var adjustedZoom = Math.max(zoomMin, Math.min(zoom, zoomMax));
this.zoomIndex_ = zoomIndex; this.zoom_ = adjustedZoom;
this.zoom_ = Viewport.ZOOM_RATIOS[zoomIndex];
this.update_(); this.update_();
}; };
/** /**
* Returns the current zoom index. * Returns the value of zoom.
* @return {number} Zoon index. * @return {number} Zoom value.
*/ */
Viewport.prototype.getZoomIndex = function() { Viewport.prototype.getZoom = function() {
return this.zoomIndex_; return this.zoom_;
}; };
/** /**
* Returns the value of zoom. * Sets the nearset larger value of ZOOM_RATIOS.
* @return {number} Zoom value.
*/ */
Viewport.prototype.getZoom = function() { Viewport.prototype.zoomIn = function() {
return this.zoomIndex_; var zoom = Viewport.ZOOM_RATIOS[0];
for (var i = 0; i < Viewport.ZOOM_RATIOS.length; i++) {
zoom = Viewport.ZOOM_RATIOS[i];
if (zoom > this.zoom_)
break;
}
this.setZoom(zoom);
};
/**
* Sets the nearest smaller value of ZOOM_RATIOS.
*/
Viewport.prototype.zoomOut = function() {
var zoom = Viewport.ZOOM_RATIOS[Viewport.ZOOM_RATIOS.length - 1];
for (var i = Viewport.ZOOM_RATIOS.length - 1; i >= 0; i--) {
zoom = Viewport.ZOOM_RATIOS[i];
if (zoom < this.zoom_)
break;
}
this.setZoom(zoom);
};
/**
* Obtains whether the picture is zoomed or not.
* @return {boolean}
*/
Viewport.prototype.isZoomed = function() {
return this.zoom_ !== 1;
}; };
/** /**
...@@ -379,7 +389,6 @@ Viewport.prototype.getCenteredRect_ = function( ...@@ -379,7 +389,6 @@ Viewport.prototype.getCenteredRect_ = function(
* Resets zoom and offset. * Resets zoom and offset.
*/ */
Viewport.prototype.resetView = function() { Viewport.prototype.resetView = function() {
this.zoomIndex_ = 0;
this.zoom_ = 1; this.zoom_ = 1;
this.offsetX_ = 0; this.offsetX_ = 0;
this.offsetY_ = 0; this.offsetY_ = 0;
......
...@@ -90,6 +90,11 @@ SlideMode.prototype.getName = function() { return 'slide'; }; ...@@ -90,6 +90,11 @@ SlideMode.prototype.getName = function() { return 'slide'; };
*/ */
SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; }; SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
/**
* @return {Viewport} Viewport.
*/
SlideMode.prototype.getViewport = function() { return this.viewport_; };
/** /**
* Initialize the listeners. * Initialize the listeners.
* @private * @private
...@@ -232,8 +237,7 @@ SlideMode.prototype.initDom_ = function() { ...@@ -232,8 +237,7 @@ SlideMode.prototype.initDom_ = function() {
this.displayStringFunction_, this.displayStringFunction_,
this.onToolsVisibilityChanged_.bind(this)); this.onToolsVisibilityChanged_.bind(this));
this.editor_.getBuffer().addOverlay( this.touchHandlers_ = new TouchHandler(this.imageContainer_, this);
new SwipeOverlay(this.advanceManually.bind(this)));
}; };
/** /**
...@@ -311,6 +315,7 @@ SlideMode.prototype.enter = function( ...@@ -311,6 +315,7 @@ SlideMode.prototype.enter = function(
this.selectionModel_.addEventListener('change', this.onSelectionBound_); this.selectionModel_.addEventListener('change', this.onSelectionBound_);
this.dataModel_.addEventListener('splice', this.onSpliceBound_); this.dataModel_.addEventListener('splice', this.onSpliceBound_);
this.dataModel_.addEventListener('content', this.onContentBound_); this.dataModel_.addEventListener('content', this.onContentBound_);
this.touchHandlers_.enabled = true;
// Wait 1000ms after the animation is done, then prefetch the next image. // Wait 1000ms after the animation is done, then prefetch the next image.
this.requestPrefetch(1, delay + 1000); this.requestPrefetch(1, delay + 1000);
...@@ -357,6 +362,9 @@ SlideMode.prototype.leave = function(zoomToRect, callback) { ...@@ -357,6 +362,9 @@ SlideMode.prototype.leave = function(zoomToRect, callback) {
// Disable the slide-mode only buttons when leaving. // Disable the slide-mode only buttons when leaving.
this.editButton_.setAttribute('disabled', ''); this.editButton_.setAttribute('disabled', '');
this.printButton_.setAttribute('disabled', ''); this.printButton_.setAttribute('disabled', '');
// Disable touch operation.
this.touchHandlers_.enabled = false;
}; };
...@@ -860,8 +868,9 @@ SlideMode.prototype.onKeyDown = function(event) { ...@@ -860,8 +868,9 @@ SlideMode.prototype.onKeyDown = function(event) {
case 'U+001B': // Escape case 'U+001B': // Escape
if (this.isEditing()) { if (this.isEditing()) {
this.toggleEditor(event); this.toggleEditor(event);
} else if (this.viewport_.getZoomIndex() !== 0) { } else if (this.viewport_.isZoomed()) {
this.viewport_.resetView(); this.viewport_.resetView();
this.touchHandlers_.stopOperation();
this.imageView_.applyViewportChange(); this.imageView_.applyViewportChange();
} else { } else {
return false; // Not handled. return false; // Not handled.
...@@ -878,14 +887,14 @@ SlideMode.prototype.onKeyDown = function(event) { ...@@ -878,14 +887,14 @@ SlideMode.prototype.onKeyDown = function(event) {
case 'Down': case 'Down':
case 'Left': case 'Left':
case 'Right': case 'Right':
if (!this.isEditing() && this.viewport_.getZoomIndex() !== 0) { if (!this.isEditing() && this.viewport_.isZoomed()) {
var delta = SlideMode.KEY_OFFSET_MAP[keyID]; var delta = SlideMode.KEY_OFFSET_MAP[keyID];
this.viewport_.setOffset( this.viewport_.setOffset(
~~(this.viewport_.getOffsetX() + ~~(this.viewport_.getOffsetX() +
delta[0] * this.viewport_.getZoom()), delta[0] * this.viewport_.getZoom()),
~~(this.viewport_.getOffsetY() + ~~(this.viewport_.getOffsetY() +
delta[1] * this.viewport_.getZoom()), delta[1] * this.viewport_.getZoom()));
true); this.touchHandlers_.stopOperation();
this.imageView_.applyViewportChange(); this.imageView_.applyViewportChange();
} else { } else {
this.advanceWithKeyboard(keyID); this.advanceWithKeyboard(keyID);
...@@ -898,21 +907,24 @@ SlideMode.prototype.onKeyDown = function(event) { ...@@ -898,21 +907,24 @@ SlideMode.prototype.onKeyDown = function(event) {
case 'Ctrl-U+00BB': // Ctrl+'=' zoom in. case 'Ctrl-U+00BB': // Ctrl+'=' zoom in.
if (!this.isEditing()) { if (!this.isEditing()) {
this.viewport_.setZoomIndex(this.viewport_.getZoomIndex() + 1); this.viewport_.zoomIn();
this.touchHandlers_.stopOperation();
this.imageView_.applyViewportChange(); this.imageView_.applyViewportChange();
} }
break; break;
case 'Ctrl-U+00BD': // Ctrl+'-' zoom out. case 'Ctrl-U+00BD': // Ctrl+'-' zoom out.
if (!this.isEditing()) { if (!this.isEditing()) {
this.viewport_.setZoomIndex(this.viewport_.getZoomIndex() - 1); this.viewport_.zoomOut();
this.touchHandlers_.stopOperation();
this.imageView_.applyViewportChange(); this.imageView_.applyViewportChange();
} }
break; break;
case 'Ctrl-U+0030': // Ctrl+'0' zoom reset. case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
if (!this.isEditing()) { if (!this.isEditing()) {
this.viewport_.resetView(); this.viewport_.setZoom(1.0);
this.touchHandlers_.stopOperation();
this.imageView_.applyViewportChange(); this.imageView_.applyViewportChange();
} }
break; break;
...@@ -928,6 +940,7 @@ SlideMode.prototype.onKeyDown = function(event) { ...@@ -928,6 +940,7 @@ SlideMode.prototype.onKeyDown = function(event) {
SlideMode.prototype.onResize_ = function() { SlideMode.prototype.onResize_ = function() {
this.viewport_.setScreenSize( this.viewport_.setScreenSize(
this.container_.clientWidth, this.container_.clientHeight); this.container_.clientWidth, this.container_.clientHeight);
this.touchHandlers_.stopOperation();
this.editor_.getBuffer().draw(); this.editor_.getBuffer().draw();
}; };
...@@ -1077,7 +1090,7 @@ SlideMode.prototype.isSlideshowOn_ = function() { ...@@ -1077,7 +1090,7 @@ SlideMode.prototype.isSlideshowOn_ = function() {
}; };
/** /**
* Start the slideshow. * Starts the slideshow.
* @param {number=} opt_interval First interval in ms. * @param {number=} opt_interval First interval in ms.
* @param {Event=} opt_event Event. * @param {Event=} opt_event Event.
*/ */
...@@ -1086,6 +1099,9 @@ SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) { ...@@ -1086,6 +1099,9 @@ SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
this.viewport_.resetView(); this.viewport_.resetView();
this.imageView_.applyViewportChange(); this.imageView_.applyViewportChange();
// Disable touch operation.
this.touchHandlers_.enabled = false;
// Set the attribute early to prevent the toolbar from flashing when // Set the attribute early to prevent the toolbar from flashing when
// the slideshow is being started from the mosaic view. // the slideshow is being started from the mosaic view.
this.container_.setAttribute('slideshow', 'playing'); this.container_.setAttribute('slideshow', 'playing');
...@@ -1116,7 +1132,7 @@ SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) { ...@@ -1116,7 +1132,7 @@ SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
}; };
/** /**
* Stop the slideshow. * Stops the slideshow.
* @param {Event=} opt_event Event. * @param {Event=} opt_event Event.
* @private * @private
*/ */
...@@ -1141,6 +1157,9 @@ SlideMode.prototype.stopSlideshow_ = function(opt_event) { ...@@ -1141,6 +1157,9 @@ SlideMode.prototype.stopSlideshow_ = function(opt_event) {
this.leaveAfterSlideshow_ = false; this.leaveAfterSlideshow_ = false;
setTimeout(this.toggleMode_.bind(this), toggleModeDelay); setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
} }
// Re-enable touch operation.
this.touchHandlers_.enabled = true;
}; };
/** /**
...@@ -1152,7 +1171,7 @@ SlideMode.prototype.isSlideshowPlaying_ = function() { ...@@ -1152,7 +1171,7 @@ SlideMode.prototype.isSlideshowPlaying_ = function() {
}; };
/** /**
* Pause/resume the slideshow. * Pauses/resumes the slideshow.
* @private * @private
*/ */
SlideMode.prototype.toggleSlideshowPause_ = function() { SlideMode.prototype.toggleSlideshowPause_ = function() {
...@@ -1182,7 +1201,7 @@ SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) { ...@@ -1182,7 +1201,7 @@ SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
}; };
/** /**
* Resume the slideshow. * Resumes the slideshow.
* @param {number=} opt_interval Slideshow interval in ms. * @param {number=} opt_interval Slideshow interval in ms.
* @private * @private
*/ */
...@@ -1192,7 +1211,7 @@ SlideMode.prototype.resumeSlideshow_ = function(opt_interval) { ...@@ -1192,7 +1211,7 @@ SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
}; };
/** /**
* Pause the slideshow. * Pauses the slideshow.
* @private * @private
*/ */
SlideMode.prototype.pauseSlideshow_ = function() { SlideMode.prototype.pauseSlideshow_ = function() {
...@@ -1211,7 +1230,7 @@ SlideMode.prototype.isEditing = function() { ...@@ -1211,7 +1230,7 @@ SlideMode.prototype.isEditing = function() {
}; };
/** /**
* Stop editing. * Stops editing.
* @private * @private
*/ */
SlideMode.prototype.stopEditing_ = function() { SlideMode.prototype.stopEditing_ = function() {
...@@ -1244,9 +1263,11 @@ SlideMode.prototype.toggleEditor = function(opt_event) { ...@@ -1244,9 +1263,11 @@ SlideMode.prototype.toggleEditor = function(opt_event) {
this.editor_.getPrompt().showAt( this.editor_.getPrompt().showAt(
'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName); 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
} }
this.touchHandlers_.enabled = false;
} else { } else {
this.editor_.getPrompt().hide(); this.editor_.getPrompt().hide();
this.editor_.leaveModeGently(); this.editor_.leaveModeGently();
this.touchHandlers_.enabled = true;
} }
}; };
...@@ -1260,7 +1281,7 @@ SlideMode.prototype.print_ = function() { ...@@ -1260,7 +1281,7 @@ SlideMode.prototype.print_ = function() {
}; };
/** /**
* Display the error banner. * Displays the error banner.
* @param {string} message Message. * @param {string} message Message.
* @private * @private
*/ */
...@@ -1272,7 +1293,7 @@ SlideMode.prototype.showErrorBanner_ = function(message) { ...@@ -1272,7 +1293,7 @@ SlideMode.prototype.showErrorBanner_ = function(message) {
}; };
/** /**
* Show/hide the busy spinner. * Shows/hides the busy spinner.
* *
* @param {boolean} on True if show, false if hide. * @param {boolean} on True if show, false if hide.
* @private * @private
...@@ -1294,45 +1315,203 @@ SlideMode.prototype.showSpinner_ = function(on) { ...@@ -1294,45 +1315,203 @@ SlideMode.prototype.showSpinner_ = function(on) {
}; };
/** /**
* Overlay that handles swipe gestures. Changes to the next or previous file. * Apply the change of viewport.
* @param {function(number)} callback A callback accepting the swipe direction */
* (1 means left, -1 right). SlideMode.prototype.applyViewportChange = function() {
this.imageView_.applyViewportChange();
};
/**
* Touch handlers of the slide mode.
* @param {DOMElement} targetElement Event source.
* @param {SlideMode} slideMode Slide mode to be operated by the handler.
* @constructor * @constructor
* @implements {ImageBuffer.Overlay}
*/ */
function SwipeOverlay(callback) { function TouchHandler(targetElement, slideMode) {
this.callback_ = callback; /**
* Event source.
* @type {DOMElement}
*/
this.targetElement_ = targetElement;
/**
* Target of touch operations.
* @type {SlideMode}
* @private
*/
this.slideMode_ = slideMode;
/**
* Flag to enable/disable touch operation.
* @type {boolean}
*/
this.enabled_ = true;
/**
* Whether it is in a touch operation that is started from targetElement or
* not.
* @type {boolean}
* @private
*/
this.touchStarted_ = false;
/**
* The swipe action that should happen only once in an operation is already
* done or not.
* @type {boolean}
* @private
*/
this.done_ = false;
/**
* Event on beginning of the current gesture.
* The variable is updated when the number of touch finger changed.
* @type {TouchEvent}
* @private
*/
this.gestureStartEvent_ = null;
/**
* Last touch event.
* @type {TouchEvent}
* @private
*/
this.lastEvent_ = null;
/**
* Zoom value just after last touch event.
* @type {number}
* @private
*/
this.lastZoom_ = 1.0;
targetElement.addEventListener('touchstart', this.onTouchStart_.bind(this));
var onTouchEventBound = this.onTouchEvent_.bind(this);
targetElement.ownerDocument.addEventListener('touchmove', onTouchEventBound);
targetElement.ownerDocument.addEventListener('touchend', onTouchEventBound);
} }
/** /**
* Inherit ImageBuffer.Overlay. * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
* horizontally it's considered as a swipe gesture (change the current image).
* @type {number}
* @const
*/ */
SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype; TouchHandler.SWIPE_THRESHOLD = 100;
/** /**
* @param {number} x X pointer position. * Obtains distance between fingers.
* @param {number} y Y pointer position. * @param {TouchEvent} event Touch event. It should include more than two
* @param {boolean} touch True if dragging caused by touch. * touches.
* @return {function} The closure to call on drag. * @return {boolean} Distance between touch[0] and touch[1].
*/ */
SwipeOverlay.prototype.getDragHandler = function(x, y, touch) { TouchHandler.getDistance = function(event) {
if (!touch) var touch1 = event.touches[0];
return null; var touch2 = event.touches[1];
var origin = x; var dx = touch1.clientX - touch2.clientX;
var done = false; var dy = touch1.clientY - touch2.clientY;
return function(x, y) { return Math.sqrt(dx * dx + dy * dy);
if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) { };
this.callback_(1);
done = true; TouchHandler.prototype = {
} else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) { /**
this.callback_(-1); * @param {boolean} flag New value.
done = true; */
} set enabled(flag) {
}.bind(this); this.enabled_ = flag;
if (!this.enabled_)
this.stopOperation();
}
}; };
/** /**
* If the user touched the image and moved the finger more than SWIPE_THRESHOLD * Stops the current touch operation.
* horizontally it's considered as a swipe gesture (change the current image).
*/ */
SwipeOverlay.SWIPE_THRESHOLD = 100; TouchHandler.prototype.stopOperation = function() {
this.touchStarted_ = false;
this.done_ = false;
this.gestureStartEvent_ = null;
this.lastEvent_ = null;
this.lastZoom_ = 1.0;
};
TouchHandler.prototype.onTouchStart_ = function(event) {
if (this.enabled_ && event.touches.length === 1)
this.touchStarted_ = true;
};
/**
* @param {event} event Touch event.
*/
TouchHandler.prototype.onTouchEvent_ = function(event) {
// Check if the current touch operation started from the target element or
// not.
if (!this.touchStarted_)
return;
// Check if the current touch operation ends with the event.
if (event.touches.length === 0) {
this.stopOperation();
return;
}
// Check if a new gesture started or not.
if (!this.lastEvent_ ||
this.lastEvent_.touches.length !== event.touches.length) {
if (event.touches.length === 2 ||
event.touches.length === 1) {
this.gestureStartEvent_ = event;
this.lastEvent_ = event;
this.lastZoom_ = this.slideMode_.getViewport().getZoom();
} else {
this.gestureStartEvent_ = null;
this.lastEvent_ = null;
this.lastZoom_ = 1.0;
}
return;
}
// Handle the gesture movement.
var viewport = this.slideMode_.getViewport();
switch (event.touches.length) {
case 1:
if (viewport.isZoomed()) {
// Scrolling an image by swipe.
var dx = event.touches[0].screenX - this.lastEvent_.touches[0].screenX;
var dy = event.touches[0].screenY - this.lastEvent_.touches[0].screenY;
viewport.setOffset(
viewport.getOffsetX() + dx, viewport.getOffsetY() + dy);
this.slideMode_.applyViewportChange();
} else {
// Traversing images by swipe.
if (this.done_)
break;
var dx =
event.touches[0].clientX -
this.gestureStartEvent_.touches[0].clientX;
if (dx > TouchHandler.SWIPE_THRESHOLD) {
this.slideMode_.advanceManually(-1);
this.done_ = true;
} else if (dx < -TouchHandler.SWIPE_THRESHOLD) {
this.slideMode_.advanceManually(1);
this.done_ = true;
}
}
break;
case 2:
// Pinch zoom.
var distance1 = TouchHandler.getDistance(this.lastEvent_);
var distance2 = TouchHandler.getDistance(event);
if (distance1 === 0)
break;
var zoom = distance2 / distance1 * this.lastZoom_;
viewport.setZoom(zoom);
this.slideMode_.applyViewportChange();
break;
}
// Update the last event.
this.lastEvent_ = event;
this.lastZoom_ = viewport.getZoom();
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment