diff options
author | yusufo@chromium.org <yusufo@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-07-28 01:20:29 +0000 |
---|---|---|
committer | yusufo@chromium.org <yusufo@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-07-28 01:20:29 +0000 |
commit | 5b2619528701d88948f6d141aeebd6bd5a7ff48f (patch) | |
tree | 0a65521e92eb7f4c9b5bb76c72c4b55eede96508 /content/public/android | |
parent | d1bbf2ea459715bb229268b4846a27ffa9773e70 (diff) | |
download | chromium_src-5b2619528701d88948f6d141aeebd6bd5a7ff48f.zip chromium_src-5b2619528701d88948f6d141aeebd6bd5a7ff48f.tar.gz chromium_src-5b2619528701d88948f6d141aeebd6bd5a7ff48f.tar.bz2 |
Enable gesture events handling on Android.
This add gesture related functionality to the java side to ContentViewCore and also
adds some missing calls on the routing path of the gesture events to webkit. The WebKit side
for related parts of the WebCompositorInputHandler has already been upstreamed.
BUG=136680
TEST=
Review URL: https://chromiumcodereview.appspot.com/10790066
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@148861 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content/public/android')
6 files changed, 1152 insertions, 55 deletions
diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentView.java b/content/public/android/java/src/org/chromium/content/browser/ContentView.java index babe4bc..d8501ec 100644 --- a/content/public/android/java/src/org/chromium/content/browser/ContentView.java +++ b/content/public/android/java/src/org/chromium/content/browser/ContentView.java @@ -289,18 +289,18 @@ public class ContentView extends FrameLayout implements ContentViewCore.Internal * Start pinch zoom. You must call {@link #pinchEnd} to stop. */ void pinchBegin(long timeMs, int x, int y) { - mContentViewCore.pinchBegin(timeMs, x, y); + mContentViewCore.getContentViewGestureHandler().pinchBegin(timeMs, x, y); } /** * Stop pinch zoom. */ void pinchEnd(long timeMs) { - mContentViewCore.pinchEnd(timeMs); + mContentViewCore.getContentViewGestureHandler().pinchEnd(timeMs); } void setIgnoreSingleTap(boolean value) { - mContentViewCore.setIgnoreSingleTap(value); + mContentViewCore.getContentViewGestureHandler().setIgnoreSingleTap(value); } /** @@ -316,7 +316,7 @@ public class ContentView extends FrameLayout implements ContentViewCore.Internal * coordinate. */ void pinchBy(long timeMs, int anchorX, int anchorY, float delta) { - mContentViewCore.pinchBy(timeMs, anchorX, anchorY, delta); + mContentViewCore.getContentViewGestureHandler().pinchBy(timeMs, anchorX, anchorY, delta); } /** @@ -347,6 +347,10 @@ public class ContentView extends FrameLayout implements ContentViewCore.Internal super.onScrollChanged(l, t, oldl, oldt); } + @Override + public boolean onTouchEvent(MotionEvent event) { + return mContentViewCore.onTouchEvent(event); + } // End FrameLayout overrides. @Override diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java index 5fcbf2c..ee08d3a 100644 --- a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java +++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java @@ -7,6 +7,7 @@ package org.chromium.content.browser; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; +import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -17,14 +18,19 @@ import android.webkit.DownloadListener; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.base.WeakContext; +import org.chromium.content.browser.ContentViewGestureHandler; +import org.chromium.content.browser.TouchPoint; +import org.chromium.content.browser.ZoomManager; import org.chromium.content.common.TraceEvent; +import org.chromium.content.browser.ContentViewGestureHandler.MotionEventDelegate; + /** * Contains all the major functionality necessary to manage the lifecycle of a ContentView without * being tied to the view system. */ @JNINamespace("content") -public class ContentViewCore { +public class ContentViewCore implements MotionEventDelegate { private static final String TAG = ContentViewCore.class.getName(); // The following constants match the ones in chrome/common/page_transition_types.h. @@ -52,6 +58,10 @@ public class ContentViewCore { // if resulting zooming will produce little visible difference. private static float WEBVIEW_ZOOM_CONTROLS_EPSILON = 0.007f; + // To avoid checkerboard, we clamp the fling velocity based on the maximum number of tiles + // should be allowed to upload per 100ms. + private static int MAX_NUM_UPLOAD_TILES = 12; + /** * Interface that consumers of {@link ContentViewCore} must implement to allow the proper * dispatching of view methods through the containing view. @@ -123,6 +133,7 @@ public class ContentViewCore { // Native pointer to C++ ContentView object which will be set by nativeInit() private int mNativeContentViewCore = 0; + private ContentViewGestureHandler mContentViewGestureHandler; private ZoomManager mZoomManager; // Cached page scale factor from native @@ -223,7 +234,6 @@ public class ContentViewCore { mPersonality = personality; mContentSettings = new ContentSettings(this, mNativeContentViewCore); - mContainerView.setWillNotDraw(false); mContainerView.setFocusable(true); mContainerView.setFocusableInTouchMode(true); if (mContainerView.getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) { @@ -231,7 +241,10 @@ public class ContentViewCore { mContainerView.setVerticalScrollBarEnabled(false); } mContainerView.setClickable(true); - initGestureDetectors(context); + + mZoomManager = new ZoomManager(context, this); + mZoomManager.updateMultiTouchSupport(); + mContentViewGestureHandler = new ContentViewGestureHandler(context, this, mZoomManager); Log.i(TAG, "mNativeContentView=0x"+ Integer.toHexString(mNativeContentViewCore)); } @@ -459,43 +472,93 @@ public class ContentViewCore { if (mNativeContentViewCore != 0) nativeClearHistory(mNativeContentViewCore); } + // End FrameLayout overrides. + + /** - * Start pinch zoom. You must call {@link #pinchEnd} to stop. + * @see View#onTouchEvent(MotionEvent) */ - void pinchBegin(long timeMs, int x, int y) { - if (mNativeContentViewCore != 0) { - // TODO(tedchoc): Pass pinch begin to native. - } + public boolean onTouchEvent(MotionEvent event) { + return mContentViewGestureHandler.onTouchEvent(event); } /** - * Stop pinch zoom. + * @return ContentViewGestureHandler for all MotionEvent and gesture related calls. */ - void pinchEnd(long timeMs) { + ContentViewGestureHandler getContentViewGestureHandler() { + return mContentViewGestureHandler; + } + + @Override + public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts) { if (mNativeContentViewCore != 0) { - // TODO(tedchoc): Pass pinch end to native. + return nativeTouchEvent(mNativeContentViewCore, timeMs, action, pts); } + return false; } - void setIgnoreSingleTap(boolean value) { - mIgnoreSingleTap = value; + @SuppressWarnings("unused") + @CalledByNative + private void didSetNeedTouchEvents(boolean needTouchEvents) { + mContentViewGestureHandler.didSetNeedTouchEvents(needTouchEvents); } - /** - * Modify the ContentView magnification level. The effect of calling this - * method is exactly as after "pinch zoom". - * - * @param timeMs The event time in milliseconds. - * @param delta The ratio of the new magnification level over the current - * magnification level. - * @param anchorX The magnification anchor (X) in the current view - * coordinate. - * @param anchorY The magnification anchor (Y) in the current view - * coordinate. - */ - void pinchBy(long timeMs, int anchorX, int anchorY, float delta) { - if (mNativeContentViewCore != 0) { - // TODO(tedchoc): Pass pinch by to native. + @SuppressWarnings("unused") + @CalledByNative + private void confirmTouchEvent(boolean handled) { + mContentViewGestureHandler.confirmTouchEvent(handled); + } + + @Override + public boolean sendGesture(int type, long timeMs, int x, int y, Bundle b) { + if (mNativeContentViewCore == 0) return false; + + switch (type) { + case ContentViewGestureHandler.GESTURE_SHOW_PRESSED_STATE: + nativeShowPressState(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_DOUBLE_TAP: + nativeDoubleTap(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SINGLE_TAP_UP: + nativeSingleTap(mNativeContentViewCore, timeMs, x, y, false); + return true; + case ContentViewGestureHandler.GESTURE_SINGLE_TAP_CONFIRMED: + handleTapOrPress(timeMs, x, y, false, + b.getBoolean(ContentViewGestureHandler.SHOW_PRESS, false)); + return true; + case ContentViewGestureHandler.GESTURE_LONG_PRESS: + handleTapOrPress(timeMs, x, y, true, false); + return true; + case ContentViewGestureHandler.GESTURE_SCROLL_START: + nativeScrollBegin(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SCROLL_BY: + nativeScrollBy(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SCROLL_END: + nativeScrollEnd(mNativeContentViewCore, timeMs); + return true; + case ContentViewGestureHandler.GESTURE_FLING_START: + nativeFlingStart(mNativeContentViewCore, timeMs, x, y, + clampFlingVelocityX(b.getInt(ContentViewGestureHandler.VELOCITY_X, 0)), + clampFlingVelocityY(b.getInt(ContentViewGestureHandler.VELOCITY_Y, 0))); + return true; + case ContentViewGestureHandler.GESTURE_FLING_CANCEL: + nativeFlingCancel(mNativeContentViewCore, timeMs); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: + nativePinchBegin(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_BY: + nativePinchBy(mNativeContentViewCore, timeMs, x, y, + b.getFloat(ContentViewGestureHandler.DELTA, 0)); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_END: + nativePinchEnd(mNativeContentViewCore, timeMs); + return true; + default: + return false; } } @@ -533,6 +596,13 @@ public class ContentViewCore { return mContentSettings; } + @Override + public boolean didUIStealScroll(float x, float y) { + // TODO(yusufo): Stubbed out for now. Upstream when computeHorizontalScrollOffset is + // available. + return false; + } + private void hidePopupDialog() { SelectPopupDialog.hide(this); } @@ -554,14 +624,22 @@ public class ContentViewCore { } } - private void initGestureDetectors(final Context context) { - try { - TraceEvent.begin(); - // TODO(tedchoc): Upstream the rest of the initialization. - mZoomManager = new ZoomManager(context, this); - mZoomManager.updateMultiTouchSupport(); - } finally { - TraceEvent.end(); + private void handleTapOrPress( + long timeMs, int x, int y, boolean isLongPress, boolean showPress) { + //TODO(yusufo):Upstream the rest of the bits about handlerControllers. + if (!mContainerView.isFocused()) mContainerView.requestFocus(); + + if (isLongPress) { + if (mNativeContentViewCore != 0) { + nativeLongPress(mNativeContentViewCore, timeMs, x, y, false); + } + } else { + if (!showPress && mNativeContentViewCore != 0) { + nativeShowPressState(mNativeContentViewCore, timeMs, x, y); + } + if (mNativeContentViewCore != 0) { + nativeSingleTap(mNativeContentViewCore, timeMs, x, y, false); + } } } @@ -579,6 +657,32 @@ public class ContentViewCore { } } + /* + * To avoid checkerboard, we clamp the fling velocity based on the maximum number of tiles + * allowed to be uploaded per 100ms. Calculation is limited to one direction. We assume the + * tile size is 256x256. The precise distance / velocity should be calculated based on the + * logic in Scroller.java. As it is almost linear for the first 100ms, we use a simple math. + */ + private int clampFlingVelocityX(int velocity) { + int cols = MAX_NUM_UPLOAD_TILES / (int) (Math.ceil((float) getHeight() / 256) + 1); + int maxVelocity = cols > 0 ? cols * 2560 : 1000; + if (Math.abs(velocity) > maxVelocity) { + return velocity > 0 ? maxVelocity : -maxVelocity; + } else { + return velocity; + } + } + + private int clampFlingVelocityY(int velocity) { + int rows = MAX_NUM_UPLOAD_TILES / (int) (Math.ceil((float) getWidth() / 256) + 1); + int maxVelocity = rows > 0 ? rows * 2560 : 1000; + if (Math.abs(velocity) > maxVelocity) { + return velocity > 0 ? maxVelocity : -maxVelocity; + } else { + return velocity; + } + } + /** * Register the listener to be used when content can not be handled by the * rendering engine, and should be downloaded instead. This will replace the @@ -698,9 +802,9 @@ public class ContentViewCore { int y = getHeight() / 2; float delta = 1.25f; - pinchBegin(timeMs, x, y); - pinchBy(timeMs, x, y, delta); - pinchEnd(timeMs); + getContentViewGestureHandler().pinchBegin(timeMs, x, y); + getContentViewGestureHandler().pinchBy(timeMs, x, y, delta); + getContentViewGestureHandler().pinchEnd(timeMs); return true; } @@ -727,14 +831,17 @@ public class ContentViewCore { int y = getHeight() / 2; float delta = 0.8f; - pinchBegin(timeMs, x, y); - pinchBy(timeMs, x, y, delta); - pinchEnd(timeMs); + getContentViewGestureHandler().pinchBegin(timeMs, x, y); + getContentViewGestureHandler().pinchBy(timeMs, x, y, delta); + getContentViewGestureHandler().pinchEnd(timeMs); return true; } - // Invokes the graphical zoom picker widget for this ContentView. + /** + * Invokes the graphical zoom picker widget for this ContentView. + */ + @Override public void invokeZoomPicker() { if (mContentSettings.supportZoom()) { mZoomManager.invokeZoomPicker(); @@ -784,6 +891,40 @@ public class ContentViewCore { // Returns true if the native side crashed so that java side can draw a sad tab. private native boolean nativeCrashed(int nativeContentViewCoreImpl); + private native boolean nativeTouchEvent(int nativeContentViewCoreImpl, + long timeMs, int action, + TouchPoint[] pts); + + private native void nativeScrollBegin(int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativeScrollEnd(int nativeContentViewCoreImpl, long timeMs); + + private native void nativeScrollBy( + int nativeContentViewCoreImpl, long timeMs, int deltaX, int deltaY); + + private native void nativeFlingStart( + int nativeContentViewCoreImpl, long timeMs, int x, int y, int vx, int vy); + + private native void nativeFlingCancel(int nativeContentViewCoreImpl, long timeMs); + + private native void nativeSingleTap( + int nativeContentViewCoreImpl, long timeMs, int x, int y, boolean linkPreviewTap); + + private native void nativeShowPressState( + int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativeDoubleTap(int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativeLongPress(int nativeContentViewCoreImpl, long timeMs, int x, int y, + boolean linkPreviewTap); + + private native void nativePinchBegin(int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativePinchEnd(int nativeContentViewCoreImpl, long timeMs); + + private native void nativePinchBy(int nativeContentViewCoreImpl, long timeMs, + int anchorX, int anchorY, float deltaScale); + private native boolean nativeCanGoBack(int nativeContentViewCoreImpl); private native boolean nativeCanGoForward(int nativeContentViewCoreImpl); diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java b/content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java new file mode 100644 index 0000000..40272a2 --- /dev/null +++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java @@ -0,0 +1,739 @@ +// Copyright (c) 2012 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.content.browser; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.util.Pair; +import android.view.GestureDetector; +import android.view.GestureDetector.OnGestureListener; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import org.chromium.content.browser.LongPressDetector.LongPressDelegate; +import org.chromium.content.common.TraceEvent; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * This class handles all MotionEvent handling done in ContentViewCore including the gesture + * recognition. It sends all related native calls through the interface MotionEventDelegate. + */ +class ContentViewGestureHandler implements LongPressDelegate { + + private static final String TAG = ContentViewGestureHandler.class.toString(); + /** + * Used for GESTURE_FLING_START x velocity + */ + static final String VELOCITY_X = "Velocity X"; + /** + * Used for GESTURE_FLING_START y velocity + */ + static final String VELOCITY_Y = "Velocity Y"; + /** + * Used in GESTURE_SINGLE_TAP_CONFIRMED to check whether ShowPress has been called before. + */ + static final String SHOW_PRESS = "ShowPress"; + /** + * Used for GESTURE_PINCH_BY delta + */ + static final String DELTA = "Delta"; + + private final Bundle mExtraParamBundle; + private GestureDetector mGestureDetector; + private final ZoomManager mZoomManager; + private LongPressDetector mLongPressDetector; + private OnGestureListener mListener; + private MotionEvent mCurrentDownEvent; + private final MotionEventDelegate mMotionEventDelegate; + + // Queue of motion events. If the boolean value is true, it means + // that the event has been offered to the native side but not yet acknowledged. If the + // value is false, it means the touch event has not been offered + // to the native side and can be immediately processed. + private final Deque<Pair<MotionEvent, Boolean>> mPendingMotionEvents = + new ArrayDeque<Pair<MotionEvent, Boolean>>(); + + // Has WebKit told us the current page requires touch events. + private boolean mNeedTouchEvents = false; + + // Remember whether onShowPress() is called. If it is not, in onSingleTapConfirmed() + // we will first show the press state, then trigger the click. + private boolean mShowPressIsCalled; + + // TODO(klobag): this is to avoid a bug in GestureDetector. With multi-touch, + // mAlwaysInTapRegion is not reset. So when the last finger is up, onSingleTapUp() + // will be mistakenly fired. + private boolean mIgnoreSingleTap; + + // Does native think we are scrolling? True from right before we + // send the first scroll event until the last finger is raised, or + // until after the follow-up fling has finished. Call + // nativeScrollBegin() when setting this to true, and use + // tellNativeScrollingHasEnded() to set it to false. + private boolean mNativeScrolling; + + private boolean mPinchInProgress = false; + + // Tracks whether a touch cancel event has been sent as a result of switching + // into scrolling or pinching mode. + private boolean mTouchCancelEventSent = false; + + private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); + + //On single tap this will store the x, y coordinates of the touch. + private int mSingleTapX; + private int mSingleTapY; + + // Used to track the last rawX/Y coordinates for moves. This gives absolute scroll distance. + // Useful for full screen tracking. + private float mLastRawX = 0; + private float mLastRawY = 0; + + // Cache of square of the scaled touch slop so we don't have to calculate it on every touch. + private int mScaledTouchSlopSquare; + + // Used to track the accumulated scroll error over time. This is used to remove the + // rounding error we introduced by passing integers to webkit. + private float mAccumulatedScrollErrorX = 0; + private float mAccumulatedScrollErrorY = 0; + + private static final int SNAP_NONE = 0; + private static final int SNAP_HORIZ = 1; + private static final int SNAP_VERT = 2; + private int mSnapScrollMode = SNAP_NONE; + private float mAverageAngle; + private boolean mSeenFirstScroll; + + /* + * Here is the snap align logic: + * 1. If it starts nearly horizontally or vertically, snap align; + * 2. If there is a dramatic direction change, let it go; + * + * Adjustable parameters. Angle is the radians on a unit circle, limited + * to quadrant 1. Values range from 0f (horizontal) to PI/2 (vertical) + */ + private static final float HSLOPE_TO_START_SNAP = .25f; + private static final float HSLOPE_TO_BREAK_SNAP = .6f; + private static final float VSLOPE_TO_START_SNAP = 1.25f; + private static final float VSLOPE_TO_BREAK_SNAP = .6f; + + /* + * These values are used to influence the average angle when entering + * snap mode. If it is the first movement entering snap, we set the average + * to the appropriate ideal. If the user is entering into snap after the + * first movement, then we average the average angle with these values. + */ + private static final float ANGLE_VERT = (float)(Math.PI / 2.0); + private static final float ANGLE_HORIZ = 0f; + + /* + * The modified moving average weight. + * Formula: MAV[t]=MAV[t-1] + (P[t]-MAV[t-1])/n + */ + private static final float MMA_WEIGHT_N = 5; + + static final int GESTURE_SHOW_PRESSED_STATE = 0; + static final int GESTURE_DOUBLE_TAP = 1; + static final int GESTURE_SINGLE_TAP_UP = 2; + static final int GESTURE_SINGLE_TAP_CONFIRMED = 3; + static final int GESTURE_LONG_PRESS = 4; + static final int GESTURE_SCROLL_START = 5; + static final int GESTURE_SCROLL_BY = 6; + static final int GESTURE_SCROLL_END = 7; + static final int GESTURE_FLING_START = 8; + static final int GESTURE_FLING_CANCEL = 9; + static final int GESTURE_PINCH_BEGIN = 10; + static final int GESTURE_PINCH_BY = 11; + static final int GESTURE_PINCH_END = 12; + + /** + * This is an interface to handle MotionEvent related communication with the native side also + * access some ContentView specific parameters. + */ + public interface MotionEventDelegate { + /** + * Send a raw {@link MotionEvent} to the native side + * @param timeMs Time of the event in ms. + * @param action The action type for the event. + * @param pts The TouchPoint array to be sent for the event. + * @return Whether the event was sent to the native side successfully or not. + */ + public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts); + + /** + * Send a gesture event to the native side. + * @param type The type of the gesture event. + * @param timeMs The time the gesture event occurred at. + * @param x The x location for the gesture event. + * @param y The y location for the gesture event. + * @param extraParams A bundle that holds specific extra parameters for certain gestures. + * Refer to gesture type definition for more information. + * @return Whether the gesture was sent successfully. + */ + boolean sendGesture( + int type, long timeMs, int x, int y, Bundle extraParams); + + /** + * Gives the UI the chance to override each scroll event. + * @param x The amount scrolled in the X direction. + * @param y The amount scrolled in the Y direction. + * @return Whether or not the UI consumed and handled this event. + */ + boolean didUIStealScroll(float x, float y); + + /** + * Show the zoom picker UI. + */ + public void invokeZoomPicker(); + } + + ContentViewGestureHandler( + Context context, MotionEventDelegate delegate, ZoomManager zoomManager) { + mExtraParamBundle = new Bundle(); + mLongPressDetector = new LongPressDetector(context, this); + mMotionEventDelegate = delegate; + mZoomManager = zoomManager; + initGestureDetectors(context); + } + + /** + * Used to override the default long press detector, gesture detector and listener. + * This is used for testing only. + * @param longPressDetector The new LongPressDetector to be assigned. + * @param gestureDetector The new GestureDetector to be assigned. + * @param listener The new onGestureListener to be assigned. + */ + void setTestDependencies( + LongPressDetector longPressDetector, GestureDetector gestureDetector, + OnGestureListener listener) { + mLongPressDetector = longPressDetector; + mGestureDetector = gestureDetector; + mListener = listener; + } + + private void initGestureDetectors(final Context context) { + int scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mScaledTouchSlopSquare = scaledTouchSlop * scaledTouchSlop; + try { + TraceEvent.begin(); + GestureDetector.SimpleOnGestureListener listener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDown(MotionEvent e) { + mShowPressIsCalled = false; + mIgnoreSingleTap = false; + mSeenFirstScroll = false; + mNativeScrolling = false; + mSnapScrollMode = SNAP_NONE; + mLastRawX = e.getRawX(); + mLastRawY = e.getRawY(); + mAccumulatedScrollErrorX = 0; + mAccumulatedScrollErrorY = 0; + // Return true to indicate that we want to handle touch + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + // Scroll snapping + if (!mSeenFirstScroll) { + mAverageAngle = calculateDragAngle(distanceX, distanceY); + // Initial scroll event + if (!mZoomManager.isScaleGestureDetectionInProgress()) { + // if it starts nearly horizontal or vertical, enforce it + if (mAverageAngle < HSLOPE_TO_START_SNAP) { + mSnapScrollMode = SNAP_HORIZ; + mAverageAngle = ANGLE_HORIZ; + } else if (mAverageAngle > VSLOPE_TO_START_SNAP) { + mSnapScrollMode = SNAP_VERT; + mAverageAngle = ANGLE_VERT; + } + } + mSeenFirstScroll = true; + // Ignore the first scroll delta to avoid a visible jump. + return true; + } else { + mAverageAngle += + (calculateDragAngle(distanceX, distanceY) - mAverageAngle) + / MMA_WEIGHT_N; + if (mSnapScrollMode != SNAP_NONE) { + if ((mSnapScrollMode == SNAP_VERT + && mAverageAngle < VSLOPE_TO_BREAK_SNAP) + || (mSnapScrollMode == SNAP_HORIZ + && mAverageAngle > HSLOPE_TO_BREAK_SNAP)) { + // radical change means getting out of snap mode + mSnapScrollMode = SNAP_NONE; + } + } else { + if (!mZoomManager.isScaleGestureDetectionInProgress()) { + if (mAverageAngle < HSLOPE_TO_START_SNAP) { + mSnapScrollMode = SNAP_HORIZ; + mAverageAngle = (mAverageAngle + ANGLE_HORIZ) / 2; + } else if (mAverageAngle > VSLOPE_TO_START_SNAP) { + mSnapScrollMode = SNAP_VERT; + mAverageAngle = (mAverageAngle + ANGLE_VERT) / 2; + } + } + } + } + + if (mSnapScrollMode != SNAP_NONE) { + if (mSnapScrollMode == SNAP_HORIZ) { + distanceY = 0; + } else { + distanceX = 0; + } + } + + boolean didUIStealScroll = mMotionEventDelegate.didUIStealScroll( + e2.getRawX() - mLastRawX, e2.getRawY() - mLastRawY); + + mLastRawX = e2.getRawX(); + mLastRawY = e2.getRawY(); + if (didUIStealScroll) return true; + if (!mNativeScrolling && mMotionEventDelegate.sendGesture( + GESTURE_SCROLL_START, e1.getEventTime(), + (int) e1.getX(), (int) e1.getY(), null)) { + mNativeScrolling = true; + + } + // distanceX and distanceY is the scrolling offset since last onScroll. + // Because we are passing integers to webkit, this could introduce + // rounding errors. The rounding errors will accumulate overtime. + // To solve this, we should adding back the rounding errors each time + // when we calculate the new offset. + int dx = (int) (distanceX + mAccumulatedScrollErrorX); + int dy = (int) (distanceY + mAccumulatedScrollErrorY); + mAccumulatedScrollErrorX = distanceX + mAccumulatedScrollErrorX - dx; + mAccumulatedScrollErrorY = distanceY + mAccumulatedScrollErrorY - dy; + if ((dx | dy) != 0) { + mMotionEventDelegate.sendGesture(GESTURE_SCROLL_BY, + e2.getEventTime(), dx, dy, null); + } + + mMotionEventDelegate.invokeZoomPicker(); + + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, + float velocityX, float velocityY) { + if (mSnapScrollMode == SNAP_NONE) { + float flingAngle = calculateDragAngle(velocityX, velocityY); + if (flingAngle < HSLOPE_TO_START_SNAP) { + mSnapScrollMode = SNAP_HORIZ; + mAverageAngle = ANGLE_HORIZ; + } else if (flingAngle > VSLOPE_TO_START_SNAP) { + mSnapScrollMode = SNAP_VERT; + mAverageAngle = ANGLE_VERT; + } + } + + if (mSnapScrollMode != SNAP_NONE) { + if (mSnapScrollMode == SNAP_HORIZ) { + velocityY = 0; + } else { + velocityX = 0; + } + } + + fling(e1.getEventTime(),(int) e1.getX(0), (int) e1.getY(0), + (int) velocityX, (int) velocityY); + return true; + } + + @Override + public void onShowPress(MotionEvent e) { + mShowPressIsCalled = true; + mMotionEventDelegate.sendGesture(GESTURE_SHOW_PRESSED_STATE, + e.getEventTime(), (int) e.getX(), (int) e.getY(), null); + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (isDistanceBetweenDownAndUpTooLong(e.getRawX(), e.getRawY())) { + mIgnoreSingleTap = true; + return true; + } + // This is a hack to address the issue where user hovers + // over a link for longer than DOUBLE_TAP_TIMEOUT, then + // onSingleTapConfirmed() is not triggered. But we still + // want to trigger the tap event at UP. So we override + // onSingleTapUp() in this case. This assumes singleTapUp + // gets always called before singleTapConfirmed. + if (!mIgnoreSingleTap && !mLongPressDetector.isInLongPress() && + (e.getEventTime() - e.getDownTime() > DOUBLE_TAP_TIMEOUT)) { + float x = e.getX(); + float y = e.getY(); + if (mMotionEventDelegate.sendGesture(GESTURE_SINGLE_TAP_UP, + e.getEventTime(), (int) x, (int) y, null)) { + mIgnoreSingleTap = true; + } + setClickXAndY((int) x, (int) y); + return true; + } + return false; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + // Long taps in the edges of the screen have their events delayed by + // ChromeViewHolder for tab swipe operations. As a consequence of the delay + // this method might be called after receiving the up event. + // These corner cases should be ignored. + if (mLongPressDetector.isInLongPress() || mIgnoreSingleTap) return true; + + int x = (int) e.getX(); + int y = (int) e.getY(); + mExtraParamBundle.clear(); + mExtraParamBundle.putBoolean(SHOW_PRESS, mShowPressIsCalled); + mMotionEventDelegate.sendGesture(GESTURE_SINGLE_TAP_CONFIRMED, + e.getEventTime(), x, y, mExtraParamBundle); + setClickXAndY(x, y); + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + mMotionEventDelegate.sendGesture(GESTURE_DOUBLE_TAP, + e.getEventTime(), (int) e.getX(), (int) e.getY(), null); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + if (!mZoomManager.isScaleGestureDetectionInProgress()) { + mMotionEventDelegate.sendGesture(GESTURE_LONG_PRESS, + e.getEventTime(), (int) e.getX(), (int) e.getY(), null); + } + } + + /** + * This method inspects the distance between where the user started touching + * the surface, and where she released. If the points are too far apart, we + * should assume that the web page has consumed the scroll-events in-between, + * and as such, this should not be considered a single-tap. + * + * We use the Android frameworks notion of how far a touch can wander before + * we think the user is scrolling. + * + * @param x the new x coordinate + * @param y the new y coordinate + * @return true if the distance is too long to be considered a single tap + */ + private boolean isDistanceBetweenDownAndUpTooLong(float x, float y) { + double deltaX = mLastRawX - x; + double deltaY = mLastRawY - y; + return deltaX * deltaX + deltaY * deltaY > mScaledTouchSlopSquare; + } + }; + mListener = listener; + mGestureDetector = new GestureDetector(context, listener); + mGestureDetector.setIsLongpressEnabled(false); + } finally { + TraceEvent.end(); + } + } + + /** + * @return LongPressDetector handling setting up timers for and canceling LongPress gestures. + */ + LongPressDetector getLongPressDetector() { + return mLongPressDetector; + } + + /** + * @param event Start a LongPress gesture event from the listener. + */ + @Override + public void onLongPress(MotionEvent event) { + mListener.onLongPress(event); + } + + /** + * Cancels any ongoing LongPress timers. + */ + void cancelLongPress() { + mLongPressDetector.cancelLongPress(); + } + + /** + * Fling the ContentView from the current position. + * @param x Fling touch starting position + * @param y Fling touch starting position + * @param velocityX Initial velocity of the fling (X) measured in pixels per second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per second. + */ + void fling(long timeMs, int x, int y, int velocityX, int velocityY) { + endFling(timeMs); + mExtraParamBundle.clear(); + mExtraParamBundle.putInt(VELOCITY_X, velocityX); + mExtraParamBundle.putInt(VELOCITY_Y, velocityY); + mMotionEventDelegate.sendGesture(GESTURE_FLING_START, + timeMs, x, y, mExtraParamBundle); + } + + /** + * Send a FlingCancel gesture event and also cancel scrolling if it is active. + * @param timeMs The time in ms for the event initiating this gesture. + */ + void endFling(long timeMs) { + mMotionEventDelegate.sendGesture(GESTURE_FLING_CANCEL, timeMs, 0, 0, null); + tellNativeScrollingHasEnded(timeMs); + } + + // If native thinks scrolling (or fling-scrolling) is going on, tell native + // it has ended. + private void tellNativeScrollingHasEnded(long timeMs) { + if (mNativeScrolling) { + mNativeScrolling = false; + mMotionEventDelegate.sendGesture(GESTURE_SCROLL_END, timeMs, 0, 0, null); + } + } + + /** + * Starts a pinch gesture. + * @param timeMs The time in ms for the event initiating this gesture. + * @param x The x coordinate for the event initiating this gesture. + * @param y The x coordinate for the event initiating this gesture. + */ + void pinchBegin(long timeMs, int x, int y) { + mMotionEventDelegate.sendGesture(GESTURE_PINCH_BEGIN, timeMs, x, y, null); + } + + /** + * Pinch by a given percentage. + * @param timeMs The time in ms for the event initiating this gesture. + * @param anchorX The x coordinate for the anchor point to be used in pinch. + * @param anchorY The y coordinate for the anchor point to be used in pinch. + * @param delta The percentage to pinch by. + */ + void pinchBy(long timeMs, int anchorX, int anchorY, float delta) { + mExtraParamBundle.clear(); + mExtraParamBundle.putFloat(DELTA, delta); + mMotionEventDelegate.sendGesture(GESTURE_PINCH_BY, + timeMs, anchorX, anchorY, mExtraParamBundle); + mPinchInProgress = true; + } + + /** + * End a pinch gesture. + * @param timeMs The time in ms for the event initiating this gesture. + */ + void pinchEnd(long timeMs) { + mMotionEventDelegate.sendGesture(GESTURE_PINCH_END, timeMs, 0, 0, null); + mPinchInProgress = false; + } + + /** + * Ignore singleTap gestures. + */ + void setIgnoreSingleTap(boolean value) { + mIgnoreSingleTap = value; + } + + private float calculateDragAngle(float dx, float dy) { + dx = Math.abs(dx); + dy = Math.abs(dy); + return (float) Math.atan2(dy, dx); + } + + private void setClickXAndY(int x, int y) { + mSingleTapX = x; + mSingleTapY = y; + } + + /** + * @return The x coordinate for the last point that a singleTap gesture was initiated from. + */ + public int getSingleTapX() { + return mSingleTapX; + } + + /** + * @return The y coordinate for the last point that a singleTap gesture was initiated from. + */ + public int getSingleTapY() { + return mSingleTapY; + } + + /** + * Handle the incoming MotionEvent. + * @return Whether the event was handled. + */ + boolean onTouchEvent(MotionEvent event) { + TraceEvent.begin("onTouchEvent"); + mLongPressDetector.cancelLongPressIfNeeded(event); + // Notify native that scrolling has stopped whenever a down action is processed prior to + // passing the event to native as it will drop them as an optimization if scrolling is + // enabled. Ending the fling ensures scrolling has stopped as well as terminating the + // current fling if applicable. + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + endFling(event.getEventTime()); + } + + if (offerTouchEventToJavaScript(event)) { + // offerTouchEventToJavaScript returns true to indicate the event was sent + // to the render process. If it is not subsequently handled, it will + // be returned via confirmTouchEvent(false) and eventually passed to + // processTouchEvent asynchronously. + TraceEvent.end("onTouchEvent"); + return true; + } + return processTouchEvent(event); + } + + /** + * Sets the flag indicating that the content has registered listeners for touch events. + */ + void didSetNeedTouchEvents(boolean needTouchEvents) { + mNeedTouchEvents = needTouchEvents; + // When mainframe is loading, FrameLoader::transitionToCommitted will + // call this method to set mNeedTouchEvents to false. We use this as + // an indicator to clear the pending motion events so that events from + // the previous page will not be carried over to the new page. + if (!mNeedTouchEvents) mPendingMotionEvents.clear(); + } + + private boolean offerTouchEventToJavaScript(MotionEvent event) { + mLongPressDetector.onOfferTouchEventToJavaScript(event); + + if (!mNeedTouchEvents) return false; + + if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { + // Only send move events if the move has exceeded the slop threshold. + if (!mLongPressDetector.confirmOfferMoveEventToJavaScript(event)) { + return true; + } + // Avoid flooding the renderer process with move events: if the previous pending + // command is also a move (common case), skip sending this event to the webkit + // side and collapse it into the pending event. + Pair<MotionEvent, Boolean> previousEvent = mPendingMotionEvents.peekLast(); + if (previousEvent != null && previousEvent.second == true + && previousEvent.first.getActionMasked() == MotionEvent.ACTION_MOVE + && previousEvent.first.getPointerCount() == event.getPointerCount()) { + MotionEvent.PointerCoords[] coords = + new MotionEvent.PointerCoords[event.getPointerCount()]; + for (int i = 0; i < coords.length; ++i) { + coords[i] = new MotionEvent.PointerCoords(); + event.getPointerCoords(i, coords[i]); + } + previousEvent.first.addBatch(event.getEventTime(), coords, event.getMetaState()); + return true; + } + } + + TouchPoint[] pts = new TouchPoint[event.getPointerCount()]; + int type = TouchPoint.createTouchPoints(event, pts); + + boolean forwarded = false; + if (type != TouchPoint.CONVERSION_ERROR && !mNativeScrolling && !mPinchInProgress) { + mTouchCancelEventSent = false; + forwarded = mMotionEventDelegate.sendTouchEvent(event.getEventTime(), type, pts); + } else if ((mNativeScrolling || mPinchInProgress) && !mTouchCancelEventSent) { + forwarded = mMotionEventDelegate.sendTouchEvent(event.getEventTime(), + TouchPoint.TOUCH_EVENT_TYPE_CANCEL, pts); + mTouchCancelEventSent = true; + } + if (forwarded || !mPendingMotionEvents.isEmpty()) { + // Copy the event, as the original may get mutated after this method returns. + mPendingMotionEvents.add(Pair.create(MotionEvent.obtain(event), forwarded)); + // TODO(joth): If needed, start a watchdog timer to pump mPendingMotionEvents + // in the case of the WebKit renderer / JS being unresponsive. + return true; + } + return false; + } + + private boolean processTouchEvent(MotionEvent event) { + boolean handled = false; + // The last "finger up" is an end to scrolling but may not be + // an end to movement (e.g. fling scroll). We do not tell + // native code to end scrolling until we are sure we did not + // fling. + boolean possiblyEndMovement = false; + + // "Last finger raised" could be an end to movement. However, + // give the mSimpleTouchDetector a chance to continue + // scrolling with a fling. + if ((event.getAction() == MotionEvent.ACTION_UP) && + (event.getPointerCount() == 1)) { + if (mNativeScrolling) { + possiblyEndMovement = true; + } + } + + mLongPressDetector.startLongPressTimerIfNeeded(event); + + // Use the framework's GestureDetector to detect pans and zooms not already + // handled by the WebKit touch events gesture manager. + if (canHandle(event)) { + handled |= mGestureDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_DOWN) mCurrentDownEvent = event; + } + + handled |= mZoomManager.processTouchEvent(event); + + if (possiblyEndMovement && !handled) { + tellNativeScrollingHasEnded(event.getEventTime()); + } + + return handled; + } + + /** + * Respond to a MotionEvent being returned from the native side. + * @param handled Whether the MotionEvent was handled on the native side. + */ + void confirmTouchEvent(boolean handled) { + MotionEvent eventToPassThrough = null; + if (mPendingMotionEvents.isEmpty()) { + Log.w(TAG, "confirmTouchEvent with Empty pending list!"); + return; + } + TraceEvent.begin(); + Pair<MotionEvent, Boolean> event = mPendingMotionEvents.removeFirst(); + if (!handled) { + if (!processTouchEvent(event.first)) { + // TODO(joth): If the Java side gesture handler also fails to consume + // this deferred event, should it be bubbled up to the parent view? + Log.w(TAG, "Unhandled deferred touch event"); + } + } else { + mZoomManager.passTouchEventThrough(event.first); + } + + // Now process all events that are in the queue but not sent to the native. + Pair<MotionEvent, Boolean> nextEvent = mPendingMotionEvents.peekFirst(); + while (nextEvent != null && nextEvent.second == false) { + processTouchEvent(nextEvent.first); + mPendingMotionEvents.removeFirst(); + nextEvent.first.recycle(); + nextEvent = mPendingMotionEvents.peekFirst(); + } + + // We may have pending events that could cancel the timers: + // For instance, if we received an UP before the DOWN completed + // its roundtrip (so it didn't cancel the timer during onTouchEvent()). + mLongPressDetector.cancelLongPressIfNeeded(mPendingMotionEvents.iterator()); + event.first.recycle(); + TraceEvent.end(); + } + + /** + * @return Whether the ContentViewGestureHandler can handle a MotionEvent right now. True only + * if it's the start of a new stream (ACTION_DOWN), or a continuation of the current stream. + */ + boolean canHandle(MotionEvent ev) { + return ev.getAction() == MotionEvent.ACTION_DOWN || + (mCurrentDownEvent != null && mCurrentDownEvent.getDownTime() == ev.getDownTime()); + } + +} diff --git a/content/public/android/java/src/org/chromium/content/browser/TouchPoint.java b/content/public/android/java/src/org/chromium/content/browser/TouchPoint.java index 66e35b1..2e44d9e 100644 --- a/content/public/android/java/src/org/chromium/content/browser/TouchPoint.java +++ b/content/public/android/java/src/org/chromium/content/browser/TouchPoint.java @@ -17,10 +17,10 @@ class TouchPoint { // Type of motion event to send to the native side. The values originate from their // webkit WebInputEvent counterparts, and are set via initializeConstants(). - private static int TOUCH_EVENT_TYPE_START; - private static int TOUCH_EVENT_TYPE_MOVE; - private static int TOUCH_EVENT_TYPE_END; - private static int TOUCH_EVENT_TYPE_CANCEL; + static int TOUCH_EVENT_TYPE_START; + static int TOUCH_EVENT_TYPE_MOVE; + static int TOUCH_EVENT_TYPE_END; + static int TOUCH_EVENT_TYPE_CANCEL; // Type of motion event to send to the native side. The values originate from their // webkit WebTouchPoint counterparts, and are set via initializeConstants(). diff --git a/content/public/android/java/src/org/chromium/content/browser/ZoomManager.java b/content/public/android/java/src/org/chromium/content/browser/ZoomManager.java index adb5cbb..e632f2c 100644 --- a/content/public/android/java/src/org/chromium/content/browser/ZoomManager.java +++ b/content/public/android/java/src/org/chromium/content/browser/ZoomManager.java @@ -55,14 +55,14 @@ class ZoomManager { public boolean onScaleBegin(ScaleGestureDetector detector) { if (ignoreDetectorEvents()) return false; mPinchEventSent = false; - mContentViewCore.setIgnoreSingleTap(true); + mContentViewCore.getContentViewGestureHandler().setIgnoreSingleTap(true); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { if (!mPinchEventSent || !mContentViewCore.isAlive()) return; - mContentViewCore.pinchEnd(detector.getEventTime()); + mContentViewCore.getContentViewGestureHandler().pinchEnd(detector.getEventTime()); mPinchEventSent = false; } @@ -76,11 +76,11 @@ class ZoomManager { // that pinchBy() is called without any pinchBegin(). // To solve this problem, we call pinchBegin() here if it is never called. if (!mPinchEventSent) { - mContentViewCore.pinchBegin(detector.getEventTime(), + mContentViewCore.getContentViewGestureHandler().pinchBegin(detector.getEventTime(), (int) detector.getFocusX(), (int) detector.getFocusY()); mPinchEventSent = true; } - mContentViewCore.pinchBy( + mContentViewCore.getContentViewGestureHandler().pinchBy( detector.getEventTime(), (int) detector.getFocusX(), (int) detector.getFocusY(), detector.getScaleFactor()); return true; diff --git a/content/public/android/javatests/src/org/chromium/content/browser/ContentViewGestureHandlerTest.java b/content/public/android/javatests/src/org/chromium/content/browser/ContentViewGestureHandlerTest.java new file mode 100644 index 0000000..29dc6ba --- /dev/null +++ b/content/public/android/javatests/src/org/chromium/content/browser/ContentViewGestureHandlerTest.java @@ -0,0 +1,213 @@ +// Copyright (c) 2012 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.content.browser; + +import android.content.Context; +import android.os.Bundle; +import android.os.SystemClock; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.GestureDetector.OnGestureListener; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.chromium.base.test.Feature; +import org.chromium.base.test.ScalableTimeout; +import org.chromium.content.browser.ContentViewGestureHandler.MotionEventDelegate; + +/** + * Test suite for ContentViewGestureHandler. + */ +public class ContentViewGestureHandlerTest extends InstrumentationTestCase { + private static final int FAKE_COORD_X = 42; + private static final int FAKE_COORD_Y = 24; + + private static final String TAG = ContentViewGestureHandler.class.toString(); + private MockListener mMockListener; + private MockGestureDetector mMockGestureDetector; + private ContentViewGestureHandler mGestureHandler; + private LongPressDetector mLongPressDetector; + + static class MockListener extends GestureDetector.SimpleOnGestureListener { + MotionEvent mLastLongPress; + MotionEvent mLastSingleTap; + MotionEvent mLastFling1; + MotionEvent mLastFling2; + CountDownLatch mLongPressCalled; + MotionEvent mLastScroll1; + MotionEvent mLastScroll2; + float mLastScrollDistanceX; + float mLastScrollDistanceY; + + public MockListener() { + mLongPressCalled = new CountDownLatch(1); + } + + @Override + public void onLongPress(MotionEvent e) { + mLastLongPress = MotionEvent.obtain(e); + mLongPressCalled.countDown(); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + mLastSingleTap = e; + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + mLastSingleTap = e; + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + mLastScroll1 = e1; + mLastScroll2 = e2; + mLastScrollDistanceX = distanceX; + mLastScrollDistanceY = distanceY; + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + mLastFling1 = e1; + mLastFling2 = e2; + return true; + } + } + + static class MockGestureDetector extends GestureDetector { + MotionEvent mLastEvent; + public MockGestureDetector(Context context, OnGestureListener listener) { + super(context, listener); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mLastEvent = MotionEvent.obtain(ev); + return super.onTouchEvent(ev); + } + } + + static class MockMotionEventDelegate implements MotionEventDelegate { + @Override + public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts) { + // Not implemented. + return false; + } + + @Override + public boolean sendGesture(int type, long timeMs, int x, int y, Bundle extraParams) { + Log.i(TAG,"Gesture event received with type id " + type); + return false; + } + + @Override + public boolean didUIStealScroll(float x, float y) { + // Not implemented. + return false; + } + + @Override + public void invokeZoomPicker() { + // Not implemented. + } + } + + static class MockZoomManager extends ZoomManager { + MockZoomManager(Context context, ContentViewCore contentViewCore) { + super(context, contentViewCore); + } + + @Override + public boolean processTouchEvent(MotionEvent event) { + return false; + } + } + + private MotionEvent motionEvent(int action, long downTime, long eventTime) { + return MotionEvent.obtain(downTime, eventTime, action, FAKE_COORD_X, FAKE_COORD_Y, 0); + } + + @Override + public void setUp() { + mMockListener = new MockListener(); + mMockGestureDetector = new MockGestureDetector( + getInstrumentation().getTargetContext(), mMockListener); + mGestureHandler = new ContentViewGestureHandler( + getInstrumentation().getTargetContext(), new MockMotionEventDelegate(), + new MockZoomManager(getInstrumentation().getTargetContext(), null)); + mLongPressDetector = new LongPressDetector( + getInstrumentation().getTargetContext(), mGestureHandler); + mGestureHandler.setTestDependencies( + mLongPressDetector, mMockGestureDetector, mMockListener); + } + + /** + * Verify that a DOWN followed shortly by an UP will trigger a single tap. + * + * @throws Exception + */ + @SmallTest + @Feature({"Android-WebView"}) + public void testGestureSingleClick() throws Exception { + final long downTime = SystemClock.uptimeMillis(); + final long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = motionEvent(MotionEvent.ACTION_DOWN, downTime, downTime); + + assertFalse(mGestureHandler.onTouchEvent(event)); + assertTrue("Should have a pending gesture", mMockGestureDetector.mLastEvent != null); + assertTrue("Should have a pending LONG_PRESS", mLongPressDetector.hasPendingMessage()); + + event = motionEvent(MotionEvent.ACTION_UP, downTime, eventTime + 10); + mLongPressDetector.cancelLongPressIfNeeded(event); + assertTrue("Should not have a pending LONG_PRESS", !mLongPressDetector.hasPendingMessage()); + assertTrue(mGestureHandler.onTouchEvent(event)); + // Synchronous, no need to wait. + assertTrue("Should have a single tap", mMockListener.mLastSingleTap != null); + } + + /** + * Verify that a DOWN followed by a MOVE will trigger fling (but not LONG). + * @throws Exception + */ + @SmallTest + @Feature({"Android-WebView"}) + public void testGestureFlingAndCancelLongClick() throws Exception { + final long downTime = SystemClock.uptimeMillis(); + final long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = motionEvent(MotionEvent.ACTION_DOWN, downTime, downTime); + + assertFalse(mGestureHandler.onTouchEvent(event)); + assertTrue("Should have a pending gesture", mMockGestureDetector.mLastEvent != null); + assertTrue("Should have a pending LONG_PRESS", mLongPressDetector.hasPendingMessage()); + + event = MotionEvent.obtain( + downTime, eventTime + 5, MotionEvent.ACTION_MOVE, + FAKE_COORD_X * 10, FAKE_COORD_Y * 10, 0); + mLongPressDetector.cancelLongPressIfNeeded(event); + assertTrue("Should not have a pending LONG_PRESS", !mLongPressDetector.hasPendingMessage()); + assertTrue(mGestureHandler.onTouchEvent(event)); + + event = MotionEvent.obtain( + downTime, eventTime + 10, MotionEvent.ACTION_UP, + FAKE_COORD_X * 10, FAKE_COORD_Y * 10, 0); + assertTrue(mGestureHandler.onTouchEvent(event)); + + // Synchronous, no need to wait. + assertTrue("Should have a fling", mMockListener.mLastFling1 != null); + assertTrue("Should not have a long press", mMockListener.mLastLongPress == null); + } +} |