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.
109 * @return true if the event is consumed, else false
111 boolean onLongPress(MotionEvent e);
114 * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
115 * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
116 * the x and y axis in pixels per second.
118 * @param e1 The first down motion event that started the fling.
119 * @param e2 The move motion event that triggered the current onFling.
120 * @param velocityX The velocity of this fling measured in pixels per second
122 * @param velocityY The velocity of this fling measured in pixels per second
124 * @return true if the event is consumed, else false
126 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
130 * The listener that is used to notify when a double-tap or a confirmed
133 public interface OnDoubleTapListener {
135 * Notified when a single-tap occurs.
137 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
138 * will only be called after the detector is confident that the user's
139 * first tap is not followed by a second tap leading to a double-tap
142 * @param e The down motion event of the single-tap.
143 * @return true if the event is consumed, else false
145 boolean onSingleTapConfirmed(MotionEvent e);
148 * Notified when a double-tap occurs.
150 * @param e The down motion event of the first tap of the double-tap.
151 * @return true if the event is consumed, else false
153 boolean onDoubleTap(MotionEvent e);
156 * Notified when an event within a double-tap gesture occurs, including
157 * the down, move, and up events.
159 * @param e The motion event that occurred during the double-tap gesture.
160 * @return true if the event is consumed, else false
162 boolean onDoubleTapEvent(MotionEvent e);
166 * A convenience class to extend when you only want to listen for a subset
167 * of all the gestures. This implements all methods in the
168 * {@link OnGestureListener} and {@link OnDoubleTapListener} but does
169 * nothing and return {@code false} for all applicable methods.
171 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
172 public boolean onSingleTapUp(MotionEvent e) {
176 public boolean onLongPress(MotionEvent e) {
180 public boolean onScroll(MotionEvent e1, MotionEvent e2,
181 float distanceX, float distanceY) {
185 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
190 public void onShowPress(MotionEvent e) {
193 public boolean onDown(MotionEvent e) {
197 public boolean onDoubleTap(MotionEvent e) {
201 public boolean onDoubleTapEvent(MotionEvent e) {
205 public boolean onSingleTapConfirmed(MotionEvent e) {
210 private int mTouchSlopSquare;
211 private int mDoubleTapTouchSlopSquare;
212 private int mDoubleTapSlopSquare;
213 private int mMinimumFlingVelocity;
214 private int mMaximumFlingVelocity;
216 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
217 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
218 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
220 // constants for Message.what used by GestureHandler below
221 private static final int SHOW_PRESS = 1;
222 private static final int LONG_PRESS = 2;
223 private static final int TAP = 3;
225 private final Handler mHandler;
226 private final OnGestureListener mListener;
227 private OnDoubleTapListener mDoubleTapListener;
229 private boolean mStillDown;
230 private boolean mDeferConfirmSingleTap;
231 private boolean mInLongPress;
232 private boolean mAlwaysInTapRegion;
233 private boolean mAlwaysInBiggerTapRegion;
235 private MotionEvent mCurrentDownEvent;
236 private MotionEvent mPreviousUpEvent;
239 * True when the user is still touching for the second tap (down, move, and
240 * up events). Can only be true if there is a double tap listener attached.
242 private boolean mIsDoubleTapping;
244 private float mLastFocusX;
245 private float mLastFocusY;
246 private float mDownFocusX;
247 private float mDownFocusY;
249 private boolean mIsLongpressEnabled;
252 * Determines speed during touch scrolling
254 private VelocityTracker mVelocityTracker;
257 * Consistency verifier for debugging purposes.
259 /* Commented out in Chromium for NDK compliance
260 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
261 InputEventConsistencyVerifier.isInstrumentationEnabled() ?
262 new InputEventConsistencyVerifier(this, 0) : null;
265 private class GestureHandler extends Handler {
270 GestureHandler(Handler handler) {
271 super(handler.getLooper());
275 public void handleMessage(Message msg) {
278 mListener.onShowPress(mCurrentDownEvent);
286 // If the user's finger is still down, do not count it as a tap
287 if (mDoubleTapListener != null) {
289 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
291 mDeferConfirmSingleTap = true;
297 throw new RuntimeException("Unknown message " + msg); //never
303 * Creates a GestureDetector with the supplied listener.
304 * This variant of the constructor should be used from a non-UI thread
305 * (as it allows specifying the Handler).
307 * @param listener the listener invoked for all the callbacks, this must
309 * @param handler the handler to use
311 * @throws NullPointerException if either {@code listener} or
312 * {@code handler} is null.
314 * @deprecated Use {@link #GestureDetector(android.content.Context,
315 * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
318 public GestureDetector(OnGestureListener listener, Handler handler) {
319 this(null, listener, handler);
323 * Creates a GestureDetector with the supplied listener.
324 * You may only use this constructor from a UI thread (this is the usual situation).
325 * @see android.os.Handler#Handler()
327 * @param listener the listener invoked for all the callbacks, this must
330 * @throws NullPointerException if {@code listener} is null.
332 * @deprecated Use {@link #GestureDetector(android.content.Context,
333 * android.view.GestureDetector.OnGestureListener)} instead.
336 public GestureDetector(OnGestureListener listener) {
337 this(null, listener, null);
341 * Creates a GestureDetector with the supplied listener.
342 * You may only use this constructor from a UI thread (this is the usual situation).
343 * @see android.os.Handler#Handler()
345 * @param context the application's context
346 * @param listener the listener invoked for all the callbacks, this must
349 * @throws NullPointerException if {@code listener} is null.
351 public GestureDetector(Context context, OnGestureListener listener) {
352 this(context, listener, null);
356 * Creates a GestureDetector with the supplied listener.
357 * You may only use this constructor from a UI thread (this is the usual situation).
358 * @see android.os.Handler#Handler()
360 * @param context the application's context
361 * @param listener the listener invoked for all the callbacks, this must
363 * @param handler the handler to use
365 * @throws NullPointerException if {@code listener} is null.
367 public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
368 if (handler != null) {
369 mHandler = new GestureHandler(handler);
371 mHandler = new GestureHandler();
373 mListener = listener;
374 if (listener instanceof OnDoubleTapListener) {
375 setOnDoubleTapListener((OnDoubleTapListener) listener);
381 * Creates a GestureDetector with the supplied listener.
382 * You may only use this constructor from a UI thread (this is the usual situation).
383 * @see android.os.Handler#Handler()
385 * @param context the application's context
386 * @param listener the listener invoked for all the callbacks, this must
388 * @param handler the handler to use
390 * @throws NullPointerException if {@code listener} is null.
392 public GestureDetector(Context context, OnGestureListener listener, Handler handler,
394 this(context, listener, handler);
397 private void init(Context context) {
398 if (mListener == null) {
399 throw new NullPointerException("OnGestureListener must not be null");
401 mIsLongpressEnabled = true;
403 // Fallback to support pre-donuts releases
404 int touchSlop, doubleTapSlop, doubleTapTouchSlop;
405 /* Commented out in Chromium for NDK compliance
406 if (context == null) {
407 //noinspection deprecation
408 touchSlop = ViewConfiguration.getTouchSlop();
409 doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
410 doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
411 //noinspection deprecation
412 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
413 mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
415 final ViewConfiguration configuration = ViewConfiguration.get(context);
416 touchSlop = configuration.getScaledTouchSlop();
417 /* Commented out in Chromium for NDK compliance and replaced with the following line. Note that
418 * ViewConfiguration.TOUCH_SLOP has the same value as DOUBLE_TAP_TOUCH_SLOP in current Android, so
419 * this doesn't introduce a behavior difference in Android versions <= 4.2.
420 doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
422 doubleTapTouchSlop = touchSlop;
423 doubleTapSlop = configuration.getScaledDoubleTapSlop();
424 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
425 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
427 mTouchSlopSquare = touchSlop * touchSlop;
428 mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
429 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
433 * Sets the listener which will be called for double-tap and related
436 * @param onDoubleTapListener the listener invoked for all the callbacks, or
437 * null to stop listening for double-tap gestures.
439 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
440 mDoubleTapListener = onDoubleTapListener;
444 * Set whether longpress is enabled, if this is enabled when a user
445 * presses and holds down you get a longpress event and nothing further.
446 * If it's disabled the user can press and hold down and then later
447 * moved their finger and you will get scroll events. By default
448 * longpress is enabled.
450 * @param isLongpressEnabled whether longpress should be enabled.
452 public void setIsLongpressEnabled(boolean isLongpressEnabled) {
453 mIsLongpressEnabled = isLongpressEnabled;
457 * @return true if longpress is enabled, else false.
459 public boolean isLongpressEnabled() {
460 return mIsLongpressEnabled;
464 * Analyzes the given motion event and if applicable triggers the
465 * appropriate callbacks on the {@link OnGestureListener} supplied.
467 * @param ev The current motion event.
468 * @return true if the {@link OnGestureListener} consumed the event,
471 public boolean onTouchEvent(MotionEvent ev) {
472 /* Commented out in Chromium for NDK compliance
473 if (mInputEventConsistencyVerifier != null) {
474 mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
478 final int action = ev.getAction();
480 if (mVelocityTracker == null) {
481 mVelocityTracker = VelocityTracker.obtain();
483 mVelocityTracker.addMovement(ev);
485 final boolean pointerUp =
486 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
487 final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
489 // Determine focal point
490 float sumX = 0, sumY = 0;
491 final int count = ev.getPointerCount();
492 for (int i = 0; i < count; i++) {
493 if (skipIndex == i) continue;
497 final int div = pointerUp ? count - 1 : count;
498 final float focusX = sumX / div;
499 final float focusY = sumY / div;
501 boolean handled = false;
503 switch (action & MotionEvent.ACTION_MASK) {
504 case MotionEvent.ACTION_POINTER_DOWN:
505 mDownFocusX = mLastFocusX = focusX;
506 mDownFocusY = mLastFocusY = focusY;
507 // Cancel long press and taps
511 case MotionEvent.ACTION_POINTER_UP:
512 mDownFocusX = mLastFocusX = focusX;
513 mDownFocusY = mLastFocusY = focusY;
515 // Check the dot product of current velocities.
516 // If the pointer that left was opposing another velocity vector, clear.
517 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
518 final int upIndex = ev.getActionIndex();
519 final int id1 = ev.getPointerId(upIndex);
520 final float x1 = mVelocityTracker.getXVelocity(id1);
521 final float y1 = mVelocityTracker.getYVelocity(id1);
522 for (int i = 0; i < count; i++) {
523 if (i == upIndex) continue;
525 final int id2 = ev.getPointerId(i);
526 final float x = x1 * mVelocityTracker.getXVelocity(id2);
527 final float y = y1 * mVelocityTracker.getYVelocity(id2);
529 final float dot = x + y;
531 mVelocityTracker.clear();
537 case MotionEvent.ACTION_DOWN:
538 if (mDoubleTapListener != null) {
539 boolean hadTapMessage = mHandler.hasMessages(TAP);
540 if (hadTapMessage) mHandler.removeMessages(TAP);
541 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
542 isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
543 // This is a second tap
544 mIsDoubleTapping = true;
545 // Give a callback with the first tap of the double-tap
546 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
547 // Give a callback with down event of the double-tap
548 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
550 // This is a first tap
551 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
555 mDownFocusX = mLastFocusX = focusX;
556 mDownFocusY = mLastFocusY = focusY;
557 if (mCurrentDownEvent != null) {
558 mCurrentDownEvent.recycle();
560 mCurrentDownEvent = MotionEvent.obtain(ev);
561 mAlwaysInTapRegion = true;
562 mAlwaysInBiggerTapRegion = true;
564 mInLongPress = false;
565 mDeferConfirmSingleTap = false;
567 if (mIsLongpressEnabled) {
568 mHandler.removeMessages(LONG_PRESS);
569 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
570 + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
572 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
573 handled |= mListener.onDown(ev);
576 case MotionEvent.ACTION_MOVE:
580 final float scrollX = mLastFocusX - focusX;
581 final float scrollY = mLastFocusY - focusY;
582 if (mIsDoubleTapping) {
583 // Give the move events of the double-tap
584 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
585 } else if (mAlwaysInTapRegion) {
586 final int deltaX = (int) (focusX - mDownFocusX);
587 final int deltaY = (int) (focusY - mDownFocusY);
588 int distance = (deltaX * deltaX) + (deltaY * deltaY);
589 if (distance > mTouchSlopSquare) {
590 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
591 mLastFocusX = focusX;
592 mLastFocusY = focusY;
593 mAlwaysInTapRegion = false;
594 mHandler.removeMessages(TAP);
595 mHandler.removeMessages(SHOW_PRESS);
596 mHandler.removeMessages(LONG_PRESS);
598 if (distance > mDoubleTapTouchSlopSquare) {
599 mAlwaysInBiggerTapRegion = false;
601 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
602 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
603 mLastFocusX = focusX;
604 mLastFocusY = focusY;
608 case MotionEvent.ACTION_UP:
610 MotionEvent currentUpEvent = MotionEvent.obtain(ev);
611 if (mIsDoubleTapping) {
612 // Finally, give the up event of the double-tap
613 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
614 } else if (mInLongPress) {
615 mHandler.removeMessages(TAP);
616 mInLongPress = false;
617 } else if (mAlwaysInTapRegion) {
618 handled = mListener.onSingleTapUp(ev);
619 if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
620 mDoubleTapListener.onSingleTapConfirmed(ev);
624 // A fling must travel the minimum tap distance
625 final VelocityTracker velocityTracker = mVelocityTracker;
626 final int pointerId = ev.getPointerId(0);
627 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
628 final float velocityY = velocityTracker.getYVelocity(pointerId);
629 final float velocityX = velocityTracker.getXVelocity(pointerId);
631 if ((Math.abs(velocityY) > mMinimumFlingVelocity)
632 || (Math.abs(velocityX) > mMinimumFlingVelocity)){
633 handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
636 if (mPreviousUpEvent != null) {
637 mPreviousUpEvent.recycle();
639 // Hold the event we obtained above - listeners may have changed the original.
640 mPreviousUpEvent = currentUpEvent;
641 if (mVelocityTracker != null) {
642 // This may have been cleared when we called out to the
643 // application above.
644 mVelocityTracker.recycle();
645 mVelocityTracker = null;
647 mIsDoubleTapping = false;
648 mDeferConfirmSingleTap = false;
649 mHandler.removeMessages(SHOW_PRESS);
650 mHandler.removeMessages(LONG_PRESS);
653 case MotionEvent.ACTION_CANCEL:
658 /* Commented out in Chromium for NDK compliance
659 if (!handled && mInputEventConsistencyVerifier != null) {
660 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
666 private void cancel() {
667 mHandler.removeMessages(SHOW_PRESS);
668 mHandler.removeMessages(LONG_PRESS);
669 mHandler.removeMessages(TAP);
670 mVelocityTracker.recycle();
671 mVelocityTracker = null;
672 mIsDoubleTapping = false;
674 mAlwaysInTapRegion = false;
675 mAlwaysInBiggerTapRegion = false;
676 mDeferConfirmSingleTap = false;
678 mInLongPress = false;
682 private void cancelTaps() {
683 mHandler.removeMessages(SHOW_PRESS);
684 mHandler.removeMessages(LONG_PRESS);
685 mHandler.removeMessages(TAP);
686 mIsDoubleTapping = false;
687 mAlwaysInTapRegion = false;
688 mAlwaysInBiggerTapRegion = false;
689 mDeferConfirmSingleTap = false;
691 mInLongPress = false;
695 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
696 MotionEvent secondDown) {
697 if (!mAlwaysInBiggerTapRegion) {
701 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
705 int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
706 int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
707 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
710 private void dispatchLongPress() {
711 mHandler.removeMessages(TAP);
712 mDeferConfirmSingleTap = false;
713 /* Changed in Chromium to allow scrolling after a longpress.
715 mListener.onLongPress(mCurrentDownEvent);
717 mInLongPress = mListener.onLongPress(mCurrentDownEvent);