Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / android / java / src / org / chromium / chromoting / TrackingInputHandler.java
blobfec67d22e62f3d8e9cac94993f5f43f7402c502b
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chromoting;
7 import android.content.Context;
8 import android.graphics.Matrix;
9 import android.graphics.PointF;
10 import android.view.GestureDetector;
11 import android.view.MotionEvent;
12 import android.view.ScaleGestureDetector;
13 import android.widget.Scroller;
15 /**
16 * This class implements the cursor-tracking behavior and gestures.
18 public class TrackingInputHandler implements TouchInputHandler {
19 /**
20 * Minimum change to the scaling factor to be recognized as a zoom gesture. Setting lower
21 * values here will result in more frequent canvas redraws during zooming.
23 private static final double MIN_ZOOM_DELTA = 0.05;
25 /**
26 * Maximum allowed zoom level - see {@link #repositionImageWithZoom()}.
28 private static final float MAX_ZOOM_FACTOR = 100.0f;
30 private DesktopViewInterface mViewer;
31 private RenderData mRenderData;
33 private GestureDetector mScroller;
34 private ScaleGestureDetector mZoomer;
35 private TapGestureDetector mTapDetector;
37 /** Used to calculate the physics for flinging the cursor. */
38 private Scroller mFlingScroller;
40 /** Used to disambiguate a 2-finger gesture as a swipe or a pinch. */
41 private SwipePinchDetector mSwipePinchDetector;
43 /**
44 * The current cursor position is stored here as floats, so that the desktop image can be
45 * positioned with sub-pixel accuracy, to give a smoother panning animation at high zoom levels.
47 private PointF mCursorPosition;
49 /**
50 * Used for tracking swipe gestures. Only the Y-direction is needed for responding to swipe-up
51 * or swipe-down.
53 private float mTotalMotionY = 0;
55 /**
56 * Distance in pixels beyond which a motion gesture is considered to be a swipe. This is
57 * initialized using the Context passed into the ctor.
59 private float mSwipeThreshold;
61 /** Mouse-button currently held down, or BUTTON_UNDEFINED otherwise. */
62 private int mHeldButton = BUTTON_UNDEFINED;
64 /**
65 * Set to true to prevent any further movement of the cursor, for example, when showing the
66 * keyboard to prevent the cursor wandering from the area where keystrokes should be sent.
68 private boolean mSuppressCursorMovement = false;
70 /**
71 * Set to true to suppress the fling animation at the end of a gesture, for example, when
72 * dragging whilst a button is held down.
74 private boolean mSuppressFling = false;
76 /**
77 * Set to true when 3-finger swipe gesture is complete, so that further movement doesn't
78 * trigger more swipe actions.
80 private boolean mSwipeCompleted = false;
82 public TrackingInputHandler(DesktopViewInterface viewer, Context context,
83 RenderData renderData) {
84 mViewer = viewer;
85 mRenderData = renderData;
87 GestureListener listener = new GestureListener();
88 mScroller = new GestureDetector(context, listener, null, false);
90 // If long-press is enabled, the gesture-detector will not emit any further onScroll
91 // notifications after the onLongPress notification. Since onScroll is being used for
92 // moving the cursor, it means that the cursor would become stuck if the finger were held
93 // down too long.
94 mScroller.setIsLongpressEnabled(false);
96 mZoomer = new ScaleGestureDetector(context, listener);
97 mTapDetector = new TapGestureDetector(context, listener);
98 mFlingScroller = new Scroller(context);
99 mSwipePinchDetector = new SwipePinchDetector(context);
101 mCursorPosition = new PointF();
103 // The threshold needs to be bigger than the ScaledTouchSlop used by the gesture-detectors,
104 // so that a gesture cannot be both a tap and a swipe. It also needs to be small enough so
105 // that intentional swipes are usually detected.
106 float density = context.getResources().getDisplayMetrics().density;
107 mSwipeThreshold = 40 * density;
111 * Moves the mouse-cursor, injects a mouse-move event and repositions the image.
113 private void moveCursor(float newX, float newY) {
114 synchronized (mRenderData) {
115 // Constrain cursor to the image area.
116 if (newX < 0) newX = 0;
117 if (newY < 0) newY = 0;
118 if (newX > mRenderData.imageWidth) newX = mRenderData.imageWidth;
119 if (newY > mRenderData.imageHeight) newY = mRenderData.imageHeight;
120 mCursorPosition.set(newX, newY);
121 repositionImage();
124 mViewer.injectMouseEvent((int) newX, (int) newY, BUTTON_UNDEFINED, false);
128 * Repositions the image by translating it (without affecting the zoom level) to place the
129 * cursor close to the center of the screen.
131 private void repositionImage() {
132 synchronized (mRenderData) {
133 // Get the current cursor position in screen coordinates.
134 float[] cursorScreen = {mCursorPosition.x, mCursorPosition.y};
135 mRenderData.transform.mapPoints(cursorScreen);
137 // Translate so the cursor is displayed in the middle of the screen.
138 mRenderData.transform.postTranslate(
139 (float) mRenderData.screenWidth / 2 - cursorScreen[0],
140 (float) mRenderData.screenHeight / 2 - cursorScreen[1]);
142 // Now the cursor is displayed in the middle of the screen, see if the image can be
143 // panned so that more of it is visible. The primary goal is to show as much of the
144 // image as possible. The secondary goal is to keep the cursor in the middle.
146 // Get the coordinates of the desktop rectangle (top-left/bottom-right corners) in
147 // screen coordinates. Order is: left, top, right, bottom.
148 float[] rectScreen = {0, 0, mRenderData.imageWidth, mRenderData.imageHeight};
149 mRenderData.transform.mapPoints(rectScreen);
151 float leftDelta = rectScreen[0];
152 float rightDelta = rectScreen[2] - mRenderData.screenWidth;
153 float topDelta = rectScreen[1];
154 float bottomDelta = rectScreen[3] - mRenderData.screenHeight;
155 float xAdjust = 0;
156 float yAdjust = 0;
158 if (rectScreen[2] - rectScreen[0] < mRenderData.screenWidth) {
159 // Image is narrower than the screen, so center it.
160 xAdjust = -(rightDelta + leftDelta) / 2;
161 } else if (leftDelta > 0 && rightDelta > 0) {
162 // Panning the image left will show more of it.
163 xAdjust = -Math.min(leftDelta, rightDelta);
164 } else if (leftDelta < 0 && rightDelta < 0) {
165 // Pan the image right.
166 xAdjust = Math.min(-leftDelta, -rightDelta);
169 // Apply similar logic for yAdjust.
170 if (rectScreen[3] - rectScreen[1] < mRenderData.screenHeight) {
171 yAdjust = -(bottomDelta + topDelta) / 2;
172 } else if (topDelta > 0 && bottomDelta > 0) {
173 yAdjust = -Math.min(topDelta, bottomDelta);
174 } else if (topDelta < 0 && bottomDelta < 0) {
175 yAdjust = Math.min(-topDelta, -bottomDelta);
178 mRenderData.transform.postTranslate(xAdjust, yAdjust);
180 mViewer.transformationChanged();
184 * Repositions the image by translating and zooming it, to keep the zoom level within sensible
185 * limits. The minimum zoom level is chosen to avoid black space around all 4 sides. The
186 * maximum zoom level is set arbitrarily, so that the user can zoom out again in a reasonable
187 * time, and to prevent arithmetic overflow problems from displaying the image.
189 private void repositionImageWithZoom() {
190 synchronized (mRenderData) {
191 // Avoid division by zero in case this gets called before the image size is initialized.
192 if (mRenderData.imageWidth == 0 || mRenderData.imageHeight == 0) {
193 return;
196 // Zoom out if the zoom level is too high.
197 float currentZoomLevel = mRenderData.transform.mapRadius(1.0f);
198 if (currentZoomLevel > MAX_ZOOM_FACTOR) {
199 mRenderData.transform.setScale(MAX_ZOOM_FACTOR, MAX_ZOOM_FACTOR);
202 // Get image size scaled to screen coordinates.
203 float[] imageSize = {mRenderData.imageWidth, mRenderData.imageHeight};
204 mRenderData.transform.mapVectors(imageSize);
206 if (imageSize[0] < mRenderData.screenWidth && imageSize[1] < mRenderData.screenHeight) {
207 // Displayed image is too small in both directions, so apply the minimum zoom
208 // level needed to fit either the width or height.
209 float scale = Math.min((float) mRenderData.screenWidth / mRenderData.imageWidth,
210 (float) mRenderData.screenHeight / mRenderData.imageHeight);
211 mRenderData.transform.setScale(scale, scale);
214 repositionImage();
218 /** Injects a button event using the current cursor location. */
219 private void injectButtonEvent(int button, boolean pressed) {
220 mViewer.injectMouseEvent((int) mCursorPosition.x, (int) mCursorPosition.y, button, pressed);
223 /** Processes a (multi-finger) swipe gesture. */
224 private boolean onSwipe() {
225 if (mTotalMotionY > mSwipeThreshold) {
226 // Swipe down occurred.
227 mViewer.showActionBar();
228 } else if (mTotalMotionY < -mSwipeThreshold) {
229 // Swipe up occurred.
230 mViewer.showKeyboard();
231 } else {
232 return false;
235 mSuppressCursorMovement = true;
236 mSuppressFling = true;
237 mSwipeCompleted = true;
238 return true;
241 /** Injects a button-up event if the button is currently held down (during a drag event). */
242 private void releaseAnyHeldButton() {
243 if (mHeldButton != BUTTON_UNDEFINED) {
244 injectButtonEvent(mHeldButton, false);
245 mHeldButton = BUTTON_UNDEFINED;
249 @Override
250 public boolean onTouchEvent(MotionEvent event) {
251 // Avoid short-circuit logic evaluation - ensure all gesture detectors see all events so
252 // that they generate correct notifications.
253 boolean handled = mScroller.onTouchEvent(event);
254 handled |= mZoomer.onTouchEvent(event);
255 handled |= mTapDetector.onTouchEvent(event);
256 mSwipePinchDetector.onTouchEvent(event);
258 switch (event.getActionMasked()) {
259 case MotionEvent.ACTION_DOWN:
260 mViewer.setAnimationEnabled(false);
261 mSuppressCursorMovement = false;
262 mSuppressFling = false;
263 mSwipeCompleted = false;
264 break;
266 case MotionEvent.ACTION_POINTER_DOWN:
267 mTotalMotionY = 0;
268 break;
270 case MotionEvent.ACTION_UP:
271 releaseAnyHeldButton();
272 break;
274 default:
275 break;
277 return handled;
280 @Override
281 public void onScreenConfigurationChanged() {
284 @Override
285 public void onClientSizeChanged(int width, int height) {
286 repositionImageWithZoom();
289 @Override
290 public void onHostSizeChanged(int width, int height) {
291 moveCursor((float) width / 2, (float) height / 2);
292 repositionImageWithZoom();
295 @Override
296 public void processAnimation() {
297 int previousX = mFlingScroller.getCurrX();
298 int previousY = mFlingScroller.getCurrY();
299 if (!mFlingScroller.computeScrollOffset()) {
300 mViewer.setAnimationEnabled(false);
301 return;
303 int deltaX = mFlingScroller.getCurrX() - previousX;
304 int deltaY = mFlingScroller.getCurrY() - previousY;
305 float[] delta = {deltaX, deltaY};
306 synchronized (mRenderData) {
307 Matrix canvasToImage = new Matrix();
308 mRenderData.transform.invert(canvasToImage);
309 canvasToImage.mapVectors(delta);
312 moveCursor(mCursorPosition.x + delta[0], mCursorPosition.y + delta[1]);
315 /** Responds to touch events filtered by the gesture detectors. */
316 private class GestureListener extends GestureDetector.SimpleOnGestureListener
317 implements ScaleGestureDetector.OnScaleGestureListener,
318 TapGestureDetector.OnTapListener {
320 * Called when the user drags one or more fingers across the touchscreen.
322 @Override
323 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
324 int pointerCount = e2.getPointerCount();
325 if (pointerCount == 3 && !mSwipeCompleted) {
326 // Note that distance values are reversed. For example, dragging a finger in the
327 // direction of increasing Y coordinate (downwards) results in distanceY being
328 // negative.
329 mTotalMotionY -= distanceY;
330 return onSwipe();
333 if (pointerCount == 2 && mSwipePinchDetector.isSwiping()) {
334 mViewer.injectMouseWheelDeltaEvent(-(int) distanceX, -(int) distanceY);
336 // Prevent the cursor being moved or flung by the gesture.
337 mSuppressCursorMovement = true;
338 return true;
341 if (pointerCount != 1 || mSuppressCursorMovement) {
342 return false;
345 float[] delta = {distanceX, distanceY};
346 synchronized (mRenderData) {
347 Matrix canvasToImage = new Matrix();
348 mRenderData.transform.invert(canvasToImage);
349 canvasToImage.mapVectors(delta);
352 moveCursor(mCursorPosition.x - delta[0], mCursorPosition.y - delta[1]);
353 return true;
357 * Called when a fling gesture is recognized.
359 @Override
360 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
361 // If cursor movement is suppressed, fling also needs to be suppressed, as the
362 // gesture-detector will still generate onFling() notifications based on movement of
363 // the fingers, which would result in unwanted cursor movement.
364 if (mSuppressCursorMovement || mSuppressFling) {
365 return false;
368 // The fling physics calculation is based on screen coordinates, so that it will
369 // behave consistently at different zoom levels (and will work nicely at high zoom
370 // levels, since |mFlingScroller| outputs integer coordinates). However, the desktop
371 // will usually be panned as the cursor is moved across the desktop, which means the
372 // transformation mapping from screen to desktop coordinates will change. To deal with
373 // this, the cursor movement is computed from relative coordinate changes from
374 // |mFlingScroller|. This means the fling can be started at (0, 0) with no bounding
375 // constraints - the cursor is already constrained by the desktop size.
376 mFlingScroller.fling(0, 0, (int) velocityX, (int) velocityY, Integer.MIN_VALUE,
377 Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
378 // Initialize the scroller's current offset coordinates, since they are used for
379 // calculating the delta values.
380 mFlingScroller.computeScrollOffset();
381 mViewer.setAnimationEnabled(true);
382 return true;
385 /** Called when the user is in the process of pinch-zooming. */
386 @Override
387 public boolean onScale(ScaleGestureDetector detector) {
388 if (!mSwipePinchDetector.isPinching()) {
389 return false;
392 if (Math.abs(detector.getScaleFactor() - 1) < MIN_ZOOM_DELTA) {
393 return false;
396 float scaleFactor = detector.getScaleFactor();
397 synchronized (mRenderData) {
398 mRenderData.transform.postScale(
399 scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
401 repositionImageWithZoom();
402 return true;
405 /** Called whenever a gesture starts. Always accepts the gesture so it isn't ignored. */
406 @Override
407 public boolean onDown(MotionEvent e) {
408 return true;
412 * Called when the user starts to zoom. Always accepts the zoom so that
413 * onScale() can decide whether to respond to it.
415 @Override
416 public boolean onScaleBegin(ScaleGestureDetector detector) {
417 return true;
420 /** Called when the user is done zooming. Defers to onScale()'s judgement. */
421 @Override
422 public void onScaleEnd(ScaleGestureDetector detector) {
423 onScale(detector);
426 /** Maps the number of fingers in a tap or long-press gesture to a mouse-button. */
427 private int mouseButtonFromPointerCount(int pointerCount) {
428 switch (pointerCount) {
429 case 1:
430 return BUTTON_LEFT;
431 case 2:
432 return BUTTON_RIGHT;
433 case 3:
434 return BUTTON_MIDDLE;
435 default:
436 return BUTTON_UNDEFINED;
440 /** Called when the user taps the screen with one or more fingers. */
441 @Override
442 public boolean onTap(int pointerCount) {
443 int button = mouseButtonFromPointerCount(pointerCount);
444 if (button == BUTTON_UNDEFINED) {
445 return false;
446 } else {
447 injectButtonEvent(button, true);
448 injectButtonEvent(button, false);
449 return true;
453 /** Called when a long-press is triggered for one or more fingers. */
454 @Override
455 public void onLongPress(int pointerCount) {
456 mHeldButton = mouseButtonFromPointerCount(pointerCount);
457 if (mHeldButton != BUTTON_UNDEFINED) {
458 injectButtonEvent(mHeldButton, true);
459 mViewer.showLongPressFeedback();
460 mSuppressFling = true;