FreeRDP
GestureDetector.java
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;
60  private boolean mIsDoubleTapping;
61  private float mLastMotionY;
62  private float mLastMotionX;
63  private boolean mIsLongpressEnabled;
69  private boolean mIgnoreMultitouch;
80  public GestureDetector(Context context, OnGestureListener listener)
81  {
82  this(context, listener, null);
83  }
84 
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 
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  {
131  }
132  init(context, ignoreMultitouch);
133  }
134 
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 
174  public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener)
175  {
176  mDoubleTapListener = onDoubleTapListener;
177  }
178 
189  {
190  mIsLongpressEnabled = isLongpressEnabled;
191  }
192 
196  public boolean isLongpressEnabled()
197  {
198  return mIsLongpressEnabled;
199  }
200 
201  public void setLongPressTimeout(int timeout)
202  {
203  mLongpressTimeout = timeout;
204  }
205 
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 
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 
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 
406  private void dispatchLongPress()
407  {
408  mHandler.removeMessages(TAP);
409  mInLongPress = true;
410  mListener.onLongPress(mCurrentDownEvent);
411  }
412 
419  public interface OnGestureListener {
420 
428  boolean onDown(MotionEvent e);
429 
437  boolean onUp(MotionEvent e);
438 
447  void onShowPress(MotionEvent e);
448 
456  boolean onSingleTapUp(MotionEvent e);
457 
473  boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
474 
481  void onLongPress(MotionEvent e);
482 
488  void onLongPressUp(MotionEvent e);
489  }
490 
495  public interface OnDoubleTapListener {
507  boolean onSingleTapConfirmed(MotionEvent e);
508 
515  boolean onDoubleTap(MotionEvent e);
516 
524  boolean onDoubleTapEvent(MotionEvent e);
525  }
526 
534  {
535  public boolean onSingleTapUp(MotionEvent e)
536  {
537  return false;
538  }
539 
540  public void onLongPress(MotionEvent e)
541  {
542  }
543 
544  public void onLongPressUp(MotionEvent e)
545  {
546  }
547 
548  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
549  {
550  return false;
551  }
552 
553  public void onShowPress(MotionEvent e)
554  {
555  }
556 
557  public boolean onDown(MotionEvent e)
558  {
559  return false;
560  }
561 
562  public boolean onUp(MotionEvent e)
563  {
564  return false;
565  }
566 
567  public boolean onDoubleTap(MotionEvent e)
568  {
569  return false;
570  }
571 
572  public boolean onDoubleTapEvent(MotionEvent e)
573  {
574  return false;
575  }
576 
577  public boolean onSingleTapConfirmed(MotionEvent e)
578  {
579  return false;
580  }
581  }
582 
583  private class GestureHandler extends Handler
584  {
585  GestureHandler()
586  {
587  super();
588  }
589 
590  GestureHandler(Handler handler)
591  {
592  super(handler.getLooper());
593  }
594 
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 }
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener)
void setIsLongpressEnabled(boolean isLongpressEnabled)
GestureDetector(Context context, OnGestureListener listener, Handler handler, boolean ignoreMultitouch)
GestureDetector(Context context, OnGestureListener listener)
GestureDetector(Context context, OnGestureListener listener, Handler handler)
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)