2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 // This is a copy of Android GestureDetector.java from AOSP 4.2. We include it in Chromium in order
18 // to have JB-style behavior on ICS devices. Some edits for NDK v16 compliance were needed; they're
19 // noted explicitly below.
21 // New imports in Chromium for NDK compliance.
22 package org.chromium.content.browser.third_party;
23 import android.view.MotionEvent;
24 import android.view.VelocityTracker;
25 import android.view.ViewConfiguration;
27 /* Commented out in Chromium for NDK compliance
31 import android.content.Context;
32 import android.os.Build;
33 import android.os.Handler;
34 import android.os.Message;
37 * Detects various gestures and events using the supplied {@link MotionEvent}s.
38 * The {@link OnGestureListener} callback will notify users when a particular
39 * motion event has occurred. This class should only be used with {@link MotionEvent}s
40 * reported via touch (don't use for trackball events).
44 * <li>Create an instance of the {@code GestureDetector} for your {@link View}
45 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
46 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
47 * will be executed when the events occur.
50 public class GestureDetector {
52 * The listener that is used to notify when gestures occur.
53 * If you want to listen for all the different gestures then implement
54 * this interface. If you only want to listen for a subset it might
55 * be easier to extend {@link SimpleOnGestureListener}.
57 public interface OnGestureListener {
60 * Notified when a tap occurs with the down {@link MotionEvent}
61 * that triggered it. This will be triggered immediately for
62 * every down event. All other events should be preceded by this.
64 * @param e The down motion event.
66 boolean onDown(MotionEvent e);
69 * The user has performed a down {@link MotionEvent} and not performed
70 * a move or up yet. This event is commonly used to provide visual
71 * feedback to the user to let them know that their action has been
72 * recognized i.e. highlight an element.
74 * @param e The down motion event
76 void onShowPress(MotionEvent e);
79 * Notified when a tap occurs with the up {@link MotionEvent}
82 * @param e The up motion event that completed the first tap
83 * @return true if the event is consumed, else false
85 boolean onSingleTapUp(MotionEvent e);
88 * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
89 * current move {@link MotionEvent}. The distance in x and y is also supplied for
92 * @param e1 The first down motion event that started the scrolling.
93 * @param e2 The move motion event that triggered the current onScroll.
94 * @param distanceX The distance along the X axis that has been scrolled since the last
95 * call to onScroll. This is NOT the distance between {@code e1}
97 * @param distanceY The distance along the Y axis that has been scrolled since the last
98 * call to onScroll. This is NOT the distance between {@code e1}
100 * @return true if the event is consumed, else false
102 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
105 * Notified when a long press occurs with the initial on down {@link MotionEvent}
108 * @param e The initial on down motion event that started the longpress.
110 void onLongPress(MotionEvent e);
113 * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
114 * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
115 * the x and y axis in pixels per second.
117 * @param e1 The first down motion event that started the fling.
118 * @param e2 The move motion event that triggered the current onFling.
119 * @param velocityX The velocity of this fling measured in pixels per second
121 * @param velocityY The velocity of this fling measured in pixels per second
123 * @return true if the event is consumed, else false
125 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
129 * The listener that is used to notify when a double-tap or a confirmed
132 public interface OnDoubleTapListener {
134 * Notified when a single-tap occurs.
136 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
137 * will only be called after the detector is confident that the user's
138 * first tap is not followed by a second tap leading to a double-tap
141 * @param e The down motion event of the single-tap.
142 * @return true if the event is consumed, else false
144 boolean onSingleTapConfirmed(MotionEvent e);
147 * Notified when a double-tap occurs.
149 * @param e The down motion event of the first tap of the double-tap.
150 * @return true if the event is consumed, else false
152 boolean onDoubleTap(MotionEvent e);
155 * Notified when an event within a double-tap gesture occurs, including
156 * the down, move, and up events.
158 * @param e The motion event that occurred during the double-tap gesture.
159 * @return true if the event is consumed, else false
161 boolean onDoubleTapEvent(MotionEvent e);
165 * A convenience class to extend when you only want to listen for a subset
166 * of all the gestures. This implements all methods in the
167 * {@link OnGestureListener} and {@link OnDoubleTapListener} but does
168 * nothing and return {@code false} for all applicable methods.
170 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
171 public boolean onSingleTapUp(MotionEvent e) {
175 public void onLongPress(MotionEvent e) {
178 public boolean onScroll(MotionEvent e1, MotionEvent e2,
179 float distanceX, float distanceY) {
183 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
188 public void onShowPress(MotionEvent e) {
191 public boolean onDown(MotionEvent e) {
195 public boolean onDoubleTap(MotionEvent e) {
199 public boolean onDoubleTapEvent(MotionEvent e) {
203 public boolean onSingleTapConfirmed(MotionEvent e) {
208 private int mTouchSlopSquare;
209 private int mDoubleTapTouchSlopSquare;
210 private int mDoubleTapSlopSquare;
211 private int mMinimumFlingVelocity;
212 private int mMaximumFlingVelocity;
214 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
215 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
216 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
218 // constants for Message.what used by GestureHandler below
219 private static final int SHOW_PRESS = 1;
220 private static final int LONG_PRESS = 2;
221 private static final int TAP = 3;
223 private final Handler mHandler;
224 private final OnGestureListener mListener;
225 private OnDoubleTapListener mDoubleTapListener;
227 private boolean mStillDown;
228 private boolean mDeferConfirmSingleTap;
229 private boolean mInLongPress;
230 private boolean mAlwaysInTapRegion;
231 private boolean mAlwaysInBiggerTapRegion;
233 private MotionEvent mCurrentDownEvent;
234 private MotionEvent mPreviousUpEvent;
237 * True when the user is still touching for the second tap (down, move, and
238 * up events). Can only be true if there is a double tap listener attached.
240 private boolean mIsDoubleTapping;
242 private float mLastFocusX;
243 private float mLastFocusY;
244 private float mDownFocusX;
245 private float mDownFocusY;
247 private boolean mIsLongpressEnabled;
250 * Determines speed during touch scrolling
252 private VelocityTracker mVelocityTracker;
255 * Consistency verifier for debugging purposes.
257 /* Commented out in Chromium for NDK compliance
258 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
259 InputEventConsistencyVerifier.isInstrumentationEnabled() ?
260 new InputEventConsistencyVerifier(this, 0) : null;
263 private class GestureHandler extends Handler {
268 GestureHandler(Handler handler) {
269 super(handler.getLooper());
273 public void handleMessage(Message msg) {
276 mListener.onShowPress(mCurrentDownEvent);
284 // If the user's finger is still down, do not count it as a tap
285 if (mDoubleTapListener != null) {
287 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
289 mDeferConfirmSingleTap = true;
295 throw new RuntimeException("Unknown message " + msg); //never
301 * Creates a GestureDetector with the supplied listener.
302 * This variant of the constructor should be used from a non-UI thread
303 * (as it allows specifying the Handler).
305 * @param listener the listener invoked for all the callbacks, this must
307 * @param handler the handler to use
309 * @throws NullPointerException if either {@code listener} or
310 * {@code handler} is null.
312 * @deprecated Use {@link #GestureDetector(android.content.Context,
313 * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
316 public GestureDetector(OnGestureListener listener, Handler handler) {
317 this(null, listener, handler);
321 * Creates a GestureDetector with the supplied listener.
322 * You may only use this constructor from a UI thread (this is the usual situation).
323 * @see android.os.Handler#Handler()
325 * @param listener the listener invoked for all the callbacks, this must
328 * @throws NullPointerException if {@code listener} is null.
330 * @deprecated Use {@link #GestureDetector(android.content.Context,
331 * android.view.GestureDetector.OnGestureListener)} instead.
334 public GestureDetector(OnGestureListener listener) {
335 this(null, listener, null);
339 * Creates a GestureDetector with the supplied listener.
340 * You may only use this constructor from a UI thread (this is the usual situation).
341 * @see android.os.Handler#Handler()
343 * @param context the application's context
344 * @param listener the listener invoked for all the callbacks, this must
347 * @throws NullPointerException if {@code listener} is null.
349 public GestureDetector(Context context, OnGestureListener listener) {
350 this(context, listener, null);
354 * Creates a GestureDetector with the supplied listener.
355 * You may only use this constructor from a UI thread (this is the usual situation).
356 * @see android.os.Handler#Handler()
358 * @param context the application's context
359 * @param listener the listener invoked for all the callbacks, this must
361 * @param handler the handler to use
363 * @throws NullPointerException if {@code listener} is null.
365 public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
366 if (handler != null) {
367 mHandler = new GestureHandler(handler);
369 mHandler = new GestureHandler();
371 mListener = listener;
372 if (listener instanceof OnDoubleTapListener) {
373 setOnDoubleTapListener((OnDoubleTapListener) listener);
379 * Creates a GestureDetector with the supplied listener.
380 * You may only use this constructor from a UI thread (this is the usual situation).
381 * @see android.os.Handler#Handler()
383 * @param context the application's context
384 * @param listener the listener invoked for all the callbacks, this must
386 * @param handler the handler to use
388 * @throws NullPointerException if {@code listener} is null.
390 public GestureDetector(Context context, OnGestureListener listener, Handler handler,
392 this(context, listener, handler);
395 private void init(Context context) {
396 if (mListener == null) {
397 throw new NullPointerException("OnGestureListener must not be null");
399 mIsLongpressEnabled = true;
401 // Fallback to support pre-donuts releases
402 int touchSlop, doubleTapSlop, doubleTapTouchSlop;
403 /* Commented out in Chromium for NDK compliance
404 if (context == null) {
405 //noinspection deprecation
406 touchSlop = ViewConfiguration.getTouchSlop();
407 doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
408 doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
409 //noinspection deprecation
410 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
411 mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
413 final ViewConfiguration configuration = ViewConfiguration.get(context);
414 touchSlop = configuration.getScaledTouchSlop();
415 /* Commented out in Chromium for NDK compliance and replaced with the following line. Note that
416 * ViewConfiguration.TOUCH_SLOP has the same value as DOUBLE_TAP_TOUCH_SLOP in current Android, so
417 * this doesn't introduce a behavior difference in Android versions <= 4.2.
418 doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
420 doubleTapTouchSlop = touchSlop;
421 doubleTapSlop = configuration.getScaledDoubleTapSlop();
422 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
423 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
425 mTouchSlopSquare = touchSlop * touchSlop;
426 mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
427 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
431 * Sets the listener which will be called for double-tap and related
434 * @param onDoubleTapListener the listener invoked for all the callbacks, or
435 * null to stop listening for double-tap gestures.
437 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
438 mDoubleTapListener = onDoubleTapListener;
442 * Set whether longpress is enabled, if this is enabled when a user
443 * presses and holds down you get a longpress event and nothing further.
444 * If it's disabled the user can press and hold down and then later
445 * moved their finger and you will get scroll events. By default
446 * longpress is enabled.
448 * @param isLongpressEnabled whether longpress should be enabled.
450 public void setIsLongpressEnabled(boolean isLongpressEnabled) {
451 mIsLongpressEnabled = isLongpressEnabled;
455 * @return true if longpress is enabled, else false.
457 public boolean isLongpressEnabled() {
458 return mIsLongpressEnabled;
462 * Analyzes the given motion event and if applicable triggers the
463 * appropriate callbacks on the {@link OnGestureListener} supplied.
465 * @param ev The current motion event.
466 * @return true if the {@link OnGestureListener} consumed the event,
469 public boolean onTouchEvent(MotionEvent ev) {
470 /* Commented out in Chromium for NDK compliance
471 if (mInputEventConsistencyVerifier != null) {
472 mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
476 final int action = ev.getAction();
478 if (mVelocityTracker == null) {
479 mVelocityTracker = VelocityTracker.obtain();
481 mVelocityTracker.addMovement(ev);
483 final boolean pointerUp =
484 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
485 final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
487 // Determine focal point
488 float sumX = 0, sumY = 0;
489 final int count = ev.getPointerCount();
490 for (int i = 0; i < count; i++) {
491 if (skipIndex == i) continue;
495 final int div = pointerUp ? count - 1 : count;
496 final float focusX = sumX / div;
497 final float focusY = sumY / div;
499 boolean handled = false;
501 switch (action & MotionEvent.ACTION_MASK) {
502 case MotionEvent.ACTION_POINTER_DOWN:
503 mDownFocusX = mLastFocusX = focusX;
504 mDownFocusY = mLastFocusY = focusY;
505 // Cancel long press and taps
509 case MotionEvent.ACTION_POINTER_UP:
510 mDownFocusX = mLastFocusX = focusX;
511 mDownFocusY = mLastFocusY = focusY;
513 // Check the dot product of current velocities.
514 // If the pointer that left was opposing another velocity vector, clear.
515 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
516 final int upIndex = ev.getActionIndex();
517 final int id1 = ev.getPointerId(upIndex);
518 final float x1 = mVelocityTracker.getXVelocity(id1);
519 final float y1 = mVelocityTracker.getYVelocity(id1);
520 for (int i = 0; i < count; i++) {
521 if (i == upIndex) continue;
523 final int id2 = ev.getPointerId(i);
524 final float x = x1 * mVelocityTracker.getXVelocity(id2);
525 final float y = y1 * mVelocityTracker.getYVelocity(id2);
527 final float dot = x + y;
529 mVelocityTracker.clear();
535 case MotionEvent.ACTION_DOWN:
536 if (mDoubleTapListener != null) {
537 boolean hadTapMessage = mHandler.hasMessages(TAP);
538 if (hadTapMessage) mHandler.removeMessages(TAP);
539 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
540 isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
541 // This is a second tap
542 mIsDoubleTapping = true;
543 // Give a callback with the first tap of the double-tap
544 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
545 // Give a callback with down event of the double-tap
546 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
548 // This is a first tap
549 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
553 mDownFocusX = mLastFocusX = focusX;
554 mDownFocusY = mLastFocusY = focusY;
555 if (mCurrentDownEvent != null) {
556 mCurrentDownEvent.recycle();
558 mCurrentDownEvent = MotionEvent.obtain(ev);
559 mAlwaysInTapRegion = true;
560 mAlwaysInBiggerTapRegion = true;
562 mInLongPress = false;
563 mDeferConfirmSingleTap = false;
565 if (mIsLongpressEnabled) {
566 mHandler.removeMessages(LONG_PRESS);
567 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
568 + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
570 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
571 handled |= mListener.onDown(ev);
574 case MotionEvent.ACTION_MOVE:
578 final float scrollX = mLastFocusX - focusX;
579 final float scrollY = mLastFocusY - focusY;
580 if (mIsDoubleTapping) {
581 // Give the move events of the double-tap
582 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
583 } else if (mAlwaysInTapRegion) {
584 final int deltaX = (int) (focusX - mDownFocusX);
585 final int deltaY = (int) (focusY - mDownFocusY);
586 int distance = (deltaX * deltaX) + (deltaY * deltaY);
587 if (distance > mTouchSlopSquare) {
588 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
589 mLastFocusX = focusX;
590 mLastFocusY = focusY;
591 mAlwaysInTapRegion = false;
592 mHandler.removeMessages(TAP);
593 mHandler.removeMessages(SHOW_PRESS);
594 mHandler.removeMessages(LONG_PRESS);
596 if (distance > mDoubleTapTouchSlopSquare) {
597 mAlwaysInBiggerTapRegion = false;
599 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
600 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
601 mLastFocusX = focusX;
602 mLastFocusY = focusY;
606 case MotionEvent.ACTION_UP:
608 MotionEvent currentUpEvent = MotionEvent.obtain(ev);
609 if (mIsDoubleTapping) {
610 // Finally, give the up event of the double-tap
611 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
612 } else if (mInLongPress) {
613 mHandler.removeMessages(TAP);
614 mInLongPress = false;
615 } else if (mAlwaysInTapRegion) {
616 handled = mListener.onSingleTapUp(ev);
617 if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
618 mDoubleTapListener.onSingleTapConfirmed(ev);
622 // A fling must travel the minimum tap distance
623 final VelocityTracker velocityTracker = mVelocityTracker;
624 final int pointerId = ev.getPointerId(0);
625 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
626 final float velocityY = velocityTracker.getYVelocity(pointerId);
627 final float velocityX = velocityTracker.getXVelocity(pointerId);
629 if ((Math.abs(velocityY) > mMinimumFlingVelocity)
630 || (Math.abs(velocityX) > mMinimumFlingVelocity)){
631 handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
634 if (mPreviousUpEvent != null) {
635 mPreviousUpEvent.recycle();
637 // Hold the event we obtained above - listeners may have changed the original.
638 mPreviousUpEvent = currentUpEvent;
639 if (mVelocityTracker != null) {
640 // This may have been cleared when we called out to the
641 // application above.
642 mVelocityTracker.recycle();
643 mVelocityTracker = null;
645 mIsDoubleTapping = false;
646 mDeferConfirmSingleTap = false;
647 mHandler.removeMessages(SHOW_PRESS);
648 mHandler.removeMessages(LONG_PRESS);
651 case MotionEvent.ACTION_CANCEL:
656 /* Commented out in Chromium for NDK compliance
657 if (!handled && mInputEventConsistencyVerifier != null) {
658 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
664 private void cancel() {
665 mHandler.removeMessages(SHOW_PRESS);
666 mHandler.removeMessages(LONG_PRESS);
667 mHandler.removeMessages(TAP);
668 mVelocityTracker.recycle();
669 mVelocityTracker = null;
670 mIsDoubleTapping = false;
672 mAlwaysInTapRegion = false;
673 mAlwaysInBiggerTapRegion = false;
674 mDeferConfirmSingleTap = false;
676 mInLongPress = false;
680 private void cancelTaps() {
681 mHandler.removeMessages(SHOW_PRESS);
682 mHandler.removeMessages(LONG_PRESS);
683 mHandler.removeMessages(TAP);
684 mIsDoubleTapping = false;
685 mAlwaysInTapRegion = false;
686 mAlwaysInBiggerTapRegion = false;
687 mDeferConfirmSingleTap = false;
689 mInLongPress = false;
693 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
694 MotionEvent secondDown) {
695 if (!mAlwaysInBiggerTapRegion) {
699 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
703 int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
704 int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
705 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
708 private void dispatchLongPress() {
709 mHandler.removeMessages(TAP);
710 mDeferConfirmSingleTap = false;
712 mListener.onLongPress(mCurrentDownEvent);