Index: content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java |
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 0000000000000000000000000000000000000000..40272a2b8dfbb0252795d8607603e79513f7606b |
--- /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()); |
+ } |
+ |
+} |