1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.content.browser; 6 7import android.content.Context; 8import android.os.Bundle; 9import android.os.Handler; 10import android.os.SystemClock; 11import android.util.Log; 12import android.view.InputDevice; 13import android.view.MotionEvent; 14import android.view.ViewConfiguration; 15 16import org.chromium.content.browser.third_party.GestureDetector; 17import org.chromium.content.browser.third_party.GestureDetector.OnGestureListener; 18import org.chromium.content.browser.LongPressDetector.LongPressDelegate; 19import org.chromium.content.browser.SnapScrollController; 20import org.chromium.content.common.TraceEvent; 21 22import java.util.ArrayDeque; 23import java.util.Deque; 24 25/** 26 * This class handles all MotionEvent handling done in ContentViewCore including the gesture 27 * recognition. It sends all related native calls through the interface MotionEventDelegate. 28 */ 29class ContentViewGestureHandler implements LongPressDelegate { 30 31 private static final String TAG = "ContentViewGestureHandler"; 32 /** 33 * Used for GESTURE_FLING_START x velocity 34 */ 35 static final String VELOCITY_X = "Velocity X"; 36 /** 37 * Used for GESTURE_FLING_START y velocity 38 */ 39 static final String VELOCITY_Y = "Velocity Y"; 40 /** 41 * Used for GESTURE_SCROLL_BY x distance 42 */ 43 static final String DISTANCE_X = "Distance X"; 44 /** 45 * Used for GESTURE_SCROLL_BY y distance 46 */ 47 static final String DISTANCE_Y = "Distance Y"; 48 /** 49 * Used in GESTURE_SINGLE_TAP_CONFIRMED to check whether ShowPress has been called before. 50 */ 51 static final String SHOW_PRESS = "ShowPress"; 52 /** 53 * Used for GESTURE_PINCH_BY delta 54 */ 55 static final String DELTA = "Delta"; 56 57 private final Bundle mExtraParamBundle; 58 private GestureDetector mGestureDetector; 59 private final ZoomManager mZoomManager; 60 private LongPressDetector mLongPressDetector; 61 private OnGestureListener mListener; 62 private MotionEvent mCurrentDownEvent; 63 private final MotionEventDelegate mMotionEventDelegate; 64 65 // Queue of motion events. 66 private final Deque<MotionEvent> mPendingMotionEvents = new ArrayDeque<MotionEvent>(); 67 68 // Has WebKit told us the current page requires touch events. 69 private boolean mHasTouchHandlers = false; 70 71 // True if the down event for the current gesture was returned back to the browser with 72 // INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS 73 private boolean mNoTouchHandlerForGesture = false; 74 75 // True if JavaScript touch event handlers returned an ACK with 76 // INPUT_EVENT_ACK_STATE_CONSUMED. In this case we should avoid, sending events from 77 // this gesture to the Gesture Detector since it will have already missed at least 78 // one event. 79 private boolean mJavaScriptIsConsumingGesture = false; 80 81 // Remember whether onShowPress() is called. If it is not, in onSingleTapConfirmed() 82 // we will first show the press state, then trigger the click. 83 private boolean mShowPressIsCalled; 84 85 // TODO(klobag): this is to avoid a bug in GestureDetector. With multi-touch, 86 // mAlwaysInTapRegion is not reset. So when the last finger is up, onSingleTapUp() 87 // will be mistakenly fired. 88 private boolean mIgnoreSingleTap; 89 90 // True from right before we send the first scroll event until the last finger is raised. 91 private boolean mTouchScrolling; 92 93 // TODO(wangxianzhu): For now it is true after a fling is started until the next 94 // touch. Should reset it to false on end of fling if the UI is able to know when the 95 // fling ends. 96 private boolean mFlingMayBeActive; 97 98 private boolean mSeenFirstScrollEvent; 99 100 private boolean mPinchInProgress = false; 101 102 // Tracks whether a touch cancel event has been sent as a result of switching 103 // into scrolling or pinching mode. 104 private boolean mTouchCancelEventSent = false; 105 106 // Last cancelled touch event as a result of scrolling or pinching. 107 private MotionEvent mLastCancelledEvent = null; 108 109 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); 110 111 //On single tap this will store the x, y coordinates of the touch. 112 private int mSingleTapX; 113 private int mSingleTapY; 114 115 // Indicate current double tap drag mode state. 116 private int mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_NONE; 117 118 // x, y coordinates for an Anchor on double tap drag zoom. 119 private float mDoubleTapDragZoomAnchorX; 120 private float mDoubleTapDragZoomAnchorY; 121 122 // On double tap this will store the y coordinates of the touch. 123 private float mDoubleTapY; 124 125 // Double tap drag zoom sensitive (speed). 126 private static final float DOUBLE_TAP_DRAG_ZOOM_SPEED = 0.005f; 127 128 // Used to track the last rawX/Y coordinates for moves. This gives absolute scroll distance. 129 // Useful for full screen tracking. 130 private float mLastRawX = 0; 131 private float mLastRawY = 0; 132 133 // Cache of square of the scaled touch slop so we don't have to calculate it on every touch. 134 private int mScaledTouchSlopSquare; 135 136 // Object that keeps track of and updates scroll snapping behavior. 137 private SnapScrollController mSnapScrollController; 138 139 // Used to track the accumulated scroll error over time. This is used to remove the 140 // rounding error we introduced by passing integers to webkit. 141 private float mAccumulatedScrollErrorX = 0; 142 private float mAccumulatedScrollErrorY = 0; 143 144 // Whether input events are delivered right before vsync. 145 private final boolean mInputEventsDeliveredAtVSync; 146 147 // Keeps track of the last long press event, if we end up opening a context menu, we would need 148 // to potentially use the event to send GESTURE_SHOW_PRESS_CANCEL to remove ::active styling 149 private MotionEvent mLastLongPressEvent; 150 151 static final int GESTURE_SHOW_PRESSED_STATE = 0; 152 static final int GESTURE_DOUBLE_TAP = 1; 153 static final int GESTURE_SINGLE_TAP_UP = 2; 154 static final int GESTURE_SINGLE_TAP_CONFIRMED = 3; 155 static final int GESTURE_SINGLE_TAP_UNCONFIRMED = 4; 156 static final int GESTURE_LONG_PRESS = 5; 157 static final int GESTURE_SCROLL_START = 6; 158 static final int GESTURE_SCROLL_BY = 7; 159 static final int GESTURE_SCROLL_END = 8; 160 static final int GESTURE_FLING_START = 9; 161 static final int GESTURE_FLING_CANCEL = 10; 162 static final int GESTURE_PINCH_BEGIN = 11; 163 static final int GESTURE_PINCH_BY = 12; 164 static final int GESTURE_PINCH_END = 13; 165 static final int GESTURE_SHOW_PRESS_CANCEL = 14; 166 static final int GESTURE_LONG_TAP = 15; 167 168 // These have to be kept in sync with content/port/common/input_event_ack_state.h 169 static final int INPUT_EVENT_ACK_STATE_UNKNOWN = 0; 170 static final int INPUT_EVENT_ACK_STATE_CONSUMED = 1; 171 static final int INPUT_EVENT_ACK_STATE_NOT_CONSUMED = 2; 172 static final int INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS = 3; 173 174 // Return values of sendPendingEventToNative(); 175 static final int EVENT_FORWARDED_TO_NATIVE = 0; 176 static final int EVENT_CONVERTED_TO_CANCEL = 1; 177 static final int EVENT_NOT_FORWARDED = 2; 178 179 private final float mPxToDp; 180 181 static final int DOUBLE_TAP_DRAG_MODE_NONE = 0; 182 static final int DOUBLE_TAP_DRAG_MODE_DETECTION_IN_PROGRESS = 1; 183 static final int DOUBLE_TAP_DRAG_MODE_ZOOM = 2; 184 static final int DOUBLE_TAP_DRAG_MODE_DISABLED = 3; 185 186 private class TouchEventTimeoutHandler implements Runnable { 187 private static final int TOUCH_EVENT_TIMEOUT = 200; 188 private static final int PENDING_ACK_NONE = 0; 189 private static final int PENDING_ACK_ORIGINAL_EVENT = 1; 190 private static final int PENDING_ACK_CANCEL_EVENT = 2; 191 192 private long mEventTime; 193 private TouchPoint[] mTouchPoints; 194 private Handler mHandler = new Handler(); 195 private int mPendingAckState; 196 197 public void start(long eventTime, TouchPoint[] pts) { 198 assert mTouchPoints == null; 199 assert mPendingAckState == PENDING_ACK_NONE; 200 mEventTime = eventTime; 201 mTouchPoints = pts; 202 mHandler.postDelayed(this, TOUCH_EVENT_TIMEOUT); 203 } 204 205 @Override 206 public void run() { 207 TraceEvent.begin("TouchEventTimeout"); 208 while (!mPendingMotionEvents.isEmpty()) { 209 MotionEvent nextEvent = mPendingMotionEvents.removeFirst(); 210 processTouchEvent(nextEvent); 211 recycleEvent(nextEvent); 212 } 213 // We are waiting for 2 ACKs: one for the timed-out event, the other for 214 // the touchcancel event injected when the timed-out event is ACK'ed. 215 mPendingAckState = PENDING_ACK_ORIGINAL_EVENT; 216 TraceEvent.end(); 217 } 218 219 public boolean hasTimeoutEvent() { 220 return mPendingAckState != PENDING_ACK_NONE; 221 } 222 223 /** 224 * @return Whether the ACK is consumed in this method. 225 */ 226 public boolean confirmTouchEvent() { 227 switch (mPendingAckState) { 228 case PENDING_ACK_NONE: 229 // The ACK to the original event is received before timeout. 230 mHandler.removeCallbacks(this); 231 mTouchPoints = null; 232 return false; 233 case PENDING_ACK_ORIGINAL_EVENT: 234 TraceEvent.instant("TouchEventTimeout:ConfirmOriginalEvent"); 235 // The ACK to the original event is received after timeout. 236 // Inject a touchcancel event. 237 mPendingAckState = PENDING_ACK_CANCEL_EVENT; 238 mMotionEventDelegate.sendTouchEvent(mEventTime + TOUCH_EVENT_TIMEOUT, 239 TouchPoint.TOUCH_EVENT_TYPE_CANCEL, mTouchPoints); 240 mTouchPoints = null; 241 return true; 242 case PENDING_ACK_CANCEL_EVENT: 243 TraceEvent.instant("TouchEventTimeout:ConfirmCancelEvent"); 244 // The ACK to the injected touchcancel event is received. 245 mPendingAckState = PENDING_ACK_NONE; 246 drainAllPendingEventsUntilNextDown(); 247 return true; 248 default: 249 assert false : "Never reached"; 250 return false; 251 } 252 } 253 254 public void mockTimeout() { 255 assert !hasTimeoutEvent(); 256 mHandler.removeCallbacks(this); 257 run(); 258 } 259 260 /** 261 * This is for testing only. 262 * @return Whether a timeout event has been scheduled but not yet run. 263 */ 264 public boolean hasScheduledTimeoutEventForTesting() { 265 return mTouchPoints != null && mPendingAckState == PENDING_ACK_NONE; 266 } 267 } 268 269 private TouchEventTimeoutHandler mTouchEventTimeoutHandler = new TouchEventTimeoutHandler(); 270 271 /** 272 * This is an interface to handle MotionEvent related communication with the native side also 273 * access some ContentView specific parameters. 274 */ 275 public interface MotionEventDelegate { 276 /** 277 * Send a raw {@link MotionEvent} to the native side 278 * @param timeMs Time of the event in ms. 279 * @param action The action type for the event. 280 * @param pts The TouchPoint array to be sent for the event. 281 * @return Whether the event was sent to the native side successfully or not. 282 */ 283 public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts); 284 285 /** 286 * Send a gesture event to the native side. 287 * @param type The type of the gesture event. 288 * @param timeMs The time the gesture event occurred at. 289 * @param x The x location for the gesture event. 290 * @param y The y location for the gesture event. 291 * @param lastInputEventForVSync Indicates that this gesture event is the last input 292 * to be event sent during the current vsync interval. 293 * @param extraParams A bundle that holds specific extra parameters for certain gestures. 294 * Refer to gesture type definition for more information. 295 * @return Whether the gesture was sent successfully. 296 */ 297 boolean sendGesture( 298 int type, long timeMs, int x, int y, boolean lastInputEventForVSync, 299 Bundle extraParams); 300 301 /** 302 * Gives the UI the chance to override each scroll event. 303 * @param x The amount scrolled in the X direction. 304 * @param y The amount scrolled in the Y direction. 305 * @return Whether or not the UI consumed and handled this event. 306 */ 307 boolean didUIStealScroll(float x, float y); 308 309 /** 310 * Show the zoom picker UI. 311 */ 312 public void invokeZoomPicker(); 313 314 /** 315 * @return Whether changing the page scale is not possible on the current page. 316 */ 317 public boolean hasFixedPageScale(); 318 } 319 320 ContentViewGestureHandler( 321 Context context, MotionEventDelegate delegate, ZoomManager zoomManager, 322 int inputEventDeliveryMode) { 323 mExtraParamBundle = new Bundle(); 324 mLongPressDetector = new LongPressDetector(context, this); 325 mMotionEventDelegate = delegate; 326 mZoomManager = zoomManager; 327 mSnapScrollController = new SnapScrollController(context, mZoomManager); 328 mInputEventsDeliveredAtVSync = 329 inputEventDeliveryMode == ContentViewCore.INPUT_EVENTS_DELIVERED_AT_VSYNC; 330 mPxToDp = 1.0f / context.getResources().getDisplayMetrics().density; 331 332 initGestureDetectors(context); 333 } 334 335 /** 336 * Used to override the default long press detector, gesture detector and listener. 337 * This is used for testing only. 338 * @param longPressDetector The new LongPressDetector to be assigned. 339 * @param gestureDetector The new GestureDetector to be assigned. 340 * @param listener The new onGestureListener to be assigned. 341 */ 342 void setTestDependencies( 343 LongPressDetector longPressDetector, GestureDetector gestureDetector, 344 OnGestureListener listener) { 345 if (longPressDetector != null) mLongPressDetector = longPressDetector; 346 if (gestureDetector != null) mGestureDetector = gestureDetector; 347 if (listener != null) mListener = listener; 348 } 349 350 private void initGestureDetectors(final Context context) { 351 final int scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 352 mScaledTouchSlopSquare = scaledTouchSlop * scaledTouchSlop; 353 try { 354 TraceEvent.begin(); 355 GestureDetector.SimpleOnGestureListener listener = 356 new GestureDetector.SimpleOnGestureListener() { 357 @Override 358 public boolean onDown(MotionEvent e) { 359 mShowPressIsCalled = false; 360 mIgnoreSingleTap = false; 361 mTouchScrolling = false; 362 mSeenFirstScrollEvent = false; 363 mSnapScrollController.resetSnapScrollMode(); 364 mLastRawX = e.getRawX(); 365 mLastRawY = e.getRawY(); 366 mAccumulatedScrollErrorX = 0; 367 mAccumulatedScrollErrorY = 0; 368 // Return true to indicate that we want to handle touch 369 return true; 370 } 371 372 @Override 373 public boolean onScroll(MotionEvent e1, MotionEvent e2, 374 float distanceX, float distanceY) { 375 if (!mSeenFirstScrollEvent) { 376 // Remove the touch slop region from the first scroll event to avoid a 377 // jump. 378 mSeenFirstScrollEvent = true; 379 double distance = Math.sqrt( 380 distanceX * distanceX + distanceY * distanceY); 381 double epsilon = 1e-3; 382 if (distance > epsilon) { 383 double ratio = Math.max(0, distance - scaledTouchSlop) / distance; 384 distanceX *= ratio; 385 distanceY *= ratio; 386 } 387 } 388 mSnapScrollController.updateSnapScrollMode(distanceX, distanceY); 389 if (mSnapScrollController.isSnappingScrolls()) { 390 if (mSnapScrollController.isSnapHorizontal()) { 391 distanceY = 0; 392 } else { 393 distanceX = 0; 394 } 395 } 396 397 boolean didUIStealScroll = mMotionEventDelegate.didUIStealScroll( 398 e2.getRawX() - mLastRawX, e2.getRawY() - mLastRawY); 399 400 mLastRawX = e2.getRawX(); 401 mLastRawY = e2.getRawY(); 402 if (didUIStealScroll) return true; 403 if (!mTouchScrolling) { 404 sendShowPressCancelIfNecessary(e1); 405 endFlingIfNecessary(e2.getEventTime()); 406 if (sendMotionEventAsGesture(GESTURE_SCROLL_START, e1, null)) { 407 mTouchScrolling = true; 408 } 409 } 410 // distanceX and distanceY is the scrolling offset since last onScroll. 411 // Because we are passing integers to webkit, this could introduce 412 // rounding errors. The rounding errors will accumulate overtime. 413 // To solve this, we should be adding back the rounding errors each time 414 // when we calculate the new offset. 415 int x = (int) e2.getX(); 416 int y = (int) e2.getY(); 417 int dx = (int) (distanceX + mAccumulatedScrollErrorX); 418 int dy = (int) (distanceY + mAccumulatedScrollErrorY); 419 mAccumulatedScrollErrorX = distanceX + mAccumulatedScrollErrorX - dx; 420 mAccumulatedScrollErrorY = distanceY + mAccumulatedScrollErrorY - dy; 421 mExtraParamBundle.clear(); 422 mExtraParamBundle.putInt(DISTANCE_X, dx); 423 mExtraParamBundle.putInt(DISTANCE_Y, dy); 424 if ((dx | dy) != 0) { 425 sendLastGestureForVSync(GESTURE_SCROLL_BY, 426 e2.getEventTime(), x, y, mExtraParamBundle); 427 } 428 429 mMotionEventDelegate.invokeZoomPicker(); 430 431 return true; 432 } 433 434 @Override 435 public boolean onFling(MotionEvent e1, MotionEvent e2, 436 float velocityX, float velocityY) { 437 if (mSnapScrollController.isSnappingScrolls()) { 438 if (mSnapScrollController.isSnapHorizontal()) { 439 velocityY = 0; 440 } else { 441 velocityX = 0; 442 } 443 } 444 445 fling(e1.getEventTime(),(int) e1.getX(0), (int) e1.getY(0), 446 (int) velocityX, (int) velocityY); 447 return true; 448 } 449 450 @Override 451 public void onShowPress(MotionEvent e) { 452 mShowPressIsCalled = true; 453 sendMotionEventAsGesture(GESTURE_SHOW_PRESSED_STATE, e, null); 454 } 455 456 @Override 457 public boolean onSingleTapUp(MotionEvent e) { 458 if (isDistanceBetweenDownAndUpTooLong(e.getRawX(), e.getRawY())) { 459 mIgnoreSingleTap = true; 460 return true; 461 } 462 // This is a hack to address the issue where user hovers 463 // over a link for longer than DOUBLE_TAP_TIMEOUT, then 464 // onSingleTapConfirmed() is not triggered. But we still 465 // want to trigger the tap event at UP. So we override 466 // onSingleTapUp() in this case. This assumes singleTapUp 467 // gets always called before singleTapConfirmed. 468 if (!mIgnoreSingleTap && !mLongPressDetector.isInLongPress()) { 469 if (e.getEventTime() - e.getDownTime() > DOUBLE_TAP_TIMEOUT) { 470 float x = e.getX(); 471 float y = e.getY(); 472 if (sendMotionEventAsGesture(GESTURE_SINGLE_TAP_UP, e, null)) { 473 mIgnoreSingleTap = true; 474 } 475 setClickXAndY((int) x, (int) y); 476 return true; 477 } else if (mMotionEventDelegate.hasFixedPageScale()) { 478 // If page is not user scalable, we don't need to wait 479 // for double tap timeout. 480 float x = e.getX(); 481 float y = e.getY(); 482 mExtraParamBundle.clear(); 483 mExtraParamBundle.putBoolean(SHOW_PRESS, mShowPressIsCalled); 484 if (sendMotionEventAsGesture(GESTURE_SINGLE_TAP_CONFIRMED, e, 485 mExtraParamBundle)) { 486 mIgnoreSingleTap = true; 487 } 488 setClickXAndY((int) x, (int) y); 489 } else { 490 // Notify Blink about this tapUp event anyway, 491 // when none of the above conditions applied. 492 sendMotionEventAsGesture(GESTURE_SINGLE_TAP_UNCONFIRMED, e, null); 493 } 494 } 495 496 return triggerLongTapIfNeeded(e); 497 } 498 499 @Override 500 public boolean onSingleTapConfirmed(MotionEvent e) { 501 // Long taps in the edges of the screen have their events delayed by 502 // ContentViewHolder for tab swipe operations. As a consequence of the delay 503 // this method might be called after receiving the up event. 504 // These corner cases should be ignored. 505 if (mLongPressDetector.isInLongPress() || mIgnoreSingleTap) return true; 506 507 int x = (int) e.getX(); 508 int y = (int) e.getY(); 509 mExtraParamBundle.clear(); 510 mExtraParamBundle.putBoolean(SHOW_PRESS, mShowPressIsCalled); 511 sendMotionEventAsGesture(GESTURE_SINGLE_TAP_CONFIRMED, e, 512 mExtraParamBundle); 513 setClickXAndY(x, y); 514 return true; 515 } 516 517 @Override 518 public boolean onDoubleTapEvent(MotionEvent e) { 519 if (isDoubleTapDragDisabled()) return false; 520 switch (e.getActionMasked()) { 521 case MotionEvent.ACTION_DOWN: 522 sendShowPressCancelIfNecessary(e); 523 mDoubleTapDragZoomAnchorX = e.getX(); 524 mDoubleTapDragZoomAnchorY = e.getY(); 525 mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_DETECTION_IN_PROGRESS; 526 break; 527 case MotionEvent.ACTION_MOVE: 528 if (mDoubleTapDragMode 529 == DOUBLE_TAP_DRAG_MODE_DETECTION_IN_PROGRESS) { 530 float distanceX = mDoubleTapDragZoomAnchorX - e.getX(); 531 float distanceY = mDoubleTapDragZoomAnchorY - e.getY(); 532 533 // Begin double tap drag zoom mode if the move distance is 534 // further than the threshold. 535 if (distanceX * distanceX + distanceY * distanceY > 536 mScaledTouchSlopSquare) { 537 sendGesture(GESTURE_SCROLL_START, e.getEventTime(), 538 (int) e.getX(), (int) e.getY(), null); 539 pinchBegin(e.getEventTime(), 540 Math.round(mDoubleTapDragZoomAnchorX), 541 Math.round(mDoubleTapDragZoomAnchorY)); 542 mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_ZOOM; 543 } 544 } else if (mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_ZOOM) { 545 mExtraParamBundle.clear(); 546 sendGesture(GESTURE_SCROLL_BY, e.getEventTime(), 547 (int) e.getX(), (int) e.getY(), mExtraParamBundle); 548 549 float dy = mDoubleTapY - e.getY(); 550 pinchBy(e.getEventTime(), 551 Math.round(mDoubleTapDragZoomAnchorX), 552 Math.round(mDoubleTapDragZoomAnchorY), 553 (float) Math.pow(dy > 0 ? 554 1.0f - DOUBLE_TAP_DRAG_ZOOM_SPEED : 555 1.0f + DOUBLE_TAP_DRAG_ZOOM_SPEED, 556 Math.abs(dy * mPxToDp))); 557 } 558 break; 559 case MotionEvent.ACTION_UP: 560 if (mDoubleTapDragMode != DOUBLE_TAP_DRAG_MODE_ZOOM) { 561 // Normal double tap gesture. 562 sendMotionEventAsGesture(GESTURE_DOUBLE_TAP, e, null); 563 } 564 endDoubleTapDragMode(e); 565 break; 566 case MotionEvent.ACTION_CANCEL: 567 endDoubleTapDragMode(e); 568 break; 569 default: 570 break; 571 } 572 mDoubleTapY = e.getY(); 573 return true; 574 } 575 576 @Override 577 public void onLongPress(MotionEvent e) { 578 if (!mZoomManager.isScaleGestureDetectionInProgress() && 579 (mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_NONE || 580 isDoubleTapDragDisabled())) { 581 mLastLongPressEvent = e; 582 sendMotionEventAsGesture(GESTURE_LONG_PRESS, e, null); 583 } 584 } 585 586 /** 587 * This method inspects the distance between where the user started touching 588 * the surface, and where she released. If the points are too far apart, we 589 * should assume that the web page has consumed the scroll-events in-between, 590 * and as such, this should not be considered a single-tap. 591 * 592 * We use the Android frameworks notion of how far a touch can wander before 593 * we think the user is scrolling. 594 * 595 * @param x the new x coordinate 596 * @param y the new y coordinate 597 * @return true if the distance is too long to be considered a single tap 598 */ 599 private boolean isDistanceBetweenDownAndUpTooLong(float x, float y) { 600 double deltaX = mLastRawX - x; 601 double deltaY = mLastRawY - y; 602 return deltaX * deltaX + deltaY * deltaY > mScaledTouchSlopSquare; 603 } 604 }; 605 mListener = listener; 606 mGestureDetector = new GestureDetector(context, listener); 607 mGestureDetector.setIsLongpressEnabled(false); 608 } finally { 609 TraceEvent.end(); 610 } 611 } 612 613 /** 614 * @return LongPressDetector handling setting up timers for and canceling LongPress gestures. 615 */ 616 LongPressDetector getLongPressDetector() { 617 return mLongPressDetector; 618 } 619 620 /** 621 * @param event Start a LongPress gesture event from the listener. 622 */ 623 @Override 624 public void onLongPress(MotionEvent event) { 625 mListener.onLongPress(event); 626 } 627 628 /** 629 * Cancels any ongoing LongPress timers. 630 */ 631 void cancelLongPress() { 632 mLongPressDetector.cancelLongPress(); 633 } 634 635 /** 636 * Fling the ContentView from the current position. 637 * @param x Fling touch starting position 638 * @param y Fling touch starting position 639 * @param velocityX Initial velocity of the fling (X) measured in pixels per second. 640 * @param velocityY Initial velocity of the fling (Y) measured in pixels per second. 641 */ 642 void fling(long timeMs, int x, int y, int velocityX, int velocityY) { 643 endFlingIfNecessary(timeMs); 644 if (!mTouchScrolling) { 645 // The native side needs a GESTURE_SCROLL_BEGIN before GESTURE_FLING_START 646 // to send the fling to the correct target. Send if it has not sent. 647 sendGesture(GESTURE_SCROLL_START, timeMs, x, y, null); 648 } 649 endTouchScrollIfNecessary(timeMs, false); 650 651 mFlingMayBeActive = true; 652 653 mExtraParamBundle.clear(); 654 mExtraParamBundle.putInt(VELOCITY_X, velocityX); 655 mExtraParamBundle.putInt(VELOCITY_Y, velocityY); 656 sendGesture(GESTURE_FLING_START, timeMs, x, y, mExtraParamBundle); 657 } 658 659 /** 660 * Send a GESTURE_FLING_CANCEL event if necessary. 661 * @param timeMs The time in ms for the event initiating this gesture. 662 */ 663 void endFlingIfNecessary(long timeMs) { 664 if (!mFlingMayBeActive) return; 665 mFlingMayBeActive = false; 666 sendGesture(GESTURE_FLING_CANCEL, timeMs, 0, 0, null); 667 } 668 669 /** 670 * End DOUBLE_TAP_DRAG_MODE_ZOOM by sending GESTURE_SCROLL_END and GESTURE_PINCH_END events. 671 * @param event A hint event that its x, y, and eventTime will be used for the ending events 672 * to send. This argument is an optional and can be null. 673 */ 674 void endDoubleTapDragMode(MotionEvent event) { 675 if (isDoubleTapDragDisabled()) return; 676 if (mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_ZOOM) { 677 if (event == null) event = obtainActionCancelMotionEvent(); 678 pinchEnd(event.getEventTime()); 679 sendGesture(GESTURE_SCROLL_END, event.getEventTime(), 680 (int) event.getX(), (int) event.getY(), null); 681 } 682 mDoubleTapDragMode = DOUBLE_TAP_DRAG_MODE_NONE; 683 } 684 685 /** 686 * Reset touch scroll flag and optionally send a GESTURE_SCROLL_END event if necessary. 687 * @param timeMs The time in ms for the event initiating this gesture. 688 * @param sendScrollEndEvent Whether to send GESTURE_SCROLL_END event. 689 */ 690 private void endTouchScrollIfNecessary(long timeMs, boolean sendScrollEndEvent) { 691 if (!mTouchScrolling) return; 692 mTouchScrolling = false; 693 if (sendScrollEndEvent) { 694 sendGesture(GESTURE_SCROLL_END, timeMs, 0, 0, null); 695 } 696 } 697 698 /** 699 * @return Whether native is tracking a scroll. 700 */ 701 boolean isNativeScrolling() { 702 // TODO(wangxianzhu): Also return true when fling is active once the UI knows exactly when 703 // the fling ends. 704 return mTouchScrolling; 705 } 706 707 /** 708 * @return Whether native is tracking a pinch (i.e. between sending GESTURE_PINCH_BEGIN and 709 * GESTURE_PINCH_END). 710 */ 711 boolean isNativePinching() { 712 return mPinchInProgress; 713 } 714 715 /** 716 * Starts a pinch gesture. 717 * @param timeMs The time in ms for the event initiating this gesture. 718 * @param x The x coordinate for the event initiating this gesture. 719 * @param y The x coordinate for the event initiating this gesture. 720 */ 721 void pinchBegin(long timeMs, int x, int y) { 722 sendGesture(GESTURE_PINCH_BEGIN, timeMs, x, y, null); 723 } 724 725 /** 726 * Pinch by a given percentage. 727 * @param timeMs The time in ms for the event initiating this gesture. 728 * @param anchorX The x coordinate for the anchor point to be used in pinch. 729 * @param anchorY The y coordinate for the anchor point to be used in pinch. 730 * @param delta The percentage to pinch by. 731 */ 732 void pinchBy(long timeMs, int anchorX, int anchorY, float delta) { 733 mExtraParamBundle.clear(); 734 mExtraParamBundle.putFloat(DELTA, delta); 735 sendLastGestureForVSync(GESTURE_PINCH_BY, timeMs, anchorX, anchorY, mExtraParamBundle); 736 mPinchInProgress = true; 737 } 738 739 /** 740 * End a pinch gesture. 741 * @param timeMs The time in ms for the event initiating this gesture. 742 */ 743 void pinchEnd(long timeMs) { 744 sendGesture(GESTURE_PINCH_END, timeMs, 0, 0, null); 745 mPinchInProgress = false; 746 } 747 748 /** 749 * Ignore singleTap gestures. 750 */ 751 void setIgnoreSingleTap(boolean value) { 752 mIgnoreSingleTap = value; 753 } 754 755 private void setClickXAndY(int x, int y) { 756 mSingleTapX = x; 757 mSingleTapY = y; 758 } 759 760 /** 761 * @return The x coordinate for the last point that a singleTap gesture was initiated from. 762 */ 763 public int getSingleTapX() { 764 return mSingleTapX; 765 } 766 767 /** 768 * @return The y coordinate for the last point that a singleTap gesture was initiated from. 769 */ 770 public int getSingleTapY() { 771 return mSingleTapY; 772 } 773 774 /** 775 * Handle the incoming MotionEvent. 776 * @return Whether the event was handled. 777 */ 778 boolean onTouchEvent(MotionEvent event) { 779 try { 780 TraceEvent.begin("onTouchEvent"); 781 mLongPressDetector.cancelLongPressIfNeeded(event); 782 mSnapScrollController.setSnapScrollingMode(event); 783 // Notify native that scrolling has stopped whenever a down action is processed prior to 784 // passing the event to native as it will drop them as an optimization if scrolling is 785 // enabled. Ending the fling ensures scrolling has stopped as well as terminating the 786 // current fling if applicable. 787 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 788 mNoTouchHandlerForGesture = false; 789 mJavaScriptIsConsumingGesture = false; 790 endFlingIfNecessary(event.getEventTime()); 791 } else if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { 792 endDoubleTapDragMode(null); 793 } 794 795 if (offerTouchEventToJavaScript(event)) { 796 // offerTouchEventToJavaScript returns true to indicate the event was sent 797 // to the render process. If it is not subsequently handled, it will 798 // be returned via confirmTouchEvent(false) and eventually passed to 799 // processTouchEvent asynchronously. 800 return true; 801 } 802 return processTouchEvent(event); 803 } finally { 804 TraceEvent.end("onTouchEvent"); 805 } 806 } 807 808 /** 809 * Handle content view losing focus -- ensure that any remaining active state is removed. 810 */ 811 void onWindowFocusLost() { 812 if (mLongPressDetector.isInLongPress() && mLastLongPressEvent != null) { 813 sendShowPressCancelIfNecessary(mLastLongPressEvent); 814 } 815 } 816 817 private MotionEvent obtainActionCancelMotionEvent() { 818 return MotionEvent.obtain( 819 SystemClock.uptimeMillis(), 820 SystemClock.uptimeMillis(), 821 MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); 822 } 823 824 /** 825 * Resets gesture handlers state; called on didStartLoading(). 826 * Note that this does NOT clear the pending motion events queue; 827 * it gets cleared in hasTouchEventHandlers() called from WebKit 828 * FrameLoader::transitionToCommitted iff the page ever had touch handlers. 829 */ 830 void resetGestureHandlers() { 831 MotionEvent me = obtainActionCancelMotionEvent(); 832 me.setSource(InputDevice.SOURCE_CLASS_POINTER); 833 mGestureDetector.onTouchEvent(me); 834 mZoomManager.processTouchEvent(me); 835 me.recycle(); 836 mLongPressDetector.cancelLongPress(); 837 } 838 839 /** 840 * Sets the flag indicating that the content has registered listeners for touch events. 841 */ 842 void hasTouchEventHandlers(boolean hasTouchHandlers) { 843 mHasTouchHandlers = hasTouchHandlers; 844 // When mainframe is loading, FrameLoader::transitionToCommitted will 845 // call this method to set mHasTouchHandlers to false. We use this as 846 // an indicator to clear the pending motion events so that events from 847 // the previous page will not be carried over to the new page. 848 if (!mHasTouchHandlers) mPendingMotionEvents.clear(); 849 } 850 851 private boolean offerTouchEventToJavaScript(MotionEvent event) { 852 mLongPressDetector.onOfferTouchEventToJavaScript(event); 853 854 if (!mHasTouchHandlers || mNoTouchHandlerForGesture) return false; 855 856 if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { 857 // Only send move events if the move has exceeded the slop threshold. 858 if (!mLongPressDetector.confirmOfferMoveEventToJavaScript(event)) { 859 return true; 860 } 861 // Avoid flooding the renderer process with move events: if the previous pending 862 // command is also a move (common case) that has not yet been forwarded, skip sending 863 // this event to the webkit side and collapse it into the pending event. 864 MotionEvent previousEvent = mPendingMotionEvents.peekLast(); 865 if (previousEvent != null 866 && previousEvent != mPendingMotionEvents.peekFirst() 867 && previousEvent.getActionMasked() == MotionEvent.ACTION_MOVE 868 && previousEvent.getPointerCount() == event.getPointerCount()) { 869 TraceEvent.instant("offerTouchEventToJavaScript:EventCoalesced", 870 "QueueSize = " + mPendingMotionEvents.size()); 871 MotionEvent.PointerCoords[] coords = 872 new MotionEvent.PointerCoords[event.getPointerCount()]; 873 for (int i = 0; i < coords.length; ++i) { 874 coords[i] = new MotionEvent.PointerCoords(); 875 event.getPointerCoords(i, coords[i]); 876 } 877 previousEvent.addBatch(event.getEventTime(), coords, event.getMetaState()); 878 return true; 879 } 880 } 881 if (mPendingMotionEvents.isEmpty()) { 882 // Add the event to the pending queue prior to calling sendPendingEventToNative. 883 // When sending an event to native, the callback to confirmTouchEvent can be 884 // synchronous or asynchronous and confirmTouchEvent expects the event to be 885 // in the queue when it is called. 886 MotionEvent clone = MotionEvent.obtain(event); 887 mPendingMotionEvents.add(clone); 888 889 int forward = sendPendingEventToNative(); 890 if (forward == EVENT_NOT_FORWARDED) mPendingMotionEvents.remove(clone); 891 return forward != EVENT_NOT_FORWARDED; 892 } else { 893 TraceEvent.instant("offerTouchEventToJavaScript:EventQueued", 894 "QueueSize = " + mPendingMotionEvents.size()); 895 // Copy the event, as the original may get mutated after this method returns. 896 MotionEvent clone = MotionEvent.obtain(event); 897 mPendingMotionEvents.add(clone); 898 return true; 899 } 900 } 901 902 private int sendPendingEventToNative() { 903 MotionEvent event = mPendingMotionEvents.peekFirst(); 904 if (event == null) { 905 assert false : "Cannot send from an empty pending event queue"; 906 return EVENT_NOT_FORWARDED; 907 } 908 909 if (mTouchEventTimeoutHandler.hasTimeoutEvent()) return EVENT_NOT_FORWARDED; 910 911 TouchPoint[] pts = new TouchPoint[event.getPointerCount()]; 912 int type = TouchPoint.createTouchPoints(event, pts); 913 914 if (type == TouchPoint.CONVERSION_ERROR) return EVENT_NOT_FORWARDED; 915 916 if (!mTouchScrolling && !mPinchInProgress) { 917 mTouchCancelEventSent = false; 918 919 if (mMotionEventDelegate.sendTouchEvent(event.getEventTime(), type, pts)) { 920 // If confirmTouchEvent() is called synchronously with respect to sendTouchEvent(), 921 // then |event| will have been recycled. Only start the timer if the sent event has 922 // not yet been confirmed. 923 if (!mJavaScriptIsConsumingGesture 924 && event == mPendingMotionEvents.peekFirst() 925 && event.getAction() != MotionEvent.ACTION_UP 926 && event.getAction() != MotionEvent.ACTION_CANCEL) { 927 mTouchEventTimeoutHandler.start(event.getEventTime(), pts); 928 } 929 return EVENT_FORWARDED_TO_NATIVE; 930 } 931 } else if (!mTouchCancelEventSent) { 932 mTouchCancelEventSent = true; 933 934 MotionEvent previousCancelEvent = mLastCancelledEvent; 935 mLastCancelledEvent = event; 936 937 if (mMotionEventDelegate.sendTouchEvent(event.getEventTime(), 938 TouchPoint.TOUCH_EVENT_TYPE_CANCEL, pts)) { 939 return EVENT_CONVERTED_TO_CANCEL; 940 } else { 941 mLastCancelledEvent = previousCancelEvent; 942 } 943 } 944 return EVENT_NOT_FORWARDED; 945 } 946 947 private boolean processTouchEvent(MotionEvent event) { 948 boolean handled = false; 949 // The last "finger up" is an end to scrolling but may not be 950 // an end to movement (e.g. fling scroll). We do not tell 951 // native code to end scrolling until we are sure we did not 952 // fling. 953 boolean possiblyEndMovement = false; 954 // "Last finger raised" could be an end to movement. However, 955 // give the mSimpleTouchDetector a chance to continue 956 // scrolling with a fling. 957 if (event.getAction() == MotionEvent.ACTION_UP) { 958 if (mTouchScrolling) { 959 possiblyEndMovement = true; 960 } 961 } 962 963 mLongPressDetector.cancelLongPressIfNeeded(event); 964 mLongPressDetector.startLongPressTimerIfNeeded(event); 965 966 // Use the framework's GestureDetector to detect pans and zooms not already 967 // handled by the WebKit touch events gesture manager. 968 if (canHandle(event)) { 969 handled |= mGestureDetector.onTouchEvent(event); 970 if (event.getAction() == MotionEvent.ACTION_DOWN) { 971 mCurrentDownEvent = MotionEvent.obtain(event); 972 } 973 } 974 975 handled |= mZoomManager.processTouchEvent(event); 976 977 if (possiblyEndMovement && !handled) { 978 endTouchScrollIfNecessary(event.getEventTime(), true); 979 } 980 981 return handled; 982 } 983 984 /** 985 * For testing to simulate a timeout of a touch event handler. 986 */ 987 void mockTouchEventTimeout() { 988 mTouchEventTimeoutHandler.mockTimeout(); 989 } 990 991 /** 992 * Respond to a MotionEvent being returned from the native side. 993 * @param ackResult The status acknowledgment code. 994 */ 995 void confirmTouchEvent(int ackResult) { 996 if (mTouchEventTimeoutHandler.confirmTouchEvent()) return; 997 if (mPendingMotionEvents.isEmpty()) { 998 Log.w(TAG, "confirmTouchEvent with Empty pending list!"); 999 return; 1000 } 1001 TraceEvent.begin("confirmTouchEvent"); 1002 MotionEvent ackedEvent = mPendingMotionEvents.removeFirst(); 1003 if (ackedEvent == mLastCancelledEvent) { 1004 // The event is canceled, just drain all the pending events until next 1005 // touch down. 1006 ackResult = INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS; 1007 TraceEvent.instant("confirmTouchEvent:CanceledEvent"); 1008 } 1009 switch (ackResult) { 1010 case INPUT_EVENT_ACK_STATE_UNKNOWN: 1011 // This should never get sent. 1012 assert(false); 1013 break; 1014 case INPUT_EVENT_ACK_STATE_CONSUMED: 1015 mJavaScriptIsConsumingGesture = true; 1016 mZoomManager.passTouchEventThrough(ackedEvent); 1017 trySendPendingEventsToNative(); 1018 break; 1019 case INPUT_EVENT_ACK_STATE_NOT_CONSUMED: 1020 if (!mJavaScriptIsConsumingGesture) processTouchEvent(ackedEvent); 1021 trySendPendingEventsToNative(); 1022 break; 1023 case INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS: 1024 mNoTouchHandlerForGesture = true; 1025 processTouchEvent(ackedEvent); 1026 drainAllPendingEventsUntilNextDown(); 1027 break; 1028 default: 1029 break; 1030 } 1031 1032 mLongPressDetector.cancelLongPressIfNeeded(mPendingMotionEvents.iterator()); 1033 1034 recycleEvent(ackedEvent); 1035 TraceEvent.end("confirmTouchEvent"); 1036 } 1037 1038 private void trySendPendingEventsToNative() { 1039 while (!mPendingMotionEvents.isEmpty()) { 1040 int forward = sendPendingEventToNative(); 1041 if (forward != EVENT_NOT_FORWARDED) break; 1042 1043 // Even though we missed sending one event to native, as long as we haven't 1044 // received INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, we should keep sending 1045 // events on the queue to native. 1046 MotionEvent event = mPendingMotionEvents.removeFirst(); 1047 if (!mJavaScriptIsConsumingGesture) processTouchEvent(event); 1048 recycleEvent(event); 1049 } 1050 } 1051 1052 private void drainAllPendingEventsUntilNextDown() { 1053 // Now process all events that are in the queue until the next down event. 1054 MotionEvent nextEvent = mPendingMotionEvents.peekFirst(); 1055 while (nextEvent != null && nextEvent.getActionMasked() != MotionEvent.ACTION_DOWN) { 1056 processTouchEvent(nextEvent); 1057 mPendingMotionEvents.removeFirst(); 1058 recycleEvent(nextEvent); 1059 nextEvent = mPendingMotionEvents.peekFirst(); 1060 } 1061 1062 if (nextEvent == null) return; 1063 1064 mNoTouchHandlerForGesture = false; 1065 trySendPendingEventsToNative(); 1066 } 1067 1068 private void recycleEvent(MotionEvent event) { 1069 if (event == mLastCancelledEvent) { 1070 mLastCancelledEvent = null; 1071 } 1072 event.recycle(); 1073 } 1074 1075 private boolean sendMotionEventAsGesture( 1076 int type, MotionEvent event, Bundle extraParams) { 1077 return mMotionEventDelegate.sendGesture(type, event.getEventTime(), 1078 (int) event.getX(), (int) event.getY(), false, extraParams); 1079 } 1080 1081 private boolean sendGesture( 1082 int type, long timeMs, int x, int y, Bundle extraParams) { 1083 return mMotionEventDelegate.sendGesture(type, timeMs, x, y, false, extraParams); 1084 } 1085 1086 private boolean sendLastGestureForVSync( 1087 int type, long timeMs, int x, int y, Bundle extraParams) { 1088 return mMotionEventDelegate.sendGesture( 1089 type, timeMs, x, y, mInputEventsDeliveredAtVSync, extraParams); 1090 } 1091 1092 void sendShowPressCancelIfNecessary(MotionEvent e) { 1093 if (!mShowPressIsCalled) return; 1094 1095 if (sendMotionEventAsGesture(GESTURE_SHOW_PRESS_CANCEL, e, null)) { 1096 mShowPressIsCalled = false; 1097 mLastLongPressEvent = null; 1098 } 1099 } 1100 1101 /** 1102 * @return Whether the ContentViewGestureHandler can handle a MotionEvent right now. True only 1103 * if it's the start of a new stream (ACTION_DOWN), or a continuation of the current stream. 1104 */ 1105 boolean canHandle(MotionEvent ev) { 1106 return ev.getAction() == MotionEvent.ACTION_DOWN || 1107 (mCurrentDownEvent != null && mCurrentDownEvent.getDownTime() == ev.getDownTime()); 1108 } 1109 1110 /** 1111 * @return Whether the event can trigger a LONG_TAP gesture. True when it can and the event 1112 * will be consumed. 1113 */ 1114 boolean triggerLongTapIfNeeded(MotionEvent ev) { 1115 if (mLongPressDetector.isInLongPress() && ev.getAction() == MotionEvent.ACTION_UP && 1116 !mZoomManager.isScaleGestureDetectionInProgress()) { 1117 sendShowPressCancelIfNecessary(ev); 1118 sendMotionEventAsGesture(GESTURE_LONG_TAP, ev, null); 1119 return true; 1120 } 1121 return false; 1122 } 1123 1124 /** 1125 * This is for testing only. 1126 * @return The first motion event on the pending motion events queue. 1127 */ 1128 MotionEvent peekFirstInPendingMotionEventsForTesting() { 1129 return mPendingMotionEvents.peekFirst(); 1130 } 1131 1132 /** 1133 * This is for testing only. 1134 * @return Whether the motion event is cancelled. 1135 */ 1136 boolean isEventCancelledForTesting(MotionEvent event) { 1137 return event != null && event == mLastCancelledEvent; 1138 } 1139 1140 /** 1141 * This is for testing only. 1142 * @return The number of motion events on the pending motion events queue. 1143 */ 1144 int getNumberOfPendingMotionEventsForTesting() { 1145 return mPendingMotionEvents.size(); 1146 } 1147 1148 /** 1149 * This is for testing only. 1150 * Sends a show pressed state gesture through mListener. This should always be called after 1151 * a down event; 1152 */ 1153 void sendShowPressedStateGestureForTesting() { 1154 if (mCurrentDownEvent == null) return; 1155 mListener.onShowPress(mCurrentDownEvent); 1156 } 1157 1158 /** 1159 * This is for testing only. 1160 * @return Whether a touch timeout event has been scheduled. 1161 */ 1162 boolean hasScheduledTouchTimeoutEventForTesting() { 1163 return mTouchEventTimeoutHandler.hasScheduledTimeoutEventForTesting(); 1164 } 1165 1166 public void updateDoubleTapDragSupport(boolean supportDoubleTapDrag) { 1167 assert (mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_DISABLED || 1168 mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_NONE); 1169 mDoubleTapDragMode = supportDoubleTapDrag ? 1170 DOUBLE_TAP_DRAG_MODE_NONE : DOUBLE_TAP_DRAG_MODE_DISABLED; 1171 } 1172 1173 private boolean isDoubleTapDragDisabled() { 1174 return mDoubleTapDragMode == DOUBLE_TAP_DRAG_MODE_DISABLED; 1175 } 1176} 1177