blob: c794a69d36808ff22868629b83706e0c4b89ffac [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package android.view;
18
Mathew Inwooda570dee2018-08-17 14:56:00 +010019import android.annotation.UnsupportedAppUsage;
Adam Powell216bccf2010-02-01 15:03:17 -080020import android.content.Context;
Jackal Guo198a2622018-11-19 11:51:31 +080021import android.os.Build;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.os.Handler;
23import android.os.Message;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024
25/**
26 * Detects various gestures and events using the supplied {@link MotionEvent}s.
27 * The {@link OnGestureListener} callback will notify users when a particular
28 * motion event has occurred. This class should only be used with {@link MotionEvent}s
29 * reported via touch (don't use for trackball events).
30 *
31 * To use this class:
32 * <ul>
33 * <li>Create an instance of the {@code GestureDetector} for your {@link View}
34 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
35 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
36 * will be executed when the events occur.
Mady Mellor015020e2015-06-05 14:41:22 -070037 * <li>If listening for {@link OnContextClickListener#onContextClick(MotionEvent)}
Mady Mellorb0933442015-05-27 14:15:57 -070038 * you must call {@link #onGenericMotionEvent(MotionEvent)}
39 * in {@link View#onGenericMotionEvent(MotionEvent)}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040 * </ul>
41 */
42public class GestureDetector {
43 /**
44 * The listener that is used to notify when gestures occur.
45 * If you want to listen for all the different gestures then implement
46 * this interface. If you only want to listen for a subset it might
47 * be easier to extend {@link SimpleOnGestureListener}.
48 */
49 public interface OnGestureListener {
50
51 /**
52 * Notified when a tap occurs with the down {@link MotionEvent}
53 * that triggered it. This will be triggered immediately for
54 * every down event. All other events should be preceded by this.
55 *
56 * @param e The down motion event.
57 */
58 boolean onDown(MotionEvent e);
59
60 /**
61 * The user has performed a down {@link MotionEvent} and not performed
62 * a move or up yet. This event is commonly used to provide visual
63 * feedback to the user to let them know that their action has been
64 * recognized i.e. highlight an element.
65 *
66 * @param e The down motion event
67 */
68 void onShowPress(MotionEvent e);
69
70 /**
71 * Notified when a tap occurs with the up {@link MotionEvent}
72 * that triggered it.
73 *
74 * @param e The up motion event that completed the first tap
75 * @return true if the event is consumed, else false
76 */
77 boolean onSingleTapUp(MotionEvent e);
78
79 /**
80 * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
81 * current move {@link MotionEvent}. The distance in x and y is also supplied for
82 * convenience.
83 *
84 * @param e1 The first down motion event that started the scrolling.
85 * @param e2 The move motion event that triggered the current onScroll.
86 * @param distanceX The distance along the X axis that has been scrolled since the last
87 * call to onScroll. This is NOT the distance between {@code e1}
88 * and {@code e2}.
89 * @param distanceY The distance along the Y axis that has been scrolled since the last
90 * call to onScroll. This is NOT the distance between {@code e1}
91 * and {@code e2}.
92 * @return true if the event is consumed, else false
93 */
94 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
95
96 /**
97 * Notified when a long press occurs with the initial on down {@link MotionEvent}
98 * that trigged it.
99 *
100 * @param e The initial on down motion event that started the longpress.
101 */
102 void onLongPress(MotionEvent e);
103
104 /**
105 * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
106 * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
107 * the x and y axis in pixels per second.
108 *
109 * @param e1 The first down motion event that started the fling.
110 * @param e2 The move motion event that triggered the current onFling.
111 * @param velocityX The velocity of this fling measured in pixels per second
112 * along the x axis.
113 * @param velocityY The velocity of this fling measured in pixels per second
114 * along the y axis.
115 * @return true if the event is consumed, else false
116 */
117 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
118 }
119
120 /**
121 * The listener that is used to notify when a double-tap or a confirmed
122 * single-tap occur.
123 */
124 public interface OnDoubleTapListener {
125 /**
126 * Notified when a single-tap occurs.
127 * <p>
128 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
129 * will only be called after the detector is confident that the user's
130 * first tap is not followed by a second tap leading to a double-tap
131 * gesture.
132 *
133 * @param e The down motion event of the single-tap.
134 * @return true if the event is consumed, else false
135 */
136 boolean onSingleTapConfirmed(MotionEvent e);
137
138 /**
139 * Notified when a double-tap occurs.
140 *
141 * @param e The down motion event of the first tap of the double-tap.
142 * @return true if the event is consumed, else false
143 */
144 boolean onDoubleTap(MotionEvent e);
145
146 /**
147 * Notified when an event within a double-tap gesture occurs, including
148 * the down, move, and up events.
149 *
150 * @param e The motion event that occurred during the double-tap gesture.
151 * @return true if the event is consumed, else false
152 */
153 boolean onDoubleTapEvent(MotionEvent e);
154 }
155
156 /**
Mady Mellor015020e2015-06-05 14:41:22 -0700157 * The listener that is used to notify when a context click occurs. When listening for a
158 * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
Mady Mellorb0933442015-05-27 14:15:57 -0700159 * {@link View#onGenericMotionEvent(MotionEvent)}.
Mady Mellora6b16452015-04-14 18:03:34 -0700160 */
Mady Mellor015020e2015-06-05 14:41:22 -0700161 public interface OnContextClickListener {
Mady Mellora6b16452015-04-14 18:03:34 -0700162 /**
Mady Mellor015020e2015-06-05 14:41:22 -0700163 * Notified when a context click occurs.
Mady Mellora6b16452015-04-14 18:03:34 -0700164 *
Mady Mellor015020e2015-06-05 14:41:22 -0700165 * @param e The motion event that occurred during the context click.
Mady Mellora6b16452015-04-14 18:03:34 -0700166 * @return true if the event is consumed, else false
167 */
Mady Mellor015020e2015-06-05 14:41:22 -0700168 boolean onContextClick(MotionEvent e);
Mady Mellora6b16452015-04-14 18:03:34 -0700169 }
170
171 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800172 * A convenience class to extend when you only want to listen for a subset
173 * of all the gestures. This implements all methods in the
Mady Mellor015020e2015-06-05 14:41:22 -0700174 * {@link OnGestureListener}, {@link OnDoubleTapListener}, and {@link OnContextClickListener}
Mady Mellora6b16452015-04-14 18:03:34 -0700175 * but does nothing and return {@code false} for all applicable methods.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800176 */
Mady Mellora6b16452015-04-14 18:03:34 -0700177 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
Mady Mellor015020e2015-06-05 14:41:22 -0700178 OnContextClickListener {
Mady Mellora6b16452015-04-14 18:03:34 -0700179
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800180 public boolean onSingleTapUp(MotionEvent e) {
181 return false;
182 }
183
184 public void onLongPress(MotionEvent e) {
185 }
186
187 public boolean onScroll(MotionEvent e1, MotionEvent e2,
188 float distanceX, float distanceY) {
189 return false;
190 }
191
192 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
193 float velocityY) {
194 return false;
195 }
196
197 public void onShowPress(MotionEvent e) {
198 }
199
200 public boolean onDown(MotionEvent e) {
201 return false;
202 }
203
204 public boolean onDoubleTap(MotionEvent e) {
205 return false;
206 }
207
208 public boolean onDoubleTapEvent(MotionEvent e) {
209 return false;
210 }
211
212 public boolean onSingleTapConfirmed(MotionEvent e) {
213 return false;
214 }
Mady Mellora6b16452015-04-14 18:03:34 -0700215
Mady Mellor015020e2015-06-05 14:41:22 -0700216 public boolean onContextClick(MotionEvent e) {
Mady Mellora6b16452015-04-14 18:03:34 -0700217 return false;
218 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800219 }
220
Mathew Inwooda570dee2018-08-17 14:56:00 +0100221 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800222 private int mTouchSlopSquare;
Gilles Debunne006fa482011-10-27 17:23:36 -0700223 private int mDoubleTapTouchSlopSquare;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800224 private int mDoubleTapSlopSquare;
Mathew Inwooda570dee2018-08-17 14:56:00 +0100225 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 private int mMinimumFlingVelocity;
Romain Guy4296fc42009-07-06 11:48:52 -0700227 private int mMaximumFlingVelocity;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800228
Jackal Guo198a2622018-11-19 11:51:31 +0800229 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800230 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
231 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800232 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
Adam Powellaf1785f2013-09-05 13:44:45 -0700233 private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234
235 // constants for Message.what used by GestureHandler below
236 private static final int SHOW_PRESS = 1;
237 private static final int LONG_PRESS = 2;
238 private static final int TAP = 3;
239
240 private final Handler mHandler;
Mathew Inwooda570dee2018-08-17 14:56:00 +0100241 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800242 private final OnGestureListener mListener;
243 private OnDoubleTapListener mDoubleTapListener;
Mady Mellor015020e2015-06-05 14:41:22 -0700244 private OnContextClickListener mContextClickListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800245
246 private boolean mStillDown;
Adam Powelleca3e602013-02-15 11:26:45 -0800247 private boolean mDeferConfirmSingleTap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800248 private boolean mInLongPress;
Mady Mellor015020e2015-06-05 14:41:22 -0700249 private boolean mInContextClick;
Mathew Inwooda570dee2018-08-17 14:56:00 +0100250 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800251 private boolean mAlwaysInTapRegion;
252 private boolean mAlwaysInBiggerTapRegion;
Mady Mellorb0933442015-05-27 14:15:57 -0700253 private boolean mIgnoreNextUpEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800254
255 private MotionEvent mCurrentDownEvent;
256 private MotionEvent mPreviousUpEvent;
257
258 /**
259 * True when the user is still touching for the second tap (down, move, and
260 * up events). Can only be true if there is a double tap listener attached.
261 */
262 private boolean mIsDoubleTapping;
263
Adam Powell05a1e1f2012-08-29 13:54:44 -0700264 private float mLastFocusX;
265 private float mLastFocusY;
266 private float mDownFocusX;
267 private float mDownFocusY;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800268
269 private boolean mIsLongpressEnabled;
270
271 /**
272 * Determines speed during touch scrolling
273 */
274 private VelocityTracker mVelocityTracker;
275
Jeff Brown21bc5c92011-02-28 18:27:14 -0800276 /**
277 * Consistency verifier for debugging purposes.
278 */
279 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
280 InputEventConsistencyVerifier.isInstrumentationEnabled() ?
281 new InputEventConsistencyVerifier(this, 0) : null;
282
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800283 private class GestureHandler extends Handler {
284 GestureHandler() {
285 super();
286 }
287
288 GestureHandler(Handler handler) {
289 super(handler.getLooper());
290 }
291
292 @Override
293 public void handleMessage(Message msg) {
294 switch (msg.what) {
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800295 case SHOW_PRESS:
296 mListener.onShowPress(mCurrentDownEvent);
297 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800298
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800299 case LONG_PRESS:
300 dispatchLongPress();
301 break;
302
303 case TAP:
304 // If the user's finger is still down, do not count it as a tap
305 if (mDoubleTapListener != null) {
306 if (!mStillDown) {
307 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
308 } else {
309 mDeferConfirmSingleTap = true;
310 }
311 }
312 break;
313
314 default:
315 throw new RuntimeException("Unknown message " + msg); //never
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800316 }
317 }
318 }
319
320 /**
321 * Creates a GestureDetector with the supplied listener.
322 * This variant of the constructor should be used from a non-UI thread
323 * (as it allows specifying the Handler).
324 *
325 * @param listener the listener invoked for all the callbacks, this must
326 * not be null.
327 * @param handler the handler to use
328 *
329 * @throws NullPointerException if either {@code listener} or
330 * {@code handler} is null.
331 *
332 * @deprecated Use {@link #GestureDetector(android.content.Context,
333 * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
334 */
335 @Deprecated
336 public GestureDetector(OnGestureListener listener, Handler handler) {
337 this(null, listener, handler);
338 }
339
340 /**
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()
344 *
345 * @param listener the listener invoked for all the callbacks, this must
346 * not be null.
347 *
348 * @throws NullPointerException if {@code listener} is null.
349 *
350 * @deprecated Use {@link #GestureDetector(android.content.Context,
351 * android.view.GestureDetector.OnGestureListener)} instead.
352 */
353 @Deprecated
354 public GestureDetector(OnGestureListener listener) {
355 this(null, listener, null);
356 }
357
358 /**
359 * Creates a GestureDetector with the supplied listener.
Scott Mainc27ea252013-08-30 13:56:05 -0700360 * You may only use this constructor from a {@link android.os.Looper} thread.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800361 * @see android.os.Handler#Handler()
362 *
363 * @param context the application's context
364 * @param listener the listener invoked for all the callbacks, this must
365 * not be null.
366 *
367 * @throws NullPointerException if {@code listener} is null.
368 */
369 public GestureDetector(Context context, OnGestureListener listener) {
370 this(context, listener, null);
371 }
372
373 /**
Scott Mainc27ea252013-08-30 13:56:05 -0700374 * Creates a GestureDetector with the supplied listener that runs deferred events on the
375 * thread associated with the supplied {@link android.os.Handler}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800376 * @see android.os.Handler#Handler()
377 *
378 * @param context the application's context
379 * @param listener the listener invoked for all the callbacks, this must
380 * not be null.
Scott Mainc27ea252013-08-30 13:56:05 -0700381 * @param handler the handler to use for running deferred listener events.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800382 *
383 * @throws NullPointerException if {@code listener} is null.
384 */
385 public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
Adam Powell05a1e1f2012-08-29 13:54:44 -0700386 if (handler != null) {
387 mHandler = new GestureHandler(handler);
388 } else {
389 mHandler = new GestureHandler();
390 }
391 mListener = listener;
392 if (listener instanceof OnDoubleTapListener) {
393 setOnDoubleTapListener((OnDoubleTapListener) listener);
394 }
Mady Mellor015020e2015-06-05 14:41:22 -0700395 if (listener instanceof OnContextClickListener) {
396 setContextClickListener((OnContextClickListener) listener);
Mady Mellora6b16452015-04-14 18:03:34 -0700397 }
Adam Powell05a1e1f2012-08-29 13:54:44 -0700398 init(context);
Adam Powell216bccf2010-02-01 15:03:17 -0800399 }
400
401 /**
Scott Mainc27ea252013-08-30 13:56:05 -0700402 * Creates a GestureDetector with the supplied listener that runs deferred events on the
403 * thread associated with the supplied {@link android.os.Handler}.
Adam Powell216bccf2010-02-01 15:03:17 -0800404 * @see android.os.Handler#Handler()
405 *
406 * @param context the application's context
407 * @param listener the listener invoked for all the callbacks, this must
408 * not be null.
Scott Mainc27ea252013-08-30 13:56:05 -0700409 * @param handler the handler to use for running deferred listener events.
410 * @param unused currently not used.
Adam Powell216bccf2010-02-01 15:03:17 -0800411 *
412 * @throws NullPointerException if {@code listener} is null.
413 */
414 public GestureDetector(Context context, OnGestureListener listener, Handler handler,
Adam Powell05a1e1f2012-08-29 13:54:44 -0700415 boolean unused) {
416 this(context, listener, handler);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800417 }
418
Adam Powell05a1e1f2012-08-29 13:54:44 -0700419 private void init(Context context) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800420 if (mListener == null) {
421 throw new NullPointerException("OnGestureListener must not be null");
422 }
423 mIsLongpressEnabled = true;
424
425 // Fallback to support pre-donuts releases
Gilles Debunne006fa482011-10-27 17:23:36 -0700426 int touchSlop, doubleTapSlop, doubleTapTouchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800427 if (context == null) {
428 //noinspection deprecation
429 touchSlop = ViewConfiguration.getTouchSlop();
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800430 doubleTapTouchSlop = touchSlop; // Hack rather than adding a hidden method for this
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800431 doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
432 //noinspection deprecation
433 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
Romain Guy4296fc42009-07-06 11:48:52 -0700434 mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800435 } else {
436 final ViewConfiguration configuration = ViewConfiguration.get(context);
437 touchSlop = configuration.getScaledTouchSlop();
Gilles Debunne006fa482011-10-27 17:23:36 -0700438 doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800439 doubleTapSlop = configuration.getScaledDoubleTapSlop();
440 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
Romain Guy4296fc42009-07-06 11:48:52 -0700441 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800442 }
443 mTouchSlopSquare = touchSlop * touchSlop;
Gilles Debunne006fa482011-10-27 17:23:36 -0700444 mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800445 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
446 }
447
448 /**
449 * Sets the listener which will be called for double-tap and related
450 * gestures.
451 *
452 * @param onDoubleTapListener the listener invoked for all the callbacks, or
453 * null to stop listening for double-tap gestures.
454 */
455 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
456 mDoubleTapListener = onDoubleTapListener;
457 }
458
459 /**
Mady Mellor015020e2015-06-05 14:41:22 -0700460 * Sets the listener which will be called for context clicks.
Mady Mellora6b16452015-04-14 18:03:34 -0700461 *
Mady Mellor015020e2015-06-05 14:41:22 -0700462 * @param onContextClickListener the listener invoked for all the callbacks, or null to stop
463 * listening for context clicks.
Mady Mellora6b16452015-04-14 18:03:34 -0700464 */
Mady Mellor015020e2015-06-05 14:41:22 -0700465 public void setContextClickListener(OnContextClickListener onContextClickListener) {
466 mContextClickListener = onContextClickListener;
Mady Mellora6b16452015-04-14 18:03:34 -0700467 }
468
469 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800470 * Set whether longpress is enabled, if this is enabled when a user
471 * presses and holds down you get a longpress event and nothing further.
472 * If it's disabled the user can press and hold down and then later
473 * moved their finger and you will get scroll events. By default
474 * longpress is enabled.
475 *
476 * @param isLongpressEnabled whether longpress should be enabled.
477 */
478 public void setIsLongpressEnabled(boolean isLongpressEnabled) {
479 mIsLongpressEnabled = isLongpressEnabled;
480 }
481
482 /**
483 * @return true if longpress is enabled, else false.
484 */
485 public boolean isLongpressEnabled() {
486 return mIsLongpressEnabled;
487 }
488
489 /**
490 * Analyzes the given motion event and if applicable triggers the
491 * appropriate callbacks on the {@link OnGestureListener} supplied.
492 *
493 * @param ev The current motion event.
494 * @return true if the {@link OnGestureListener} consumed the event,
495 * else false.
496 */
497 public boolean onTouchEvent(MotionEvent ev) {
Jeff Brown21bc5c92011-02-28 18:27:14 -0800498 if (mInputEventConsistencyVerifier != null) {
499 mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
500 }
501
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800502 final int action = ev.getAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503
504 if (mVelocityTracker == null) {
505 mVelocityTracker = VelocityTracker.obtain();
506 }
507 mVelocityTracker.addMovement(ev);
508
Adam Powellf90165a2012-08-31 11:11:39 -0700509 final boolean pointerUp =
510 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
Adam Powell05a1e1f2012-08-29 13:54:44 -0700511 final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
Dennis Kempinac1b31d2016-11-02 17:02:25 -0700512 final boolean isGeneratedGesture =
513 (ev.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
Adam Powell05a1e1f2012-08-29 13:54:44 -0700514
515 // Determine focal point
516 float sumX = 0, sumY = 0;
517 final int count = ev.getPointerCount();
518 for (int i = 0; i < count; i++) {
519 if (skipIndex == i) continue;
520 sumX += ev.getX(i);
521 sumY += ev.getY(i);
522 }
523 final int div = pointerUp ? count - 1 : count;
524 final float focusX = sumX / div;
525 final float focusY = sumY / div;
526
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800527 boolean handled = false;
528
Adam Powell216bccf2010-02-01 15:03:17 -0800529 switch (action & MotionEvent.ACTION_MASK) {
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800530 case MotionEvent.ACTION_POINTER_DOWN:
531 mDownFocusX = mLastFocusX = focusX;
532 mDownFocusY = mLastFocusY = focusY;
533 // Cancel long press and taps
534 cancelTaps();
535 break;
Adam Powell216bccf2010-02-01 15:03:17 -0800536
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800537 case MotionEvent.ACTION_POINTER_UP:
538 mDownFocusX = mLastFocusX = focusX;
539 mDownFocusY = mLastFocusY = focusY;
Adam Powellcd663592012-09-11 09:52:37 -0700540
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800541 // Check the dot product of current velocities.
542 // If the pointer that left was opposing another velocity vector, clear.
543 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
544 final int upIndex = ev.getActionIndex();
545 final int id1 = ev.getPointerId(upIndex);
546 final float x1 = mVelocityTracker.getXVelocity(id1);
547 final float y1 = mVelocityTracker.getYVelocity(id1);
548 for (int i = 0; i < count; i++) {
549 if (i == upIndex) continue;
Adam Powellcd663592012-09-11 09:52:37 -0700550
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800551 final int id2 = ev.getPointerId(i);
552 final float x = x1 * mVelocityTracker.getXVelocity(id2);
553 final float y = y1 * mVelocityTracker.getYVelocity(id2);
Adam Powellcd663592012-09-11 09:52:37 -0700554
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800555 final float dot = x + y;
556 if (dot < 0) {
557 mVelocityTracker.clear();
558 break;
559 }
560 }
561 break;
562
563 case MotionEvent.ACTION_DOWN:
564 if (mDoubleTapListener != null) {
565 boolean hadTapMessage = mHandler.hasMessages(TAP);
566 if (hadTapMessage) mHandler.removeMessages(TAP);
567 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
568 && hadTapMessage
569 && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
570 // This is a second tap
571 mIsDoubleTapping = true;
572 // Give a callback with the first tap of the double-tap
573 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
574 // Give a callback with down event of the double-tap
575 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
576 } else {
577 // This is a first tap
578 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
579 }
580 }
581
582 mDownFocusX = mLastFocusX = focusX;
583 mDownFocusY = mLastFocusY = focusY;
584 if (mCurrentDownEvent != null) {
585 mCurrentDownEvent.recycle();
586 }
587 mCurrentDownEvent = MotionEvent.obtain(ev);
588 mAlwaysInTapRegion = true;
589 mAlwaysInBiggerTapRegion = true;
590 mStillDown = true;
591 mInLongPress = false;
592 mDeferConfirmSingleTap = false;
593
594 if (mIsLongpressEnabled) {
595 mHandler.removeMessages(LONG_PRESS);
Jackal Guo198a2622018-11-19 11:51:31 +0800596 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
597 + ViewConfiguration.getLongPressTimeout());
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800598 }
599 mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
600 mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
601 handled |= mListener.onDown(ev);
602 break;
603
604 case MotionEvent.ACTION_MOVE:
605 if (mInLongPress || mInContextClick) {
Adam Powellcd663592012-09-11 09:52:37 -0700606 break;
607 }
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800608
609 final int motionClassification = ev.getClassification();
610 final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS);
611
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800612 final float scrollX = mLastFocusX - focusX;
613 final float scrollY = mLastFocusY - focusY;
614 if (mIsDoubleTapping) {
615 // Give the move events of the double-tap
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800616 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800617 } else if (mAlwaysInTapRegion) {
618 final int deltaX = (int) (focusX - mDownFocusX);
619 final int deltaY = (int) (focusY - mDownFocusY);
620 int distance = (deltaX * deltaX) + (deltaY * deltaY);
621 int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800622
623 final boolean ambiguousGesture =
624 motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
625 final boolean shouldInhibitDefaultAction =
626 hasPendingLongPress && ambiguousGesture;
627 if (shouldInhibitDefaultAction) {
628 // Inhibit default long press
Siarhei Vishniakou8b047dd2019-01-24 16:04:13 -0800629 final float multiplier = ViewConfiguration.getAmbiguousGestureMultiplier();
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800630 if (distance > slopSquare) {
631 // The default action here is to remove long press. But if the touch
632 // slop below gets increased, and we never exceed the modified touch
633 // slop while still receiving AMBIGUOUS_GESTURE, we risk that *nothing*
634 // will happen in response to user input. To prevent this,
635 // reschedule long press with a modified timeout.
636 mHandler.removeMessages(LONG_PRESS);
Siarhei Vishniakou8b047dd2019-01-24 16:04:13 -0800637 final long longPressTimeout = ViewConfiguration.getLongPressTimeout();
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800638 mHandler.sendEmptyMessageAtTime(LONG_PRESS, ev.getDownTime()
Siarhei Vishniakou8b047dd2019-01-24 16:04:13 -0800639 + (long) (longPressTimeout * multiplier));
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800640 }
641 // Inhibit default scroll. If a gesture is ambiguous, we prevent scroll
642 // until the gesture is resolved.
643 // However, for safety, simply increase the touch slop in case the
644 // classification is erroneous. Since the value is squared, multiply twice.
Siarhei Vishniakou8b047dd2019-01-24 16:04:13 -0800645 slopSquare *= multiplier * multiplier;
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800646 }
647
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800648 if (distance > slopSquare) {
649 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
650 mLastFocusX = focusX;
651 mLastFocusY = focusY;
652 mAlwaysInTapRegion = false;
653 mHandler.removeMessages(TAP);
654 mHandler.removeMessages(SHOW_PRESS);
655 mHandler.removeMessages(LONG_PRESS);
656 }
657 int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
658 if (distance > doubleTapSlopSquare) {
659 mAlwaysInBiggerTapRegion = false;
660 }
661 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800662 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
Adam Powell05a1e1f2012-08-29 13:54:44 -0700663 mLastFocusX = focusX;
664 mLastFocusY = focusY;
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800665 }
Siarhei Vishniakou07f440a2017-12-12 16:35:19 -0800666 final boolean deepPress =
667 motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
668 if (deepPress && hasPendingLongPress) {
669 mHandler.removeMessages(LONG_PRESS);
670 mHandler.sendEmptyMessage(LONG_PRESS);
671 }
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800672 break;
673
674 case MotionEvent.ACTION_UP:
675 mStillDown = false;
676 MotionEvent currentUpEvent = MotionEvent.obtain(ev);
677 if (mIsDoubleTapping) {
678 // Finally, give the up event of the double-tap
679 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
680 } else if (mInLongPress) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800681 mHandler.removeMessages(TAP);
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800682 mInLongPress = false;
683 } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
684 handled = mListener.onSingleTapUp(ev);
685 if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
686 mDoubleTapListener.onSingleTapConfirmed(ev);
687 }
688 } else if (!mIgnoreNextUpEvent) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800689
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800690 // A fling must travel the minimum tap distance
691 final VelocityTracker velocityTracker = mVelocityTracker;
692 final int pointerId = ev.getPointerId(0);
693 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
694 final float velocityY = velocityTracker.getYVelocity(pointerId);
695 final float velocityX = velocityTracker.getXVelocity(pointerId);
696
697 if ((Math.abs(velocityY) > mMinimumFlingVelocity)
698 || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
699 handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
700 }
Adam Powelleca3e602013-02-15 11:26:45 -0800701 }
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800702 if (mPreviousUpEvent != null) {
703 mPreviousUpEvent.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800704 }
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800705 // Hold the event we obtained above - listeners may have changed the original.
706 mPreviousUpEvent = currentUpEvent;
707 if (mVelocityTracker != null) {
708 // This may have been cleared when we called out to the
709 // application above.
710 mVelocityTracker.recycle();
711 mVelocityTracker = null;
712 }
713 mIsDoubleTapping = false;
714 mDeferConfirmSingleTap = false;
715 mIgnoreNextUpEvent = false;
716 mHandler.removeMessages(SHOW_PRESS);
717 mHandler.removeMessages(LONG_PRESS);
718 break;
Jeff Brownbbdc50b2011-04-19 23:46:52 -0700719
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800720 case MotionEvent.ACTION_CANCEL:
721 cancel();
722 break;
Jeff Brownbbdc50b2011-04-19 23:46:52 -0700723 }
724
725 if (!handled && mInputEventConsistencyVerifier != null) {
726 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800727 }
728 return handled;
729 }
730
Mady Mellorb0933442015-05-27 14:15:57 -0700731 /**
732 * Analyzes the given generic motion event and if applicable triggers the
733 * appropriate callbacks on the {@link OnGestureListener} supplied.
734 *
735 * @param ev The current motion event.
736 * @return true if the {@link OnGestureListener} consumed the event,
737 * else false.
738 */
739 public boolean onGenericMotionEvent(MotionEvent ev) {
740 if (mInputEventConsistencyVerifier != null) {
741 mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);
742 }
743
Mady Mellor015020e2015-06-05 14:41:22 -0700744 final int actionButton = ev.getActionButton();
Mady Mellorb0933442015-05-27 14:15:57 -0700745 switch (ev.getActionMasked()) {
746 case MotionEvent.ACTION_BUTTON_PRESS:
Mady Mellor015020e2015-06-05 14:41:22 -0700747 if (mContextClickListener != null && !mInContextClick && !mInLongPress
748 && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
749 || actionButton == MotionEvent.BUTTON_SECONDARY)) {
750 if (mContextClickListener.onContextClick(ev)) {
751 mInContextClick = true;
Mady Mellorb0933442015-05-27 14:15:57 -0700752 mHandler.removeMessages(LONG_PRESS);
753 mHandler.removeMessages(TAP);
754 return true;
755 }
756 }
757 break;
758
759 case MotionEvent.ACTION_BUTTON_RELEASE:
Mady Mellor015020e2015-06-05 14:41:22 -0700760 if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
761 || actionButton == MotionEvent.BUTTON_SECONDARY)) {
762 mInContextClick = false;
Mady Mellorb0933442015-05-27 14:15:57 -0700763 mIgnoreNextUpEvent = true;
764 }
765 break;
766 }
767 return false;
768 }
769
Adam Powell216bccf2010-02-01 15:03:17 -0800770 private void cancel() {
771 mHandler.removeMessages(SHOW_PRESS);
772 mHandler.removeMessages(LONG_PRESS);
773 mHandler.removeMessages(TAP);
774 mVelocityTracker.recycle();
775 mVelocityTracker = null;
776 mIsDoubleTapping = false;
777 mStillDown = false;
Adam Powell17921ee2011-06-09 11:39:21 -0700778 mAlwaysInTapRegion = false;
779 mAlwaysInBiggerTapRegion = false;
Adam Powelleca3e602013-02-15 11:26:45 -0800780 mDeferConfirmSingleTap = false;
Mady Mellora6b16452015-04-14 18:03:34 -0700781 mInLongPress = false;
Mady Mellor015020e2015-06-05 14:41:22 -0700782 mInContextClick = false;
Mady Mellorb0933442015-05-27 14:15:57 -0700783 mIgnoreNextUpEvent = false;
Adam Powell216bccf2010-02-01 15:03:17 -0800784 }
785
Adam Powell05a1e1f2012-08-29 13:54:44 -0700786 private void cancelTaps() {
787 mHandler.removeMessages(SHOW_PRESS);
788 mHandler.removeMessages(LONG_PRESS);
789 mHandler.removeMessages(TAP);
790 mIsDoubleTapping = false;
791 mAlwaysInTapRegion = false;
792 mAlwaysInBiggerTapRegion = false;
Adam Powelleca3e602013-02-15 11:26:45 -0800793 mDeferConfirmSingleTap = false;
Mady Mellora6b16452015-04-14 18:03:34 -0700794 mInLongPress = false;
Mady Mellor015020e2015-06-05 14:41:22 -0700795 mInContextClick = false;
Mady Mellorb0933442015-05-27 14:15:57 -0700796 mIgnoreNextUpEvent = false;
Adam Powell05a1e1f2012-08-29 13:54:44 -0700797 }
798
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800799 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
800 MotionEvent secondDown) {
801 if (!mAlwaysInBiggerTapRegion) {
802 return false;
803 }
804
Adam Powellaf1785f2013-09-05 13:44:45 -0700805 final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
806 if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800807 return false;
808 }
809
810 int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
811 int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
Dennis Kempinac1b31d2016-11-02 17:02:25 -0700812 final boolean isGeneratedGesture =
813 (firstDown.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
814 int slopSquare = isGeneratedGesture ? 0 : mDoubleTapSlopSquare;
815 return (deltaX * deltaX + deltaY * deltaY < slopSquare);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800816 }
817
818 private void dispatchLongPress() {
819 mHandler.removeMessages(TAP);
Adam Powelleca3e602013-02-15 11:26:45 -0800820 mDeferConfirmSingleTap = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800821 mInLongPress = true;
822 mListener.onLongPress(mCurrentDownEvent);
823 }
824}