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