FreeRDP
DoubleGestureDetector.java
1 /*
2  2 finger gesture detector
3 
4  Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz
5 
6  This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
7  If a copy of the MPL was not distributed with this file, You can obtain one at
8  http://mozilla.org/MPL/2.0/.
9 */
10 
11 package com.freerdp.freerdpcore.utils;
12 
13 import android.content.Context;
14 import android.os.Handler;
15 import android.view.MotionEvent;
16 import android.view.ScaleGestureDetector;
17 
18 import com.freerdp.freerdpcore.utils.GestureDetector.OnGestureListener;
19 
21 {
22  // timeout during that the second finger has to touch the screen before the double finger
23  // detection is cancelled
24  private static final long DOUBLE_TOUCH_TIMEOUT = 100;
25  // timeout during that an UP event will trigger a single double touch event
26  private static final long SINGLE_DOUBLE_TOUCH_TIMEOUT = 1000;
27  // constants for Message.what used by GestureHandler below
28  private static final int TAP = 1;
29  // different detection modes
30  private static final int MODE_UNKNOWN = 0;
31  private static final int MODE_PINCH_ZOOM = 1;
32  private static final int MODE_SCROLL = 2;
33  private static final int SCROLL_SCORE_TO_REACH = 20;
34  private final OnDoubleGestureListener mListener;
35  private int mPointerDistanceSquare;
36  private int mCurrentMode;
37  private int mScrollDetectionScore;
38  private ScaleGestureDetector scaleGestureDetector;
39  private boolean mCancelDetection;
40  private boolean mDoubleInProgress;
41  private GestureHandler mHandler;
42  private MotionEvent mCurrentDownEvent;
43  private MotionEvent mCurrentDoubleDownEvent;
44  private MotionEvent mPreviousUpEvent;
45  private MotionEvent mPreviousPointerUpEvent;
56  public DoubleGestureDetector(Context context, Handler handler, OnDoubleGestureListener listener)
57  {
58  mListener = listener;
59  init(context, handler);
60  }
61 
62  private void init(Context context, Handler handler)
63  {
64  if (mListener == null)
65  {
66  throw new NullPointerException("OnGestureListener must not be null");
67  }
68 
69  if (handler != null)
70  mHandler = new GestureHandler(handler);
71  else
72  mHandler = new GestureHandler();
73 
74  // we use 1cm distance to decide between scroll and pinch zoom
75  // - first convert cm to inches
76  // - then multiply inches by dots per inch
77  float distInches = 0.5f / 2.54f;
78  float distPixelsX = distInches * context.getResources().getDisplayMetrics().xdpi;
79  float distPixelsY = distInches * context.getResources().getDisplayMetrics().ydpi;
80 
81  mPointerDistanceSquare = (int)(distPixelsX * distPixelsX + distPixelsY * distPixelsY);
82  }
83 
89  public void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector)
90  {
91  this.scaleGestureDetector = scaleGestureDetector;
92  }
93 
102  public boolean onTouchEvent(MotionEvent ev)
103  {
104  boolean handled = false;
105  final int action = ev.getAction();
106  // dumpEvent(ev);
107 
108  switch (action & MotionEvent.ACTION_MASK)
109  {
110  case MotionEvent.ACTION_DOWN:
111  if (mCurrentDownEvent != null)
112  mCurrentDownEvent.recycle();
113 
114  mCurrentMode = MODE_UNKNOWN;
115  mCurrentDownEvent = MotionEvent.obtain(ev);
116  mCancelDetection = false;
117  mDoubleInProgress = false;
118  mScrollDetectionScore = 0;
119  handled = true;
120  break;
121 
122  case MotionEvent.ACTION_POINTER_UP:
123  if (mPreviousPointerUpEvent != null)
124  mPreviousPointerUpEvent.recycle();
125  mPreviousPointerUpEvent = MotionEvent.obtain(ev);
126  break;
127 
128  case MotionEvent.ACTION_POINTER_DOWN:
129  // more than 2 fingers down? cancel
130  // 2nd finger touched too late? cancel
131  if (ev.getPointerCount() > 2 ||
132  (ev.getEventTime() - mCurrentDownEvent.getEventTime()) > DOUBLE_TOUCH_TIMEOUT)
133  {
134  cancel();
135  break;
136  }
137 
138  // detection cancelled?
139  if (mCancelDetection)
140  break;
141 
142  // double touch gesture in progress
143  mDoubleInProgress = true;
144  if (mCurrentDoubleDownEvent != null)
145  mCurrentDoubleDownEvent.recycle();
146  mCurrentDoubleDownEvent = MotionEvent.obtain(ev);
147 
148  // set detection mode to unkown and send a TOUCH timeout event to detect single taps
149  mCurrentMode = MODE_UNKNOWN;
150  mHandler.sendEmptyMessageDelayed(TAP, SINGLE_DOUBLE_TOUCH_TIMEOUT);
151 
152  handled |= mListener.onDoubleTouchDown(ev);
153  break;
154 
155  case MotionEvent.ACTION_MOVE:
156 
157  // detection cancelled or not active?
158  if (mCancelDetection || !mDoubleInProgress || ev.getPointerCount() != 2)
159  break;
160 
161  // determine mode
162  if (mCurrentMode == MODE_UNKNOWN)
163  {
164  // did the pointer distance change?
165  if (pointerDistanceChanged(mCurrentDoubleDownEvent, ev))
166  {
167  handled |= scaleGestureDetector.onTouchEvent(mCurrentDownEvent);
168  MotionEvent e = MotionEvent.obtain(ev);
169  e.setAction(mCurrentDoubleDownEvent.getAction());
170  handled |= scaleGestureDetector.onTouchEvent(e);
171  mCurrentMode = MODE_PINCH_ZOOM;
172  break;
173  }
174  else
175  {
176  mScrollDetectionScore++;
177  if (mScrollDetectionScore >= SCROLL_SCORE_TO_REACH)
178  mCurrentMode = MODE_SCROLL;
179  }
180  }
181 
182  switch (mCurrentMode)
183  {
184  case MODE_PINCH_ZOOM:
185  if (scaleGestureDetector != null)
186  handled |= scaleGestureDetector.onTouchEvent(ev);
187  break;
188 
189  case MODE_SCROLL:
190  handled = mListener.onDoubleTouchScroll(mCurrentDownEvent, ev);
191  break;
192 
193  default:
194  handled = true;
195  break;
196  }
197 
198  break;
199 
200  case MotionEvent.ACTION_UP:
201  // fingers were not removed equally? cancel
202  if (mPreviousPointerUpEvent != null &&
203  (ev.getEventTime() - mPreviousPointerUpEvent.getEventTime()) >
204  DOUBLE_TOUCH_TIMEOUT)
205  {
206  mPreviousPointerUpEvent.recycle();
207  mPreviousPointerUpEvent = null;
208  cancel();
209  break;
210  }
211 
212  // detection cancelled or not active?
213  if (mCancelDetection || !mDoubleInProgress)
214  break;
215 
216  boolean hasTapEvent = mHandler.hasMessages(TAP);
217  MotionEvent currentUpEvent = MotionEvent.obtain(ev);
218  if (mCurrentMode == MODE_UNKNOWN && hasTapEvent)
219  handled = mListener.onDoubleTouchSingleTap(mCurrentDoubleDownEvent);
220  else if (mCurrentMode == MODE_PINCH_ZOOM)
221  handled = scaleGestureDetector.onTouchEvent(ev);
222 
223  if (mPreviousUpEvent != null)
224  mPreviousUpEvent.recycle();
225 
226  // Hold the event we obtained above - listeners may have changed the original.
227  mPreviousUpEvent = currentUpEvent;
228  handled |= mListener.onDoubleTouchUp(ev);
229  break;
230 
231  case MotionEvent.ACTION_CANCEL:
232  cancel();
233  break;
234  }
235 
236  if ((action == MotionEvent.ACTION_MOVE) && handled == false)
237  handled = true;
238 
239  return handled;
240  }
241 
242  private void cancel()
243  {
244  mHandler.removeMessages(TAP);
245  mCurrentMode = MODE_UNKNOWN;
246  mCancelDetection = true;
247  mDoubleInProgress = false;
248  }
249 
250  // returns true of the distance between the two pointers changed
251  private boolean pointerDistanceChanged(MotionEvent oldEvent, MotionEvent newEvent)
252  {
253  int deltaX1 = Math.abs((int)oldEvent.getX(0) - (int)oldEvent.getX(1));
254  int deltaX2 = Math.abs((int)newEvent.getX(0) - (int)newEvent.getX(1));
255  int distXSquare = (deltaX2 - deltaX1) * (deltaX2 - deltaX1);
256 
257  int deltaY1 = Math.abs((int)oldEvent.getY(0) - (int)oldEvent.getY(1));
258  int deltaY2 = Math.abs((int)newEvent.getY(0) - (int)newEvent.getY(1));
259  int distYSquare = (deltaY2 - deltaY1) * (deltaY2 - deltaY1);
260 
261  return (distXSquare + distYSquare) > mPointerDistanceSquare;
262  }
263 
270  public interface OnDoubleGestureListener {
271 
275  boolean onDoubleTouchDown(MotionEvent e);
276 
280  boolean onDoubleTouchUp(MotionEvent e);
281 
289  boolean onDoubleTouchSingleTap(MotionEvent e);
290 
306  boolean onDoubleTouchScroll(MotionEvent e1, MotionEvent e2);
307  }
308 
309  /*
310  private void dumpEvent(MotionEvent event) {
311  String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" ,
312  "POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" };
313  StringBuilder sb = new StringBuilder();
314  int action = event.getAction();
315  int actionCode = action & MotionEvent.ACTION_MASK;
316  sb.append("event ACTION_" ).append(names[actionCode]);
317  if (actionCode == MotionEvent.ACTION_POINTER_DOWN
318  || actionCode == MotionEvent.ACTION_POINTER_UP) {
319  sb.append("(pid " ).append(
320  action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
321  sb.append(")" );
322  }
323  sb.append("[" );
324  for (int i = 0; i < event.getPointerCount(); i++) {
325  sb.append("#" ).append(i);
326  sb.append("(pid " ).append(event.getPointerId(i));
327  sb.append(")=" ).append((int) event.getX(i));
328  sb.append("," ).append((int) event.getY(i));
329  if (i + 1 < event.getPointerCount())
330  sb.append(";" );
331  }
332  sb.append("]" );
333  Log.d("DoubleDetector", sb.toString());
334  }
335  */
336 
337  private class GestureHandler extends Handler
338  {
339  GestureHandler()
340  {
341  super();
342  }
343 
344  GestureHandler(Handler handler)
345  {
346  super(handler.getLooper());
347  }
348  }
349 }
void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector)
DoubleGestureDetector(Context context, Handler handler, OnDoubleGestureListener listener)