diff options
author | lambroslambrou@chromium.org <lambroslambrou@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-25 16:50:20 +0000 |
---|---|---|
committer | lambroslambrou@chromium.org <lambroslambrou@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-25 16:50:20 +0000 |
commit | ca74711f5188fa335c14d2f0f12aabf163002f0e (patch) | |
tree | 0fa9f8bc3bb4d9479173d6201e233b714515aaa4 /remoting | |
parent | 86576a61dbe2907b004cc1ac639ba6822264a227 (diff) | |
download | chromium_src-ca74711f5188fa335c14d2f0f12aabf163002f0e.zip chromium_src-ca74711f5188fa335c14d2f0f12aabf163002f0e.tar.gz chromium_src-ca74711f5188fa335c14d2f0f12aabf163002f0e.tar.bz2 |
Cursor-tracking in Android Chromoting client.
This is a first step towards implementing the new gestures:
* Single-finger swipe moves the cursor (and pans the image in the
opposite direction).
* Single-finger tap injects left-click.
* Two-finger tap injects right-click.
* Two-finger pinch to zoom (as before)
* Three-finger tap to show the action-bar (as before)
There are some regressions (to keep this CL simple) but I plan to
address these in follow-up CLs:
* No constraints on image-positioning or zoom-level.
* Cannot do left-button dragging.
* Cannot send middle-button press.
* Removed minimum-scroll constraint, as that makes it difficult to
control the cursor precisely.
BUG=270348
Review URL: https://codereview.chromium.org/26336004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@231042 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
7 files changed, 434 insertions, 354 deletions
diff --git a/remoting/android/java/src/org/chromium/chromoting/Desktop.java b/remoting/android/java/src/org/chromium/chromoting/Desktop.java index 8a9917d..85e2493 100644 --- a/remoting/android/java/src/org/chromium/chromoting/Desktop.java +++ b/remoting/android/java/src/org/chromium/chromoting/Desktop.java @@ -42,7 +42,7 @@ public class Desktop extends Activity { @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - remoteHostDesktop.requestRecheckConstrainingDimension(); + remoteHostDesktop.onScreenConfigurationChanged(); } /** Called to initialize the action bar. */ diff --git a/remoting/android/java/src/org/chromium/chromoting/DesktopView.java b/remoting/android/java/src/org/chromium/chromoting/DesktopView.java index 8209d28..26cf83a 100644 --- a/remoting/android/java/src/org/chromium/chromoting/DesktopView.java +++ b/remoting/android/java/src/org/chromium/chromoting/DesktopView.java @@ -9,16 +9,12 @@ import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; -import android.os.Bundle; import android.os.Looper; import android.text.InputType; import android.util.Log; -import android.view.GestureDetector; import android.view.MotionEvent; -import android.view.ScaleGestureDetector; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.inputmethod.EditorInfo; @@ -32,62 +28,16 @@ import org.chromium.chromoting.jni.JniInterface; * multitouch pan and zoom gestures, and collects and forwards input events. */ /** GUI element that holds the drawing canvas. */ -public class DesktopView extends SurfaceView implements Runnable, SurfaceHolder.Callback { - /** - * *Square* of the minimum displacement (in pixels) to be recognized as a scroll gesture. - * Setting this to a lower value forces more frequent canvas redraws during scrolling. - */ - private static final int MIN_SCROLL_DISTANCE = 8 * 8; - - /** - * Minimum change to the scaling factor to be recognized as a zoom gesture. Setting lower - * values here will result in more frequent canvas redraws during zooming. - */ - private static final double MIN_ZOOM_FACTOR = 0.05; - - /* - * These constants must match those in the generated struct protoc::MouseEvent_MouseButton. - */ - private static final int BUTTON_UNDEFINED = 0; - private static final int BUTTON_LEFT = 1; - private static final int BUTTON_RIGHT = 3; - - /** Specifies one dimension of an image. */ - private static enum Constraint { - UNDEFINED, WIDTH, HEIGHT - } - +public class DesktopView extends SurfaceView implements DesktopViewInterface, Runnable, + SurfaceHolder.Callback { + private RenderData mRenderData; + private TouchInputHandler mInputHandler; private ActionBar mActionBar; - private GestureDetector mScroller; - private ScaleGestureDetector mZoomer; - - /** Stores pan and zoom configuration and converts image coordinates to screen coordinates. */ - private Matrix mTransform; - - private int mScreenWidth; - private int mScreenHeight; - - /** - * Specifies the position, in image coordinates, at which the cursor image will be drawn. - * This will normally be at the location of the most recently injected motion event. - */ - private Point mCursorPosition; - - /** Specifies the dimension by which the zoom level is being lower-bounded. */ - private Constraint mConstraint; - - /** Whether the dimension of constraint should be reckecked on the next aspect ratio change. */ - private boolean mRecheckConstraint; - - /** Whether the right edge of the image was visible on-screen during the last render. */ - private boolean mRightUsedToBeOut; - - /** Whether the bottom edge of the image was visible on-screen during the last render. */ - private boolean mBottomUsedToBeOut; - - private int mMouseButton; - private boolean mMousePressed; + // Flag to prevent multiple repaint requests from being backed up. Requests for repainting will + // be dropped if this is already set to true. This is used by the main thread and the painting + // thread, so the access should be synchronized on |mRenderData|. + private boolean mRepaintPending; public DesktopView(Activity context) { super(context); @@ -95,26 +45,30 @@ public class DesktopView extends SurfaceView implements Runnable, SurfaceHolder. // Give this view keyboard focus, allowing us to customize the soft keyboard's settings. setFocusableInTouchMode(true); + mRenderData = new RenderData(); + mInputHandler = new TrackingInputHandler(this, context, mRenderData); mActionBar = context.getActionBar(); + mRepaintPending = false; getHolder().addCallback(this); - DesktopListener listener = new DesktopListener(); - mScroller = new GestureDetector(context, listener, null, false); - mZoomer = new ScaleGestureDetector(context, listener); - - mTransform = new Matrix(); - mScreenWidth = 0; - mScreenHeight = 0; - mCursorPosition = new Point(); - - mConstraint = Constraint.UNDEFINED; - mRecheckConstraint = false; + } - mRightUsedToBeOut = false; - mBottomUsedToBeOut = false; + /** + * Request repainting of the desktop view. + */ + void requestRepaint() { + synchronized (mRenderData) { + if (mRepaintPending) { + return; + } + mRepaintPending = true; + } + JniInterface.redrawGraphics(); + } - mMouseButton = BUTTON_UNDEFINED; - mMousePressed = false; + /** Called whenever the screen configuration is changed. */ + public void onScreenConfigurationChanged() { + mInputHandler.onScreenConfigurationChanged(); } /** @@ -129,97 +83,28 @@ public class DesktopView extends SurfaceView implements Runnable, SurfaceHolder. } Bitmap image = JniInterface.getVideoFrame(); - Canvas canvas = getHolder().lockCanvas(); - synchronized (mTransform) { - canvas.setMatrix(mTransform); - - // Internal parameters of the transformation matrix. - float[] values = new float[9]; - mTransform.getValues(values); - - // Screen coordinates of two defining points of the image. - float[] topleft = {0, 0}; - mTransform.mapPoints(topleft); - float[] bottomright = {image.getWidth(), image.getHeight()}; - mTransform.mapPoints(bottomright); - - // Whether to rescale and recenter the view. - boolean recenter = false; - - if (mConstraint == Constraint.UNDEFINED) { - mConstraint = (double)image.getWidth()/image.getHeight() > - (double)mScreenWidth/mScreenHeight ? Constraint.WIDTH : Constraint.HEIGHT; - recenter = true; // We always rescale and recenter after a rotation. - } - if (mConstraint == Constraint.WIDTH && - ((int)(bottomright[0] - topleft[0] + 0.5) < mScreenWidth || recenter)) { - // The vertical edges of the image are flush against the device's screen edges - // when the entire host screen is visible, and the user has zoomed out too far. - float imageMiddle = (float)image.getHeight() / 2; - float screenMiddle = (float)mScreenHeight / 2; - mTransform.setPolyToPoly( - new float[] {0, imageMiddle, image.getWidth(), imageMiddle}, 0, - new float[] {0, screenMiddle, mScreenWidth, screenMiddle}, 0, 2); - } else if (mConstraint == Constraint.HEIGHT && - ((int)(bottomright[1] - topleft[1] + 0.5) < mScreenHeight || recenter)) { - // The horizontal image edges are flush against the device's screen edges when - // the entire host screen is visible, and the user has zoomed out too far. - float imageCenter = (float)image.getWidth() / 2; - float screenCenter = (float)mScreenWidth / 2; - mTransform.setPolyToPoly( - new float[] {imageCenter, 0, imageCenter, image.getHeight()}, 0, - new float[] {screenCenter, 0, screenCenter, mScreenHeight}, 0, 2); - } else { - // It's fine for both members of a pair of image edges to be within the screen - // edges (or "out of bounds"); that simply means that the image is zoomed out as - // far as permissible. And both members of a pair can obviously be outside the - // screen's edges, which indicates that the image is zoomed in to far to see the - // whole host screen. However, if only one of a pair of edges has entered the - // screen, the user is attempting to scroll into a blank area of the canvas. - - // A value of true means the corresponding edge has entered the screen's borders. - boolean leftEdgeOutOfBounds = values[Matrix.MTRANS_X] > 0; - boolean topEdgeOutOfBounds = values[Matrix.MTRANS_Y] > 0; - boolean rightEdgeOutOfBounds = bottomright[0] < mScreenWidth; - boolean bottomEdgeOutOfBounds = bottomright[1] < mScreenHeight; - - // Prevent the user from scrolling past the left or right edge of the image. - if (leftEdgeOutOfBounds != rightEdgeOutOfBounds) { - if (leftEdgeOutOfBounds != mRightUsedToBeOut) { - // Make the left edge of the image flush with the left screen edge. - values[Matrix.MTRANS_X] = 0; - } - else { - // Make the right edge of the image flush with the right screen edge. - values[Matrix.MTRANS_X] += mScreenWidth - bottomright[0]; - } - } else { - // The else prevents this from being updated during the repositioning process, - // in which case the view would begin to oscillate. - mRightUsedToBeOut = rightEdgeOutOfBounds; - } - - // Prevent the user from scrolling past the top or bottom edge of the image. - if (topEdgeOutOfBounds != bottomEdgeOutOfBounds) { - if (topEdgeOutOfBounds != mBottomUsedToBeOut) { - // Make the top edge of the image flush with the top screen edge. - values[Matrix.MTRANS_Y] = 0; - } else { - // Make the bottom edge of the image flush with the bottom screen edge. - values[Matrix.MTRANS_Y] += mScreenHeight - bottomright[1]; - } - } - else { - // The else prevents this from being updated during the repositioning process, - // in which case the view would begin to oscillate. - mBottomUsedToBeOut = bottomEdgeOutOfBounds; - } - - mTransform.setValues(values); + int width = image.getWidth(); + int height = image.getHeight(); + boolean sizeChanged = false; + synchronized (mRenderData) { + if (mRenderData.imageWidth != width || mRenderData.imageHeight != height) { + // TODO(lambroslambrou): Move this code into a sizeChanged() callback, to be + // triggered from JniInterface (on the display thread) when the remote screen size + // changes. + mRenderData.imageWidth = width; + mRenderData.imageHeight = height; + sizeChanged = true; } + } + if (sizeChanged) { + mInputHandler.onHostSizeChanged(width, height); + } - canvas.setMatrix(mTransform); + Canvas canvas = getHolder().lockCanvas(); + synchronized (mRenderData) { + mRepaintPending = false; + canvas.setMatrix(mRenderData.transform); } canvas.drawColor(Color.BLACK); @@ -228,9 +113,9 @@ public class DesktopView extends SurfaceView implements Runnable, SurfaceHolder. if (cursorBitmap != null) { Point hotspot = JniInterface.getCursorHotspot(); int bitmapX, bitmapY; - synchronized (mCursorPosition) { - bitmapX = mCursorPosition.x - hotspot.x; - bitmapY = mCursorPosition.y - hotspot.y; + synchronized (mRenderData) { + bitmapX = mRenderData.cursorPosition.x - hotspot.x; + bitmapY = mRenderData.cursorPosition.y - hotspot.y; } canvas.drawBitmap(cursorBitmap, bitmapX, bitmapY, new Paint()); } @@ -238,43 +123,21 @@ public class DesktopView extends SurfaceView implements Runnable, SurfaceHolder. } /** - * Causes the next canvas redraw to perform a check for which screen dimension more tightly - * constrains the view of the image. This should be called between the time that a screen size - * change is requested and the time it actually occurs. If it is not called in such a case, the - * screen will not be rearranged as aggressively (which is desirable when the software keyboard - * appears in order to allow it to cover the image without forcing a resize). - */ - public void requestRecheckConstrainingDimension() { - mRecheckConstraint = true; - } - - /** - * Called after the canvas is initially created, then after every - * subsequent resize, as when the display is rotated. + * Called after the canvas is initially created, then after every subsequent resize, as when + * the display is rotated. */ @Override - public void surfaceChanged( - SurfaceHolder holder, int format, int width, int height) { + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { mActionBar.hide(); - synchronized (mTransform) { - mScreenWidth = width; - mScreenHeight = height; - - if (mRecheckConstraint) { - mConstraint = Constraint.UNDEFINED; - mRecheckConstraint = false; - } + synchronized (mRenderData) { + mRenderData.screenWidth = width; + mRenderData.screenHeight = height; } - synchronized (mCursorPosition) { - mCursorPosition.x = width / 2; - mCursorPosition.y = height / 2; - } + mInputHandler.onClientSizeChanged(width, height); - if (!JniInterface.redrawGraphics()) { - JniInterface.provideRedrawCallback(this); - } + JniInterface.provideRedrawCallback(this); } /** Called when the canvas is first created. */ @@ -313,174 +176,53 @@ public class DesktopView extends SurfaceView implements Runnable, SurfaceHolder. return null; } - /** Called when a mouse action is made. */ - private void handleMouseMovement(float x, float y, int button, boolean pressed) { - float[] coordinates = {x, y}; - - // Coordinates are relative to the canvas, but we need image coordinates. - Matrix canvasToImage = new Matrix(); - synchronized (mTransform) { - mTransform.invert(canvasToImage); - } - canvasToImage.mapPoints(coordinates); - - int imageX = (int)coordinates[0]; - int imageY = (int)coordinates[1]; - - synchronized (mCursorPosition) { - mCursorPosition.x = imageX; - mCursorPosition.y = imageY; - } - - // Coordinates are now relative to the image, so transmit them to the host. - JniInterface.mouseAction(imageX, imageY, button, pressed); - - // TODO(lambroslambrou): Optimize this to repaint only the areas covered by the old and new - // cursor positions. - JniInterface.redrawGraphics(); - } - - /** - * Called whenever the user attempts to touch the canvas. Forwards such - * events to the appropriate gesture detector until one accepts them. - */ + /** Called whenever the user attempts to touch the canvas. */ @Override public boolean onTouchEvent(MotionEvent event) { - if (event.getPointerCount() == 3) { - mActionBar.show(); - } - - boolean handled = mScroller.onTouchEvent(event) || mZoomer.onTouchEvent(event); - - if (event.getPointerCount() == 1) { - float x = event.getRawX(); - float y = event.getY(); - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - Log.i("mouse", "Found a finger"); - mMouseButton = BUTTON_UNDEFINED; - mMousePressed = false; - break; - - case MotionEvent.ACTION_MOVE: - Log.i("mouse", "Finger is dragging"); - if (mMouseButton == BUTTON_UNDEFINED) { - Log.i("mouse", "\tStarting left click"); - mMouseButton = BUTTON_LEFT; - mMousePressed = true; - } - break; - - case MotionEvent.ACTION_UP: - Log.i("mouse", "Lost the finger"); - if (mMouseButton == BUTTON_UNDEFINED) { - // The user pressed and released without moving: do left click and release. - Log.i("mouse", "\tStarting and finishing left click"); - handleMouseMovement(x, y, BUTTON_LEFT, true); - mMouseButton = BUTTON_LEFT; - mMousePressed = false; - } - else if (mMousePressed) { - Log.i("mouse", "\tReleasing the currently-pressed button"); - mMousePressed = false; - } - else { - Log.w("mouse", "Button already in released state before gesture ended"); - } - break; - - default: - return handled; - } - handleMouseMovement(x, y, mMouseButton, mMousePressed); - - return true; - } - - return handled; + return mInputHandler.onTouchEvent(event); } - /** Responds to touch events filtered by the gesture detectors. */ - private class DesktopListener extends GestureDetector.SimpleOnGestureListener - implements ScaleGestureDetector.OnScaleGestureListener { - /** - * Called when the user is scrolling. We refuse to accept or process the event unless it - * is being performed with 2 or more touch points, in order to reserve single-point touch - * events for emulating mouse input. - */ - @Override - public boolean onScroll( - MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - if (e2.getPointerCount() < 2 || - Math.pow(distanceX, 2) + Math.pow(distanceY, 2) < MIN_SCROLL_DISTANCE) { - return false; - } - - synchronized (mTransform) { - mTransform.postTranslate(-distanceX, -distanceY); - } - JniInterface.redrawGraphics(); - return true; - } - - /** Called when the user is in the process of pinch-zooming. */ - @Override - public boolean onScale(ScaleGestureDetector detector) { - if (Math.abs(detector.getScaleFactor() - 1) < MIN_ZOOM_FACTOR) { - return false; + @Override + public void injectMouseEvent(int x, int y, int button, boolean pressed) { + boolean cursorMoved = false; + synchronized (mRenderData) { + // Test if the cursor actually moved, which requires repainting the cursor. This + // requires that the TouchInputHandler doesn't mutate |mRenderData.cursorPosition| + // directly. + if (x != mRenderData.cursorPosition.x) { + mRenderData.cursorPosition.x = x; + cursorMoved = true; } - - synchronized (mTransform) { - float scaleFactor = detector.getScaleFactor(); - mTransform.postScale( - scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY()); + if (y != mRenderData.cursorPosition.y) { + mRenderData.cursorPosition.y = y; + cursorMoved = true; } - JniInterface.redrawGraphics(); - return true; } - /** Called whenever a gesture starts. Always accepts the gesture so it isn't ignored. */ - @Override - public boolean onDown(MotionEvent e) { - return true; + if (button == TouchInputHandler.BUTTON_UNDEFINED && !cursorMoved) { + // No need to inject anything or repaint. + return; } - /** - * Called when the user starts to zoom. Always accepts the zoom so that - * onScale() can decide whether to respond to it. - */ - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - return true; + JniInterface.mouseAction(x, y, button, pressed); + if (cursorMoved) { + // TODO(lambroslambrou): Optimize this by only repainting the affected areas. + requestRepaint(); } + } - /** Called when the user is done zooming. Defers to onScale()'s judgement. */ - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - onScale(detector); - } - - /** Called when the user holds down on the screen. Starts a right-click. */ - @Override - public void onLongPress(MotionEvent e) { - if (e.getPointerCount() > 1) { - return; - } - - float x = e.getRawX(); - float y = e.getY(); + @Override + public void showActionBar() { + mActionBar.show(); + } - Log.i("mouse", "Finger held down"); - if (mMousePressed) { - Log.i("mouse", "\tReleasing the currently-pressed button"); - handleMouseMovement(x, y, mMouseButton, false); - } + @Override + public void showKeyboard() { + // TODO(lambroslambrou): Implement this. + } - Log.i("mouse", "\tStarting right click"); - mMouseButton = BUTTON_RIGHT; - mMousePressed = true; - handleMouseMovement(x, y, mMouseButton, mMousePressed); - } + @Override + public void transformationChanged() { + requestRepaint(); } } diff --git a/remoting/android/java/src/org/chromium/chromoting/DesktopViewInterface.java b/remoting/android/java/src/org/chromium/chromoting/DesktopViewInterface.java new file mode 100644 index 0000000..ba9e1e6 --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/DesktopViewInterface.java @@ -0,0 +1,25 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chromoting; + +/** + * Callback interface to allow the TouchInputHandler to request actions on the DesktopView. + */ +interface DesktopViewInterface { + /** Inject a mouse-move event, with optional button press/release. */ + public void injectMouseEvent(int x, int y, int button, boolean pressed); + + /** Shows the action bar. */ + public void showActionBar(); + + /** Shows the software keyboard. */ + public void showKeyboard(); + + /** + * Informs the view that its transformation matrix (for rendering the remote desktop bitmap) + * has been changed by the TouchInputHandler, which requires repainting. + */ + public void transformationChanged(); +} diff --git a/remoting/android/java/src/org/chromium/chromoting/RenderData.java b/remoting/android/java/src/org/chromium/chromoting/RenderData.java new file mode 100644 index 0000000..f161c9f --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/RenderData.java @@ -0,0 +1,28 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chromoting; + +import android.graphics.Matrix; +import android.graphics.Point; + +/** + * This class stores data that needs to be accessed on both the display thread and the + * event-processing thread. + */ +public class RenderData { + /** Stores pan and zoom configuration and converts image coordinates to screen coordinates. */ + public Matrix transform = new Matrix(); + + public int screenWidth = 0; + public int screenHeight = 0; + public int imageWidth = 0; + public int imageHeight = 0; + + /** + * Specifies the position, in image coordinates, at which the cursor image will be drawn. + * This will normally be at the location of the most recently injected motion event. + */ + public Point cursorPosition = new Point(); +} diff --git a/remoting/android/java/src/org/chromium/chromoting/TouchInputHandler.java b/remoting/android/java/src/org/chromium/chromoting/TouchInputHandler.java new file mode 100644 index 0000000..85578b6 --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/TouchInputHandler.java @@ -0,0 +1,49 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chromoting; + +import android.view.MotionEvent; + +/** + * This interface allows multiple styles of touchscreen UI to be implemented and dynamically + * switched. The DesktopView passes the low-level touchscreen events and other events via this + * interface. The implementation recognizes and processes the touchscreen gestures, and then + * performs actions on the DesktopView (such as panning/zooming the display, injecting input, or + * showing/hiding UI elements). + */ +interface TouchInputHandler { + // These constants must match those in the generated struct protoc::MouseEvent_MouseButton. + public static final int BUTTON_UNDEFINED = 0; + public static final int BUTTON_LEFT = 1; + public static final int BUTTON_MIDDLE = 2; + public static final int BUTTON_RIGHT = 3; + + /** + * Processes a touch event. This should be called by the View in its onTouchEvent() handler. + */ + boolean onTouchEvent(MotionEvent event); + + /** + * Called when the screen configuration is changed, such as when the screen is rotated or an + * external display is plugged in. This is not called if the client display area changes as a + * result of showing/hiding UI elements such as a keyboard. For example, an implementation + * could set a flag to reset the zoom level when the screen is rotated, but not when the + * software keyboard appears. After this is called, the onClientSizeChanged() method will + * shortly be called with the dimensions of the new client display area. + */ + void onScreenConfigurationChanged(); + + /** + * Called whenever the client display area changes size. The caller will handle repainting + * after this method returns. + */ + void onClientSizeChanged(int width, int height); + + /** + * Called when the host screen size is changed. The caller will handle repainting after this + * method returns. + */ + void onHostSizeChanged(int width, int height); +} diff --git a/remoting/android/java/src/org/chromium/chromoting/TrackingInputHandler.java b/remoting/android/java/src/org/chromium/chromoting/TrackingInputHandler.java new file mode 100644 index 0000000..30b66cc --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/TrackingInputHandler.java @@ -0,0 +1,240 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chromoting; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +/** + * This class implements the cursor-tracking behavior and gestures. + */ +public class TrackingInputHandler implements TouchInputHandler { + /** + * Minimum change to the scaling factor to be recognized as a zoom gesture. Setting lower + * values here will result in more frequent canvas redraws during zooming. + */ + private static final double MIN_ZOOM_DELTA = 0.05; + + private DesktopViewInterface mViewer; + private RenderData mRenderData; + + private GestureDetector mScroller; + private ScaleGestureDetector mZoomer; + + /** + * The current cursor position is stored here as floats, so that the desktop image can be + * positioned with sub-pixel accuracy, to give a smoother panning animation at high zoom levels. + */ + private PointF mCursorPosition; + + private int mMouseButton; + + /** + * Distinguish between finger tap and swipe. One-finger down then up should inject a + * left-click event. But if the finger is dragged before being released, this should move + * the cursor without injecting any button event. This flag is set when a motion event is + * detected. + */ + private boolean mFingerMoved = false; + + public TrackingInputHandler(DesktopViewInterface viewer, Context context, + RenderData renderData) { + mViewer = viewer; + mRenderData = renderData; + + GestureListener listener = new GestureListener(); + mScroller = new GestureDetector(context, listener, null, false); + + // If long-press is enabled, the gesture-detector will not emit any further onScroll + // notifications after the onLongPress notification. Since onScroll is being used for + // moving the cursor, it means that the cursor would become stuck if the finger were held + // down too long. + mScroller.setIsLongpressEnabled(false); + + mZoomer = new ScaleGestureDetector(context, listener); + + mCursorPosition = new PointF(); + + mMouseButton = BUTTON_UNDEFINED; + mFingerMoved = false; + } + + /** + * Moves the mouse-cursor, injects a mouse-move event and repositions the image. + */ + private void moveCursor(float newX, float newY) { + synchronized (mRenderData) { + // Constrain cursor to the image area. + if (newX < 0) newX = 0; + if (newY < 0) newY = 0; + if (newX > mRenderData.imageWidth) newX = mRenderData.imageWidth; + if (newY > mRenderData.imageHeight) newY = mRenderData.imageHeight; + mCursorPosition.set(newX, newY); + repositionImage(); + } + + mViewer.injectMouseEvent((int)newX, (int)newY, BUTTON_UNDEFINED, false); + } + + /** + * Repositions the image by translating it (without affecting the zoom level) to place the + * cursor close to the center of the screen. + */ + private void repositionImage() { + synchronized (mRenderData) { + // Get the current cursor position in screen coordinates. + float[] cursorScreen = {mCursorPosition.x, mCursorPosition.y}; + mRenderData.transform.mapPoints(cursorScreen); + + // Translate so the cursor is displayed in the middle of the screen. + mRenderData.transform.postTranslate( + (float)mRenderData.screenWidth / 2 - cursorScreen[0], + (float)mRenderData.screenHeight / 2 - cursorScreen[1]); + } + mViewer.transformationChanged(); + } + + /** Injects a button event using the current cursor location. */ + private void injectButtonEvent(int button, boolean pressed) { + mViewer.injectMouseEvent((int)mCursorPosition.x, (int)mCursorPosition.y, button, pressed); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Avoid short-circuit logic evaluation - ensure both gesture detectors see all events so + // that they generate correct notifications. + boolean handled = mScroller.onTouchEvent(event); + handled = mZoomer.onTouchEvent(event) || handled; + + int pointerCount = event.getPointerCount(); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + switch (pointerCount) { + case 1: + mFingerMoved = false; + mMouseButton = BUTTON_LEFT; + break; + case 2: + mMouseButton = BUTTON_RIGHT; + break; + case 3: + mMouseButton = BUTTON_UNDEFINED; + // TODO(lambroslambrou): Add 3-finger-tap for middle-click, and use 3-finger + // swipe to show the action-bar or keyboard. + break; + default: + break; + } + break; + case MotionEvent.ACTION_UP: + if (mFingerMoved) { + // Don't inject anything. + mFingerMoved = false; + } else { + // The user pressed and released without moving. Inject a click event for a + // mouse button according to how many fingers were used. + injectButtonEvent(mMouseButton, true); + injectButtonEvent(mMouseButton, false); + handled = true; + } + break; + case MotionEvent.ACTION_POINTER_UP: + // |pointerCount| is the number of fingers that were on the screen prior to the UP + // event. + if (pointerCount == 3) { + mViewer.showActionBar(); + handled = true; + } + break; + default: + break; + } + return handled; + } + + @Override + public void onScreenConfigurationChanged() { + } + + @Override + public void onClientSizeChanged(int width, int height) { + repositionImage(); + } + + @Override + public void onHostSizeChanged(int width, int height) { + moveCursor((float)width / 2, (float)height / 2); + } + + /** Responds to touch events filtered by the gesture detectors. */ + private class GestureListener extends GestureDetector.SimpleOnGestureListener + implements ScaleGestureDetector.OnScaleGestureListener { + /** + * Called when the user drags one or more fingers across the touchscreen. + */ + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (e2.getPointerCount() != 1) { + return false; + } + mFingerMoved = true; + + float[] delta = {distanceX, distanceY}; + synchronized (mRenderData) { + Matrix canvasToImage = new Matrix(); + mRenderData.transform.invert(canvasToImage); + canvasToImage.mapVectors(delta); + } + + moveCursor(mCursorPosition.x - delta[0], mCursorPosition.y - delta[1]); + return true; + } + + /** Called when the user is in the process of pinch-zooming. */ + @Override + public boolean onScale(ScaleGestureDetector detector) { + if (Math.abs(detector.getScaleFactor() - 1) < MIN_ZOOM_DELTA) { + return false; + } + mFingerMoved = true; + + float scaleFactor = detector.getScaleFactor(); + synchronized (mRenderData) { + mRenderData.transform.postScale( + scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY()); + } + repositionImage(); + return true; + } + + /** Called whenever a gesture starts. Always accepts the gesture so it isn't ignored. */ + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + /** + * Called when the user starts to zoom. Always accepts the zoom so that + * onScale() can decide whether to respond to it. + */ + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return true; + } + + /** Called when the user is done zooming. Defers to onScale()'s judgement. */ + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + onScale(detector); + } + } +} diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index 0567506..aa20474 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -1894,10 +1894,6 @@ 'java_in_dir': 'android/java', 'additional_res_dirs': [ '<(SHARED_INTERMEDIATE_DIR)/remoting/android/res' ], 'additional_input_paths': [ - 'android/java/src/org/chromium/chromoting/Chromoting.java', - 'android/java/src/org/chromium/chromoting/Desktop.java', - 'android/java/src/org/chromium/chromoting/DesktopView.java', - 'android/java/src/org/chromium/chromoting/jni/JniInterface.java', '<(PRODUCT_DIR)/obj/remoting/remoting_android_resources.actions_rules_copies.stamp', ], }, |