blob: 3ee5f1fcc8833d3826948895034fa3d2bb8daaf3 [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 Inwoode5ad5982018-08-17 15:07:52 +010019import android.annotation.UnsupportedAppUsage;
Adam Powell216bccf2010-02-01 15:03:17 -080020import android.content.Context;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.os.Handler;
22import android.os.Message;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023
24/**
25 * Detects various gestures and events using the supplied {@link MotionEvent}s.
26 * The {@link OnGestureListener} callback will notify users when a particular
27 * motion event has occurred. This class should only be used with {@link MotionEvent}s
28 * reported via touch (don't use for trackball events).
29 *
30 * To use this class:
31 * <ul>
32 * <li>Create an instance of the {@code GestureDetector} for your {@link View}
33 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
34 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
35 * will be executed when the events occur.
Mady Mellor015020e2015-06-05 14:41:22 -070036 * <li>If listening for {@link OnContextClickListener#onContextClick(MotionEvent)}
Mady Mellorb0933442015-05-27 14:15:57 -070037 * you must call {@link #onGenericMotionEvent(MotionEvent)}
38 * in {@link View#onGenericMotionEvent(MotionEvent)}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039 * </ul>
40 */
41public class GestureDetector {
42 /**
43 * The listener that is used to notify when gestures occur.
44 * If you want to listen for all the different gestures then implement
45 * this interface. If you only want to listen for a subset it might
46 * be easier to extend {@link SimpleOnGestureListener}.
47 */
48 public interface OnGestureListener {
49
50 /**
51 * Notified when a tap occurs with the down {@link MotionEvent}
52 * that triggered it. This will be triggered immediately for
53 * every down event. All other events should be preceded by this.
54 *
55 * @param e The down motion event.
56 */
57 boolean onDown(MotionEvent e);
58
59 /**
60 * The user has performed a down {@link MotionEvent} and not performed
61 * a move or up yet. This event is commonly used to provide visual
62 * feedback to the user to let them know that their action has been
63 * recognized i.e. highlight an element.
64 *
65 * @param e The down motion event
66 */
67 void onShowPress(MotionEvent e);
68
69 /**
70 * Notified when a tap occurs with the up {@link MotionEvent}
71 * that triggered it.
72 *
73 * @param e The up motion event that completed the first tap
74 * @return true if the event is consumed, else false
75 */
76 boolean onSingleTapUp(MotionEvent e);
77
78 /**
79 * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
80 * current move {@link MotionEvent}. The distance in x and y is also supplied for
81 * convenience.
82 *
83 * @param e1 The first down motion event that started the scrolling.
84 * @param e2 The move motion event that triggered the current onScroll.
85 * @param distanceX The distance along the X axis that has been scrolled since the last
86 * call to onScroll. This is NOT the distance between {@code e1}
87 * and {@code e2}.
88 * @param distanceY The distance along the Y axis that has been scrolled since the last
89 * call to onScroll. This is NOT the distance between {@code e1}
90 * and {@code e2}.
91 * @return true if the event is consumed, else false
92 */
93 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
94
95 /**
96 * Notified when a long press occurs with the initial on down {@link MotionEvent}
97 * that trigged it.
98 *
99 * @param e The initial on down motion event that started the longpress.
100 */
101 void onLongPress(MotionEvent e);
102
103 /**
104 * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
105 * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
106 * the x and y axis in pixels per second.
107 *
108 * @param e1 The first down motion event that started the fling.
109 * @param e2 The move motion event that triggered the current onFling.
110 * @param velocityX The velocity of this fling measured in pixels per second
111 * along the x axis.
112 * @param velocityY The velocity of this fling measured in pixels per second
113 * along the y axis.
114 * @return true if the event is consumed, else false
115 */
116 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
117 }
118
119 /**
120 * The listener that is used to notify when a double-tap or a confirmed
121 * single-tap occur.
122 */
123 public interface OnDoubleTapListener {
124 /**
125 * Notified when a single-tap occurs.
126 * <p>
127 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
128 * will only be called after the detector is confident that the user's
129 * first tap is not followed by a second tap leading to a double-tap
130 * gesture.
131 *
132 * @param e The down motion event of the single-tap.
133 * @return true if the event is consumed, else false
134 */
135 boolean onSingleTapConfirmed(MotionEvent e);
136
137 /**
138 * Notified when a double-tap occurs.
139 *
140 * @param e The down motion event of the first tap of the double-tap.
141 * @return true if the event is consumed, else false
142 */
143 boolean onDoubleTap(MotionEvent e);
144
145 /**
146 * Notified when an event within a double-tap gesture occurs, including
147 * the down, move, and up events.
148 *
149 * @param e The motion event that occurred during the double-tap gesture.
150 * @return true if the event is consumed, else false
151 */
152 boolean onDoubleTapEvent(MotionEvent e);
153 }
154
155 /**
Mady Mellor015020e2015-06-05 14:41:22 -0700156 * The listener that is used to notify when a context click occurs. When listening for a
157 * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
Mady Mellorb0933442015-05-27 14:15:57 -0700158 * {@link View#onGenericMotionEvent(MotionEvent)}.
Mady Mellora6b16452015-04-14 18:03:34 -0700159 */
Mady Mellor015020e2015-06-05 14:41:22 -0700160 public interface OnContextClickListener {
Mady Mellora6b16452015-04-14 18:03:34 -0700161 /**
Mady Mellor015020e2015-06-05 14:41:22 -0700162 * Notified when a context click occurs.
Mady Mellora6b16452015-04-14 18:03:34 -0700163 *
Mady Mellor015020e2015-06-05 14:41:22 -0700164 * @param e The motion event that occurred during the context click.
Mady Mellora6b16452015-04-14 18:03:34 -0700165 * @return true if the event is consumed, else false
166 */
Mady Mellor015020e2015-06-05 14:41:22 -0700167 boolean onContextClick(MotionEvent e);
Mady Mellora6b16452015-04-14 18:03:34 -0700168 }
169
170 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800171 * A convenience class to extend when you only want to listen for a subset
172 * of all the gestures. This implements all methods in the
Mady Mellor015020e2015-06-05 14:41:22 -0700173 * {@link OnGestureListener}, {@link OnDoubleTapListener}, and {@link OnContextClickListener}
Mady Mellora6b16452015-04-14 18:03:34 -0700174 * but does nothing and return {@code false} for all applicable methods.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800175 */
Mady Mellora6b16452015-04-14 18:03:34 -0700176 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
Mady Mellor015020e2015-06-05 14:41:22 -0700177 OnContextClickListener {
Mady Mellora6b16452015-04-14 18:03:34 -0700178
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800179 public boolean onSingleTapUp(MotionEvent e) {
180 return false;
181 }
182
183 public void onLongPress(MotionEvent e) {
184 }
185
186 public boolean onScroll(MotionEvent e1, MotionEvent e2,
187 float distanceX, float distanceY) {
188 return false;
189 }
190
191 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
192 float velocityY) {
193 return false;
194 }
195
196 public void onShowPress(MotionEvent e) {
197 }
198
199 public boolean onDown(MotionEvent e) {
200 return false;
201 }
202
203 public boolean onDoubleTap(MotionEvent e) {
204 return false;
205 }
206
207 public boolean onDoubleTapEvent(MotionEvent e) {
208 return false;
209 }
210
211 public boolean onSingleTapConfirmed(MotionEvent e) {
212 return false;
213 }
Mady Mellora6b16452015-04-14 18:03:34 -0700214
Mady Mellor015020e2015-06-05 14:41:22 -0700215 public boolean onContextClick(MotionEvent e) {
Mady Mellora6b16452015-04-14 18:03:34 -0700216 return false;
217 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800218 }
219
Mathew Inwoode5ad5982018-08-17 15:07:52 +0100220 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800221 private int mTouchSlopSquare;
Gilles Debunne006fa482011-10-27 17:23:36 -0700222 private int mDoubleTapTouchSlopSquare;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800223 private int mDoubleTapSlopSquare;
Mathew Inwoode5ad5982018-08-17 15:07:52 +0100224 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800225 private int mMinimumFlingVelocity;
Romain Guy4296fc42009-07-06 11:48:52 -0700226 private int mMaximumFlingVelocity;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800227
Mathew Inwoode5ad5982018-08-17 15:07:52 +0100228 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800229 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
230 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800231 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
Adam Powellaf1785f2013-09-05 13:44:45 -0700232 private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800233
234 // constants for Message.what used by GestureHandler below
235 private static final int SHOW_PRESS = 1;
236 private static final int LONG_PRESS = 2;
237 private static final int TAP = 3;
238
239 private final Handler mHandler;
Mathew Inwoode5ad5982018-08-17 15:07:52 +0100240 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800241 private final OnGestureListener mListener;
242 private OnDoubleTapListener mDoubleTapListener;
Mady Mellor015020e2015-06-05 14:41:22 -0700243 private OnContextClickListener mContextClickListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800244
245 private boolean mStillDown;
Adam Powelleca3e602013-02-15 11:26:45 -0800246 private boolean mDeferConfirmSingleTap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800247 private boolean mInLongPress;
Mady Mellor015020e2015-06-05 14:41:22 -0700248 private boolean mInContextClick;
Mathew Inwoode5ad5982018-08-17 15:07:52 +0100249 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800250 private boolean mAlwaysInTapRegion;
251 private boolean mAlwaysInBiggerTapRegion;
Mady Mellorb0933442015-05-27 14:15:57 -0700252 private boolean mIgnoreNextUpEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800253
254 private MotionEvent mCurrentDownEvent;
255 private MotionEvent mPreviousUpEvent;
256
257 /**
258 * True when the user is still touching for the second tap (down, move, and
259 * up events). Can only be true if there is a double tap listener attached.
260 */
261 private boolean mIsDoubleTapping;
262
Adam Powell05a1e1f2012-08-29 13:54:44 -0700263 private float mLastFocusX;
264 private float mLastFocusY;
265 private float mDownFocusX;
266 private float mDownFocusY;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800267
268 private boolean mIsLongpressEnabled;
269
270 /**
271 * Determines speed during touch scrolling
272 */
273 private VelocityTracker mVelocityTracker;
274
Jeff Brown21bc5c92011-02-28 18:27:14 -0800275 /**
276 * Consistency verifier for debugging purposes.
277 */
278 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
279 InputEventConsistencyVerifier.isInstrumentationEnabled() ?
280 new InputEventConsistencyVerifier(this, 0) : null;
281
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282 private class GestureHandler extends Handler {
283 GestureHandler() {
284 super();
285 }
286
287 GestureHandler(Handler handler) {
288 super(handler.getLooper());
289 }
290
291 @Override
292 public void handleMessage(Message msg) {
293 switch (msg.what) {
294 case SHOW_PRESS:
295 mListener.onShowPress(mCurrentDownEvent);
296 break;
297
298 case LONG_PRESS:
299 dispatchLongPress();
300 break;
301
302 case TAP:
303 // If the user's finger is still down, do not count it as a tap
Adam Powelleca3e602013-02-15 11:26:45 -0800304 if (mDoubleTapListener != null) {
305 if (!mStillDown) {
306 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
307 } else {
308 mDeferConfirmSingleTap = true;
309 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800310 }
311 break;
312
313 default:
314 throw new RuntimeException("Unknown message " + msg); //never
315 }
316 }
317 }
318
319 /**
320 * Creates a GestureDetector with the supplied listener.
321 * This variant of the constructor should be used from a non-UI thread
322 * (as it allows specifying the Handler).
323 *
324 * @param listener the listener invoked for all the callbacks, this must
325 * not be null.
326 * @param handler the handler to use
327 *
328 * @throws NullPointerException if either {@code listener} or
329 * {@code handler} is null.
330 *
331 * @deprecated Use {@link #GestureDetector(android.content.Context,
332 * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
333 */
334 @Deprecated
335 public GestureDetector(OnGestureListener listener, Handler handler) {
336 this(null, listener, handler);
337 }
338
339 /**
340 * Creates a GestureDetector with the supplied listener.
341 * You may only use this constructor from a UI thread (this is the usual situation).
342 * @see android.os.Handler#Handler()
343 *
344 * @param listener the listener invoked for all the callbacks, this must
345 * not be null.
346 *
347 * @throws NullPointerException if {@code listener} is null.
348 *
349 * @deprecated Use {@link #GestureDetector(android.content.Context,
350 * android.view.GestureDetector.OnGestureListener)} instead.
351 */
352 @Deprecated
353 public GestureDetector(OnGestureListener listener) {
354 this(null, listener, null);
355 }
356
357 /**
358 * Creates a GestureDetector with the supplied listener.
Scott Mainc27ea252013-08-30 13:56:05 -0700359 * You may only use this constructor from a {@link android.os.Looper} thread.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800360 * @see android.os.Handler#Handler()
361 *
362 * @param context the application's context
363 * @param listener the listener invoked for all the callbacks, this must
364 * not be null.
365 *
366 * @throws NullPointerException if {@code listener} is null.
367 */
368 public GestureDetector(Context context, OnGestureListener listener) {
369 this(context, listener, null);
370 }
371
372 /**
Scott Mainc27ea252013-08-30 13:56:05 -0700373 * Creates a GestureDetector with the supplied listener that runs deferred events on the
374 * thread associated with the supplied {@link android.os.Handler}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800375 * @see android.os.Handler#Handler()
376 *
377 * @param context the application's context
378 * @param listener the listener invoked for all the callbacks, this must
379 * not be null.
Scott Mainc27ea252013-08-30 13:56:05 -0700380 * @param handler the handler to use for running deferred listener events.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800381 *
382 * @throws NullPointerException if {@code listener} is null.
383 */
384 public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
Adam Powell05a1e1f2012-08-29 13:54:44 -0700385 if (handler != null) {
386 mHandler = new GestureHandler(handler);
387 } else {
388 mHandler = new GestureHandler();
389 }
390 mListener = listener;
391 if (listener instanceof OnDoubleTapListener) {
392 setOnDoubleTapListener((OnDoubleTapListener) listener);
393 }
Mady Mellor015020e2015-06-05 14:41:22 -0700394 if (listener instanceof OnContextClickListener) {
395 setContextClickListener((OnContextClickListener) listener);
Mady Mellora6b16452015-04-14 18:03:34 -0700396 }
Adam Powell05a1e1f2012-08-29 13:54:44 -0700397 init(context);
Adam Powell216bccf2010-02-01 15:03:17 -0800398 }
399
400 /**
Scott Mainc27ea252013-08-30 13:56:05 -0700401 * Creates a GestureDetector with the supplied listener that runs deferred events on the
402 * thread associated with the supplied {@link android.os.Handler}.
Adam Powell216bccf2010-02-01 15:03:17 -0800403 * @see android.os.Handler#Handler()
404 *
405 * @param context the application's context
406 * @param listener the listener invoked for all the callbacks, this must
407 * not be null.
Scott Mainc27ea252013-08-30 13:56:05 -0700408 * @param handler the handler to use for running deferred listener events.
409 * @param unused currently not used.
Adam Powell216bccf2010-02-01 15:03:17 -0800410 *
411 * @throws NullPointerException if {@code listener} is null.
412 */
413 public GestureDetector(Context context, OnGestureListener listener, Handler handler,
Adam Powell05a1e1f2012-08-29 13:54:44 -0700414 boolean unused) {
415 this(context, listener, handler);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800416 }
417
Adam Powell05a1e1f2012-08-29 13:54:44 -0700418 private void init(Context context) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800419 if (mListener == null) {
420 throw new NullPointerException("OnGestureListener must not be null");
421 }
422 mIsLongpressEnabled = true;
423
424 // Fallback to support pre-donuts releases
Gilles Debunne006fa482011-10-27 17:23:36 -0700425 int touchSlop, doubleTapSlop, doubleTapTouchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800426 if (context == null) {
427 //noinspection deprecation
428 touchSlop = ViewConfiguration.getTouchSlop();
Gilles Debunne006fa482011-10-27 17:23:36 -0700429 doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800430 doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
431 //noinspection deprecation
432 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
Romain Guy4296fc42009-07-06 11:48:52 -0700433 mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800434 } else {
435 final ViewConfiguration configuration = ViewConfiguration.get(context);
436 touchSlop = configuration.getScaledTouchSlop();
Gilles Debunne006fa482011-10-27 17:23:36 -0700437 doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800438 doubleTapSlop = configuration.getScaledDoubleTapSlop();
439 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
Romain Guy4296fc42009-07-06 11:48:52 -0700440 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800441 }
442 mTouchSlopSquare = touchSlop * touchSlop;
Gilles Debunne006fa482011-10-27 17:23:36 -0700443 mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800444 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
445 }
446
447 /**
448 * Sets the listener which will be called for double-tap and related
449 * gestures.
450 *
451 * @param onDoubleTapListener the listener invoked for all the callbacks, or
452 * null to stop listening for double-tap gestures.
453 */
454 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
455 mDoubleTapListener = onDoubleTapListener;
456 }
457
458 /**
Mady Mellor015020e2015-06-05 14:41:22 -0700459 * Sets the listener which will be called for context clicks.
Mady Mellora6b16452015-04-14 18:03:34 -0700460 *
Mady Mellor015020e2015-06-05 14:41:22 -0700461 * @param onContextClickListener the listener invoked for all the callbacks, or null to stop
462 * listening for context clicks.
Mady Mellora6b16452015-04-14 18:03:34 -0700463 */
Mady Mellor015020e2015-06-05 14:41:22 -0700464 public void setContextClickListener(OnContextClickListener onContextClickListener) {
465 mContextClickListener = onContextClickListener;
Mady Mellora6b16452015-04-14 18:03:34 -0700466 }
467
468 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800469 * Set whether longpress is enabled, if this is enabled when a user
470 * presses and holds down you get a longpress event and nothing further.
471 * If it's disabled the user can press and hold down and then later
472 * moved their finger and you will get scroll events. By default
473 * longpress is enabled.
474 *
475 * @param isLongpressEnabled whether longpress should be enabled.
476 */
477 public void setIsLongpressEnabled(boolean isLongpressEnabled) {
478 mIsLongpressEnabled = isLongpressEnabled;
479 }
480
481 /**
482 * @return true if longpress is enabled, else false.
483 */
484 public boolean isLongpressEnabled() {
485 return mIsLongpressEnabled;
486 }
487
488 /**
489 * Analyzes the given motion event and if applicable triggers the
490 * appropriate callbacks on the {@link OnGestureListener} supplied.
491 *
492 * @param ev The current motion event.
493 * @return true if the {@link OnGestureListener} consumed the event,
494 * else false.
495 */
496 public boolean onTouchEvent(MotionEvent ev) {
Jeff Brown21bc5c92011-02-28 18:27:14 -0800497 if (mInputEventConsistencyVerifier != null) {
498 mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
499 }
500
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800501 final int action = ev.getAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800502
503 if (mVelocityTracker == null) {
504 mVelocityTracker = VelocityTracker.obtain();
505 }
506 mVelocityTracker.addMovement(ev);
507
Adam Powellf90165a2012-08-31 11:11:39 -0700508 final boolean pointerUp =
509 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
Adam Powell05a1e1f2012-08-29 13:54:44 -0700510 final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
Dennis Kempinac1b31d2016-11-02 17:02:25 -0700511 final boolean isGeneratedGesture =
512 (ev.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
Adam Powell05a1e1f2012-08-29 13:54:44 -0700513
514 // Determine focal point
515 float sumX = 0, sumY = 0;
516 final int count = ev.getPointerCount();
517 for (int i = 0; i < count; i++) {
518 if (skipIndex == i) continue;
519 sumX += ev.getX(i);
520 sumY += ev.getY(i);
521 }
522 final int div = pointerUp ? count - 1 : count;
523 final float focusX = sumX / div;
524 final float focusY = sumY / div;
525
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800526 boolean handled = false;
527
Adam Powell216bccf2010-02-01 15:03:17 -0800528 switch (action & MotionEvent.ACTION_MASK) {
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800529 case MotionEvent.ACTION_POINTER_DOWN:
530 mDownFocusX = mLastFocusX = focusX;
531 mDownFocusY = mLastFocusY = focusY;
532 // Cancel long press and taps
533 cancelTaps();
534 break;
Adam Powell216bccf2010-02-01 15:03:17 -0800535
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800536 case MotionEvent.ACTION_POINTER_UP:
537 mDownFocusX = mLastFocusX = focusX;
538 mDownFocusY = mLastFocusY = focusY;
Adam Powellcd663592012-09-11 09:52:37 -0700539
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800540 // Check the dot product of current velocities.
541 // If the pointer that left was opposing another velocity vector, clear.
542 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
543 final int upIndex = ev.getActionIndex();
544 final int id1 = ev.getPointerId(upIndex);
545 final float x1 = mVelocityTracker.getXVelocity(id1);
546 final float y1 = mVelocityTracker.getYVelocity(id1);
547 for (int i = 0; i < count; i++) {
548 if (i == upIndex) continue;
Adam Powellcd663592012-09-11 09:52:37 -0700549
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800550 final int id2 = ev.getPointerId(i);
551 final float x = x1 * mVelocityTracker.getXVelocity(id2);
552 final float y = y1 * mVelocityTracker.getYVelocity(id2);
Adam Powellcd663592012-09-11 09:52:37 -0700553
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800554 final float dot = x + y;
555 if (dot < 0) {
556 mVelocityTracker.clear();
557 break;
558 }
559 }
560 break;
561
562 case MotionEvent.ACTION_DOWN:
563 if (mDoubleTapListener != null) {
564 boolean hadTapMessage = mHandler.hasMessages(TAP);
565 if (hadTapMessage) mHandler.removeMessages(TAP);
566 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
567 && hadTapMessage
568 && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
569 // This is a second tap
570 mIsDoubleTapping = true;
571 // Give a callback with the first tap of the double-tap
572 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
573 // Give a callback with down event of the double-tap
574 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
575 } else {
576 // This is a first tap
577 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
578 }
579 }
580
581 mDownFocusX = mLastFocusX = focusX;
582 mDownFocusY = mLastFocusY = focusY;
583 if (mCurrentDownEvent != null) {
584 mCurrentDownEvent.recycle();
585 }
586 mCurrentDownEvent = MotionEvent.obtain(ev);
587 mAlwaysInTapRegion = true;
588 mAlwaysInBiggerTapRegion = true;
589 mStillDown = true;
590 mInLongPress = false;
591 mDeferConfirmSingleTap = false;
592
593 if (mIsLongpressEnabled) {
594 mHandler.removeMessages(LONG_PRESS);
595 mHandler.sendEmptyMessageAtTime(LONG_PRESS,
596 mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT);
597 }
598 mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
599 mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
600 handled |= mListener.onDown(ev);
601 break;
602
603 case MotionEvent.ACTION_MOVE:
604 if (mInLongPress || mInContextClick) {
Adam Powellcd663592012-09-11 09:52:37 -0700605 break;
606 }
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800607 final float scrollX = mLastFocusX - focusX;
608 final float scrollY = mLastFocusY - focusY;
609 if (mIsDoubleTapping) {
610 // Give the move events of the double-tap
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800611 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800612 } else if (mAlwaysInTapRegion) {
613 final int deltaX = (int) (focusX - mDownFocusX);
614 final int deltaY = (int) (focusY - mDownFocusY);
615 int distance = (deltaX * deltaX) + (deltaY * deltaY);
616 int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
617 if (distance > slopSquare) {
618 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
619 mLastFocusX = focusX;
620 mLastFocusY = focusY;
621 mAlwaysInTapRegion = false;
622 mHandler.removeMessages(TAP);
623 mHandler.removeMessages(SHOW_PRESS);
624 mHandler.removeMessages(LONG_PRESS);
625 }
626 int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
627 if (distance > doubleTapSlopSquare) {
628 mAlwaysInBiggerTapRegion = false;
629 }
630 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800631 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
Adam Powell05a1e1f2012-08-29 13:54:44 -0700632 mLastFocusX = focusX;
633 mLastFocusY = focusY;
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800634 }
635 break;
636
637 case MotionEvent.ACTION_UP:
638 mStillDown = false;
639 MotionEvent currentUpEvent = MotionEvent.obtain(ev);
640 if (mIsDoubleTapping) {
641 // Finally, give the up event of the double-tap
642 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
643 } else if (mInLongPress) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800644 mHandler.removeMessages(TAP);
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800645 mInLongPress = false;
646 } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
647 handled = mListener.onSingleTapUp(ev);
648 if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
649 mDoubleTapListener.onSingleTapConfirmed(ev);
650 }
651 } else if (!mIgnoreNextUpEvent) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800652
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800653 // A fling must travel the minimum tap distance
654 final VelocityTracker velocityTracker = mVelocityTracker;
655 final int pointerId = ev.getPointerId(0);
656 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
657 final float velocityY = velocityTracker.getYVelocity(pointerId);
658 final float velocityX = velocityTracker.getXVelocity(pointerId);
659
660 if ((Math.abs(velocityY) > mMinimumFlingVelocity)
661 || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
662 handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
663 }
Adam Powelleca3e602013-02-15 11:26:45 -0800664 }
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800665 if (mPreviousUpEvent != null) {
666 mPreviousUpEvent.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800667 }
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800668 // Hold the event we obtained above - listeners may have changed the original.
669 mPreviousUpEvent = currentUpEvent;
670 if (mVelocityTracker != null) {
671 // This may have been cleared when we called out to the
672 // application above.
673 mVelocityTracker.recycle();
674 mVelocityTracker = null;
675 }
676 mIsDoubleTapping = false;
677 mDeferConfirmSingleTap = false;
678 mIgnoreNextUpEvent = false;
679 mHandler.removeMessages(SHOW_PRESS);
680 mHandler.removeMessages(LONG_PRESS);
681 break;
Jeff Brownbbdc50b2011-04-19 23:46:52 -0700682
Siarhei Vishniakoud60090d2017-12-12 16:24:26 -0800683 case MotionEvent.ACTION_CANCEL:
684 cancel();
685 break;
Jeff Brownbbdc50b2011-04-19 23:46:52 -0700686 }
687
688 if (!handled && mInputEventConsistencyVerifier != null) {
689 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800690 }
691 return handled;
692 }
693
Mady Mellorb0933442015-05-27 14:15:57 -0700694 /**
695 * Analyzes the given generic motion event and if applicable triggers the
696 * appropriate callbacks on the {@link OnGestureListener} supplied.
697 *
698 * @param ev The current motion event.
699 * @return true if the {@link OnGestureListener} consumed the event,
700 * else false.
701 */
702 public boolean onGenericMotionEvent(MotionEvent ev) {
703 if (mInputEventConsistencyVerifier != null) {
704 mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);
705 }
706
Mady Mellor015020e2015-06-05 14:41:22 -0700707 final int actionButton = ev.getActionButton();
Mady Mellorb0933442015-05-27 14:15:57 -0700708 switch (ev.getActionMasked()) {
709 case MotionEvent.ACTION_BUTTON_PRESS:
Mady Mellor015020e2015-06-05 14:41:22 -0700710 if (mContextClickListener != null && !mInContextClick && !mInLongPress
711 && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
712 || actionButton == MotionEvent.BUTTON_SECONDARY)) {
713 if (mContextClickListener.onContextClick(ev)) {
714 mInContextClick = true;
Mady Mellorb0933442015-05-27 14:15:57 -0700715 mHandler.removeMessages(LONG_PRESS);
716 mHandler.removeMessages(TAP);
717 return true;
718 }
719 }
720 break;
721
722 case MotionEvent.ACTION_BUTTON_RELEASE:
Mady Mellor015020e2015-06-05 14:41:22 -0700723 if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
724 || actionButton == MotionEvent.BUTTON_SECONDARY)) {
725 mInContextClick = false;
Mady Mellorb0933442015-05-27 14:15:57 -0700726 mIgnoreNextUpEvent = true;
727 }
728 break;
729 }
730 return false;
731 }
732
Adam Powell216bccf2010-02-01 15:03:17 -0800733 private void cancel() {
734 mHandler.removeMessages(SHOW_PRESS);
735 mHandler.removeMessages(LONG_PRESS);
736 mHandler.removeMessages(TAP);
737 mVelocityTracker.recycle();
738 mVelocityTracker = null;
739 mIsDoubleTapping = false;
740 mStillDown = false;
Adam Powell17921ee2011-06-09 11:39:21 -0700741 mAlwaysInTapRegion = false;
742 mAlwaysInBiggerTapRegion = false;
Adam Powelleca3e602013-02-15 11:26:45 -0800743 mDeferConfirmSingleTap = false;
Mady Mellora6b16452015-04-14 18:03:34 -0700744 mInLongPress = false;
Mady Mellor015020e2015-06-05 14:41:22 -0700745 mInContextClick = false;
Mady Mellorb0933442015-05-27 14:15:57 -0700746 mIgnoreNextUpEvent = false;
Adam Powell216bccf2010-02-01 15:03:17 -0800747 }
748
Adam Powell05a1e1f2012-08-29 13:54:44 -0700749 private void cancelTaps() {
750 mHandler.removeMessages(SHOW_PRESS);
751 mHandler.removeMessages(LONG_PRESS);
752 mHandler.removeMessages(TAP);
753 mIsDoubleTapping = false;
754 mAlwaysInTapRegion = false;
755 mAlwaysInBiggerTapRegion = false;
Adam Powelleca3e602013-02-15 11:26:45 -0800756 mDeferConfirmSingleTap = false;
Mady Mellora6b16452015-04-14 18:03:34 -0700757 mInLongPress = false;
Mady Mellor015020e2015-06-05 14:41:22 -0700758 mInContextClick = false;
Mady Mellorb0933442015-05-27 14:15:57 -0700759 mIgnoreNextUpEvent = false;
Adam Powell05a1e1f2012-08-29 13:54:44 -0700760 }
761
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800762 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
763 MotionEvent secondDown) {
764 if (!mAlwaysInBiggerTapRegion) {
765 return false;
766 }
767
Adam Powellaf1785f2013-09-05 13:44:45 -0700768 final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
769 if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800770 return false;
771 }
772
773 int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
774 int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
Dennis Kempinac1b31d2016-11-02 17:02:25 -0700775 final boolean isGeneratedGesture =
776 (firstDown.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
777 int slopSquare = isGeneratedGesture ? 0 : mDoubleTapSlopSquare;
778 return (deltaX * deltaX + deltaY * deltaY < slopSquare);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800779 }
780
781 private void dispatchLongPress() {
782 mHandler.removeMessages(TAP);
Adam Powelleca3e602013-02-15 11:26:45 -0800783 mDeferConfirmSingleTap = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800784 mInLongPress = true;
785 mListener.onLongPress(mCurrentDownEvent);
786 }
787}