1 /*
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  * Modified for aFreeRDP by Martin Fleisz (martin.fleisz@thincast.com)
17  */
18 
19 package com.freerdp.freerdpcore.utils;
20 
21 import android.content.Context;
22 import android.os.Build;
23 import android.os.Handler;
24 import android.os.Message;
25 import android.util.DisplayMetrics;
26 import android.view.MotionEvent;
27 import android.view.ViewConfiguration;
28 
29 public class GestureDetector
30 {
31 
32 	private static final int TAP_TIMEOUT = 100;
33 	private static final int DOUBLE_TAP_TIMEOUT = 200;
34 	// Distance a touch can wander before we think the user is the first touch in a sequence of
35 	// double tap
36 	private static final int LARGE_TOUCH_SLOP = 18;
37 	// Distance between the first touch and second touch to still be considered a double tap
38 	private static final int DOUBLE_TAP_SLOP = 100;
39 	// constants for Message.what used by GestureHandler below
40 	private static final int SHOW_PRESS = 1;
41 	private static final int LONG_PRESS = 2;
42 	private static final int TAP = 3;
43 	private final Handler mHandler;
44 	private final OnGestureListener mListener;
45 	private int mTouchSlopSquare;
46 	private int mLargeTouchSlopSquare;
47 	private int mDoubleTapSlopSquare;
48 	private int mLongpressTimeout = 100;
49 	private OnDoubleTapListener mDoubleTapListener;
50 	private boolean mStillDown;
51 	private boolean mInLongPress;
52 	private boolean mAlwaysInTapRegion;
53 	private boolean mAlwaysInBiggerTapRegion;
54 	private MotionEvent mCurrentDownEvent;
55 	private MotionEvent mPreviousUpEvent;
56 	/**
57 	 * True when the user is still touching for the second tap (down, move, and
58 	 * up events). Can only be true if there is a double tap listener attached.
59 	 */
60 	private boolean mIsDoubleTapping;
61 	private float mLastMotionY;
62 	private float mLastMotionX;
63 	private boolean mIsLongpressEnabled;
64 	/**
65 	 * True if we are at a target API level of >= Froyo or the developer can
66 	 * explicitly set it. If true, input events with > 1 pointer will be ignored
67 	 * so we can work side by side with multitouch gesture detectors.
68 	 */
69 	private boolean mIgnoreMultitouch;
70 	/**
71 	 * Creates a GestureDetector with the supplied listener.
72 	 * You may only use this constructor from a UI thread (this is the usual situation).
73 	 *
74 	 * @param context  the application's context
75 	 * @param listener the listener invoked for all the callbacks, this must
76 	 *                 not be null.
77 	 * @throws NullPointerException if {@code listener} is null.
78 	 * @see android.os.Handler#Handler()
79 	 */
GestureDetector(Context context, OnGestureListener listener)80 	public GestureDetector(Context context, OnGestureListener listener)
81 	{
82 		this(context, listener, null);
83 	}
84 
85 	/**
86 	 * Creates a GestureDetector with the supplied listener.
87 	 * You may only use this constructor from a UI thread (this is the usual situation).
88 	 *
89 	 * @param context  the application's context
90 	 * @param listener the listener invoked for all the callbacks, this must
91 	 *                 not be null.
92 	 * @param handler  the handler to use
93 	 * @throws NullPointerException if {@code listener} is null.
94 	 * @see android.os.Handler#Handler()
95 	 */
GestureDetector(Context context, OnGestureListener listener, Handler handler)96 	public GestureDetector(Context context, OnGestureListener listener, Handler handler)
97 	{
98 		this(context, listener, handler,
99 		     context != null &&
100 		         context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO);
101 	}
102 
103 	/**
104 	 * Creates a GestureDetector with the supplied listener.
105 	 * You may only use this constructor from a UI thread (this is the usual situation).
106 	 *
107 	 * @param context          the application's context
108 	 * @param listener         the listener invoked for all the callbacks, this must
109 	 *                         not be null.
110 	 * @param handler          the handler to use
111 	 * @param ignoreMultitouch whether events involving more than one pointer should
112 	 *                         be ignored.
113 	 * @throws NullPointerException if {@code listener} is null.
114 	 * @see android.os.Handler#Handler()
115 	 */
GestureDetector(Context context, OnGestureListener listener, Handler handler, boolean ignoreMultitouch)116 	public GestureDetector(Context context, OnGestureListener listener, Handler handler,
117 	                       boolean ignoreMultitouch)
118 	{
119 		if (handler != null)
120 		{
121 			mHandler = new GestureHandler(handler);
122 		}
123 		else
124 		{
125 			mHandler = new GestureHandler();
126 		}
127 		mListener = listener;
128 		if (listener instanceof OnDoubleTapListener)
129 		{
130 			setOnDoubleTapListener((OnDoubleTapListener)listener);
131 		}
132 		init(context, ignoreMultitouch);
133 	}
134 
init(Context context, boolean ignoreMultitouch)135 	private void init(Context context, boolean ignoreMultitouch)
136 	{
137 		if (mListener == null)
138 		{
139 			throw new NullPointerException("OnGestureListener must not be null");
140 		}
141 		mIsLongpressEnabled = true;
142 		mIgnoreMultitouch = ignoreMultitouch;
143 
144 		// Fallback to support pre-donuts releases
145 		int touchSlop, largeTouchSlop, doubleTapSlop;
146 		if (context == null)
147 		{
148 			// noinspection deprecation
149 			touchSlop = ViewConfiguration.getTouchSlop();
150 			largeTouchSlop = touchSlop + 2;
151 			doubleTapSlop = DOUBLE_TAP_SLOP;
152 		}
153 		else
154 		{
155 			final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
156 			final float density = metrics.density;
157 			final ViewConfiguration configuration = ViewConfiguration.get(context);
158 			touchSlop = configuration.getScaledTouchSlop();
159 			largeTouchSlop = (int)(density * LARGE_TOUCH_SLOP + 0.5f);
160 			doubleTapSlop = configuration.getScaledDoubleTapSlop();
161 		}
162 		mTouchSlopSquare = touchSlop * touchSlop;
163 		mLargeTouchSlopSquare = largeTouchSlop * largeTouchSlop;
164 		mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
165 	}
166 
167 	/**
168 	 * Sets the listener which will be called for double-tap and related
169 	 * gestures.
170 	 *
171 	 * @param onDoubleTapListener the listener invoked for all the callbacks, or
172 	 *                            null to stop listening for double-tap gestures.
173 	 */
setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener)174 	public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener)
175 	{
176 		mDoubleTapListener = onDoubleTapListener;
177 	}
178 
179 	/**
180 	 * Set whether longpress is enabled, if this is enabled when a user
181 	 * presses and holds down you get a longpress event and nothing further.
182 	 * If it's disabled the user can press and hold down and then later
183 	 * moved their finger and you will get scroll events. By default
184 	 * longpress is enabled.
185 	 *
186 	 * @param isLongpressEnabled whether longpress should be enabled.
187 	 */
setIsLongpressEnabled(boolean isLongpressEnabled)188 	public void setIsLongpressEnabled(boolean isLongpressEnabled)
189 	{
190 		mIsLongpressEnabled = isLongpressEnabled;
191 	}
192 
193 	/**
194 	 * @return true if longpress is enabled, else false.
195 	 */
isLongpressEnabled()196 	public boolean isLongpressEnabled()
197 	{
198 		return mIsLongpressEnabled;
199 	}
200 
setLongPressTimeout(int timeout)201 	public void setLongPressTimeout(int timeout)
202 	{
203 		mLongpressTimeout = timeout;
204 	}
205 
206 	/**
207 	 * Analyzes the given motion event and if applicable triggers the
208 	 * appropriate callbacks on the {@link OnGestureListener} supplied.
209 	 *
210 	 * @param ev The current motion event.
211 	 * @return true if the {@link OnGestureListener} consumed the event,
212 	 * else false.
213 	 */
onTouchEvent(MotionEvent ev)214 	public boolean onTouchEvent(MotionEvent ev)
215 	{
216 		final int action = ev.getAction();
217 		final float y = ev.getY();
218 		final float x = ev.getX();
219 
220 		boolean handled = false;
221 
222 		switch (action & MotionEvent.ACTION_MASK)
223 		{
224 			case MotionEvent.ACTION_POINTER_DOWN:
225 				if (mIgnoreMultitouch)
226 				{
227 					// Multitouch event - abort.
228 					cancel();
229 				}
230 				break;
231 
232 			case MotionEvent.ACTION_POINTER_UP:
233 				// Ending a multitouch gesture and going back to 1 finger
234 				if (mIgnoreMultitouch && ev.getPointerCount() == 2)
235 				{
236 					int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
237 					              MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0)
238 					                ? 1
239 					                : 0;
240 					mLastMotionX = ev.getX(index);
241 					mLastMotionY = ev.getY(index);
242 				}
243 				break;
244 
245 			case MotionEvent.ACTION_DOWN:
246 				if (mDoubleTapListener != null)
247 				{
248 					boolean hadTapMessage = mHandler.hasMessages(TAP);
249 					if (hadTapMessage)
250 						mHandler.removeMessages(TAP);
251 					if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) &&
252 					    hadTapMessage &&
253 					    isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev))
254 					{
255 						// This is a second tap
256 						mIsDoubleTapping = true;
257 						// Give a callback with the first tap of the double-tap
258 						handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
259 						// Give a callback with down event of the double-tap
260 						handled |= mDoubleTapListener.onDoubleTapEvent(ev);
261 					}
262 					else
263 					{
264 						// This is a first tap
265 						mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
266 					}
267 				}
268 
269 				mLastMotionX = x;
270 				mLastMotionY = y;
271 				if (mCurrentDownEvent != null)
272 				{
273 					mCurrentDownEvent.recycle();
274 				}
275 				mCurrentDownEvent = MotionEvent.obtain(ev);
276 				mAlwaysInTapRegion = true;
277 				mAlwaysInBiggerTapRegion = true;
278 				mStillDown = true;
279 				mInLongPress = false;
280 
281 				if (mIsLongpressEnabled)
282 				{
283 					mHandler.removeMessages(LONG_PRESS);
284 					mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() +
285 					                                                TAP_TIMEOUT +
286 					                                                mLongpressTimeout);
287 				}
288 				mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
289 				                                mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
290 				handled |= mListener.onDown(ev);
291 				break;
292 
293 			case MotionEvent.ACTION_MOVE:
294 				if (mIgnoreMultitouch && ev.getPointerCount() > 1)
295 				{
296 					break;
297 				}
298 				final float scrollX = mLastMotionX - x;
299 				final float scrollY = mLastMotionY - y;
300 				if (mIsDoubleTapping)
301 				{
302 					// Give the move events of the double-tap
303 					handled |= mDoubleTapListener.onDoubleTapEvent(ev);
304 				}
305 				else if (mAlwaysInTapRegion)
306 				{
307 					final int deltaX = (int)(x - mCurrentDownEvent.getX());
308 					final int deltaY = (int)(y - mCurrentDownEvent.getY());
309 					int distance = (deltaX * deltaX) + (deltaY * deltaY);
310 					if (distance > mTouchSlopSquare)
311 					{
312 						mLastMotionX = x;
313 						mLastMotionY = y;
314 						mAlwaysInTapRegion = false;
315 						mHandler.removeMessages(TAP);
316 						mHandler.removeMessages(SHOW_PRESS);
317 						mHandler.removeMessages(LONG_PRESS);
318 					}
319 					if (distance > mLargeTouchSlopSquare)
320 					{
321 						mAlwaysInBiggerTapRegion = false;
322 					}
323 					handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
324 				}
325 				else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1))
326 				{
327 					handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
328 					mLastMotionX = x;
329 					mLastMotionY = y;
330 				}
331 				break;
332 
333 			case MotionEvent.ACTION_UP:
334 				mStillDown = false;
335 				MotionEvent currentUpEvent = MotionEvent.obtain(ev);
336 				if (mIsDoubleTapping)
337 				{
338 					// Finally, give the up event of the double-tap
339 					handled |= mDoubleTapListener.onDoubleTapEvent(ev);
340 				}
341 				else if (mInLongPress)
342 				{
343 					mHandler.removeMessages(TAP);
344 					mListener.onLongPressUp(ev);
345 					mInLongPress = false;
346 				}
347 				else if (mAlwaysInTapRegion)
348 				{
349 					handled = mListener.onSingleTapUp(mCurrentDownEvent);
350 				}
351 				else
352 				{
353 					// A fling must travel the minimum tap distance
354 				}
355 				if (mPreviousUpEvent != null)
356 				{
357 					mPreviousUpEvent.recycle();
358 				}
359 				// Hold the event we obtained above - listeners may have changed the original.
360 				mPreviousUpEvent = currentUpEvent;
361 				mIsDoubleTapping = false;
362 				mHandler.removeMessages(SHOW_PRESS);
363 				mHandler.removeMessages(LONG_PRESS);
364 				handled |= mListener.onUp(ev);
365 				break;
366 			case MotionEvent.ACTION_CANCEL:
367 				cancel();
368 				break;
369 		}
370 		return handled;
371 	}
372 
cancel()373 	private void cancel()
374 	{
375 		mHandler.removeMessages(SHOW_PRESS);
376 		mHandler.removeMessages(LONG_PRESS);
377 		mHandler.removeMessages(TAP);
378 		mAlwaysInTapRegion = false; // ensures that we won't receive an OnSingleTap notification
379 		                            // when a 2-Finger tap is performed
380 		mIsDoubleTapping = false;
381 		mStillDown = false;
382 		if (mInLongPress)
383 		{
384 			mInLongPress = false;
385 		}
386 	}
387 
isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, MotionEvent secondDown)388 	private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
389 	                                      MotionEvent secondDown)
390 	{
391 		if (!mAlwaysInBiggerTapRegion)
392 		{
393 			return false;
394 		}
395 
396 		if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT)
397 		{
398 			return false;
399 		}
400 
401 		int deltaX = (int)firstDown.getX() - (int)secondDown.getX();
402 		int deltaY = (int)firstDown.getY() - (int)secondDown.getY();
403 		return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
404 	}
405 
dispatchLongPress()406 	private void dispatchLongPress()
407 	{
408 		mHandler.removeMessages(TAP);
409 		mInLongPress = true;
410 		mListener.onLongPress(mCurrentDownEvent);
411 	}
412 
413 	/**
414 	 * The listener that is used to notify when gestures occur.
415 	 * If you want to listen for all the different gestures then implement
416 	 * this interface. If you only want to listen for a subset it might
417 	 * be easier to extend {@link SimpleOnGestureListener}.
418 	 */
419 	public interface OnGestureListener {
420 
421 		/**
422 		 * Notified when a tap occurs with the down {@link MotionEvent}
423 		 * that triggered it. This will be triggered immediately for
424 		 * every down event. All other events should be preceded by this.
425 		 *
426 		 * @param e The down motion event.
427 		 */
onDown(MotionEvent e)428 		boolean onDown(MotionEvent e);
429 
430 		/**
431 		 * Notified when a tap finishes with the up {@link MotionEvent}
432 		 * that triggered it. This will be triggered immediately for
433 		 * every up event. All other events should be preceded by this.
434 		 *
435 		 * @param e The up motion event.
436 		 */
onUp(MotionEvent e)437 		boolean onUp(MotionEvent e);
438 
439 		/**
440 		 * The user has performed a down {@link MotionEvent} and not performed
441 		 * a move or up yet. This event is commonly used to provide visual
442 		 * feedback to the user to let them know that their action has been
443 		 * recognized i.e. highlight an element.
444 		 *
445 		 * @param e The down motion event
446 		 */
onShowPress(MotionEvent e)447 		void onShowPress(MotionEvent e);
448 
449 		/**
450 		 * Notified when a tap occurs with the up {@link MotionEvent}
451 		 * that triggered it.
452 		 *
453 		 * @param e The up motion event that completed the first tap
454 		 * @return true if the event is consumed, else false
455 		 */
onSingleTapUp(MotionEvent e)456 		boolean onSingleTapUp(MotionEvent e);
457 
458 		/**
459 		 * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
460 		 * current move {@link MotionEvent}. The distance in x and y is also supplied for
461 		 * convenience.
462 		 *
463 		 * @param e1        The first down motion event that started the scrolling.
464 		 * @param e2        The move motion event that triggered the current onScroll.
465 		 * @param distanceX The distance along the X axis that has been scrolled since the last
466 		 *                  call to onScroll. This is NOT the distance between {@code e1}
467 		 *                  and {@code e2}.
468 		 * @param distanceY The distance along the Y axis that has been scrolled since the last
469 		 *                  call to onScroll. This is NOT the distance between {@code e1}
470 		 *                  and {@code e2}.
471 		 * @return true if the event is consumed, else false
472 		 */
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)473 		boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
474 
475 		/**
476 		 * Notified when a long press occurs with the initial on down {@link MotionEvent}
477 		 * that trigged it.
478 		 *
479 		 * @param e The initial on down motion event that started the longpress.
480 		 */
onLongPress(MotionEvent e)481 		void onLongPress(MotionEvent e);
482 
483 		/**
484 		 * Notified when a long press ends with the final {@link MotionEvent}.
485 		 *
486 		 * @param e The up motion event that ended the longpress.
487 		 */
onLongPressUp(MotionEvent e)488 		void onLongPressUp(MotionEvent e);
489 	}
490 
491 	/**
492 	 * The listener that is used to notify when a double-tap or a confirmed
493 	 * single-tap occur.
494 	 */
495 	public interface OnDoubleTapListener {
496 		/**
497 		 * Notified when a single-tap occurs.
498 		 * <p>
499 		 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
500 		 * will only be called after the detector is confident that the user's
501 		 * first tap is not followed by a second tap leading to a double-tap
502 		 * gesture.
503 		 *
504 		 * @param e The down motion event of the single-tap.
505 		 * @return true if the event is consumed, else false
506 		 */
onSingleTapConfirmed(MotionEvent e)507 		boolean onSingleTapConfirmed(MotionEvent e);
508 
509 		/**
510 		 * Notified when a double-tap occurs.
511 		 *
512 		 * @param e The down motion event of the first tap of the double-tap.
513 		 * @return true if the event is consumed, else false
514 		 */
onDoubleTap(MotionEvent e)515 		boolean onDoubleTap(MotionEvent e);
516 
517 		/**
518 		 * Notified when an event within a double-tap gesture occurs, including
519 		 * the down, move, and up events.
520 		 *
521 		 * @param e The motion event that occurred during the double-tap gesture.
522 		 * @return true if the event is consumed, else false
523 		 */
onDoubleTapEvent(MotionEvent e)524 		boolean onDoubleTapEvent(MotionEvent e);
525 	}
526 
527 	/**
528 	 * A convenience class to extend when you only want to listen for a subset
529 	 * of all the gestures. This implements all methods in the
530 	 * {@link OnGestureListener} and {@link OnDoubleTapListener} but does
531 	 * nothing and return {@code false} for all applicable methods.
532 	 */
533 	public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener
534 	{
onSingleTapUp(MotionEvent e)535 		public boolean onSingleTapUp(MotionEvent e)
536 		{
537 			return false;
538 		}
539 
onLongPress(MotionEvent e)540 		public void onLongPress(MotionEvent e)
541 		{
542 		}
543 
onLongPressUp(MotionEvent e)544 		public void onLongPressUp(MotionEvent e)
545 		{
546 		}
547 
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)548 		public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
549 		{
550 			return false;
551 		}
552 
onShowPress(MotionEvent e)553 		public void onShowPress(MotionEvent e)
554 		{
555 		}
556 
onDown(MotionEvent e)557 		public boolean onDown(MotionEvent e)
558 		{
559 			return false;
560 		}
561 
onUp(MotionEvent e)562 		public boolean onUp(MotionEvent e)
563 		{
564 			return false;
565 		}
566 
onDoubleTap(MotionEvent e)567 		public boolean onDoubleTap(MotionEvent e)
568 		{
569 			return false;
570 		}
571 
onDoubleTapEvent(MotionEvent e)572 		public boolean onDoubleTapEvent(MotionEvent e)
573 		{
574 			return false;
575 		}
576 
onSingleTapConfirmed(MotionEvent e)577 		public boolean onSingleTapConfirmed(MotionEvent e)
578 		{
579 			return false;
580 		}
581 	}
582 
583 	private class GestureHandler extends Handler
584 	{
GestureHandler()585 		GestureHandler()
586 		{
587 			super();
588 		}
589 
GestureHandler(Handler handler)590 		GestureHandler(Handler handler)
591 		{
592 			super(handler.getLooper());
593 		}
594 
handleMessage(Message msg)595 		@Override public void handleMessage(Message msg)
596 		{
597 			switch (msg.what)
598 			{
599 				case SHOW_PRESS:
600 					mListener.onShowPress(mCurrentDownEvent);
601 					break;
602 
603 				case LONG_PRESS:
604 					dispatchLongPress();
605 					break;
606 
607 				case TAP:
608 					// If the user's finger is still down, do not count it as a tap
609 					if (mDoubleTapListener != null && !mStillDown)
610 					{
611 						mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
612 					}
613 					break;
614 
615 				default:
616 					throw new RuntimeException("Unknown message " + msg); // never
617 			}
618 		}
619 	}
620 }
621