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 unknown and send a TOUCH timeout event to detect single
149  // taps
150  mCurrentMode = MODE_UNKNOWN;
151  mHandler.sendEmptyMessageDelayed(TAP, SINGLE_DOUBLE_TOUCH_TIMEOUT);
152 
153  handled |= mListener.onDoubleTouchDown(ev);
154  break;
155 
156  case MotionEvent.ACTION_MOVE:
157 
158  // detection cancelled or not active?
159  if (mCancelDetection || !mDoubleInProgress || ev.getPointerCount() != 2)
160  break;
161 
162  // determine mode
163  if (mCurrentMode == MODE_UNKNOWN)
164  {
165  // did the pointer distance change?
166  if (pointerDistanceChanged(mCurrentDoubleDownEvent, ev))
167  {
168  handled |= scaleGestureDetector.onTouchEvent(mCurrentDownEvent);
169  MotionEvent e = MotionEvent.obtain(ev);
170  e.setAction(mCurrentDoubleDownEvent.getAction());
171  handled |= scaleGestureDetector.onTouchEvent(e);
172  mCurrentMode = MODE_PINCH_ZOOM;
173  break;
174  }
175  else
176  {
177  mScrollDetectionScore++;
178  if (mScrollDetectionScore >= SCROLL_SCORE_TO_REACH)
179  mCurrentMode = MODE_SCROLL;
180  }
181  }
182 
183  switch (mCurrentMode)
184  {
185  case MODE_PINCH_ZOOM:
186  if (scaleGestureDetector != null)
187  handled |= scaleGestureDetector.onTouchEvent(ev);
188  break;
189 
190  case MODE_SCROLL:
191  handled = mListener.onDoubleTouchScroll(mCurrentDownEvent, ev);
192  break;
193 
194  default:
195  handled = true;
196  break;
197  }
198 
199  break;
200 
201  case MotionEvent.ACTION_UP:
202  // fingers were not removed equally? cancel
203  if (mPreviousPointerUpEvent != null &&
204  (ev.getEventTime() - mPreviousPointerUpEvent.getEventTime()) >
205  DOUBLE_TOUCH_TIMEOUT)
206  {
207  mPreviousPointerUpEvent.recycle();
208  mPreviousPointerUpEvent = null;
209  cancel();
210  break;
211  }
212 
213  // detection cancelled or not active?
214  if (mCancelDetection || !mDoubleInProgress)
215  break;
216 
217  boolean hasTapEvent = mHandler.hasMessages(TAP);
218  MotionEvent currentUpEvent = MotionEvent.obtain(ev);
219  if (mCurrentMode == MODE_UNKNOWN && hasTapEvent)
220  handled = mListener.onDoubleTouchSingleTap(mCurrentDoubleDownEvent);
221  else if (mCurrentMode == MODE_PINCH_ZOOM)
222  handled = scaleGestureDetector.onTouchEvent(ev);
223 
224  if (mPreviousUpEvent != null)
225  mPreviousUpEvent.recycle();
226 
227  // Hold the event we obtained above - listeners may have changed the original.
228  mPreviousUpEvent = currentUpEvent;
229  handled |= mListener.onDoubleTouchUp(ev);
230  break;
231 
232  case MotionEvent.ACTION_CANCEL:
233  cancel();
234  break;
235  }
236 
237  if ((action == MotionEvent.ACTION_MOVE) && handled == false)
238  handled = true;
239 
240  return handled;
241  }
242 
243  private void cancel()
244  {
245  mHandler.removeMessages(TAP);
246  mCurrentMode = MODE_UNKNOWN;
247  mCancelDetection = true;
248  mDoubleInProgress = false;
249  }
250 
251  // returns true of the distance between the two pointers changed
252  private boolean pointerDistanceChanged(MotionEvent oldEvent, MotionEvent newEvent)
253  {
254  int deltaX1 = Math.abs((int)oldEvent.getX(0) - (int)oldEvent.getX(1));
255  int deltaX2 = Math.abs((int)newEvent.getX(0) - (int)newEvent.getX(1));
256  int distXSquare = (deltaX2 - deltaX1) * (deltaX2 - deltaX1);
257 
258  int deltaY1 = Math.abs((int)oldEvent.getY(0) - (int)oldEvent.getY(1));
259  int deltaY2 = Math.abs((int)newEvent.getY(0) - (int)newEvent.getY(1));
260  int distYSquare = (deltaY2 - deltaY1) * (deltaY2 - deltaY1);
261 
262  return (distXSquare + distYSquare) > mPointerDistanceSquare;
263  }
264 
271  public interface OnDoubleGestureListener {
272 
276  boolean onDoubleTouchDown(MotionEvent e);
277 
281  boolean onDoubleTouchUp(MotionEvent e);
282 
290  boolean onDoubleTouchSingleTap(MotionEvent e);
291 
307  boolean onDoubleTouchScroll(MotionEvent e1, MotionEvent e2);
308  }
309 
310  /*
311  private void dumpEvent(MotionEvent event) {
312  String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" ,
313  "POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" };
314  StringBuilder sb = new StringBuilder();
315  int action = event.getAction();
316  int actionCode = action & MotionEvent.ACTION_MASK;
317  sb.append("event ACTION_" ).append(names[actionCode]);
318  if (actionCode == MotionEvent.ACTION_POINTER_DOWN
319  || actionCode == MotionEvent.ACTION_POINTER_UP) {
320  sb.append("(pid " ).append(
321  action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
322  sb.append(")" );
323  }
324  sb.append("[" );
325  for (int i = 0; i < event.getPointerCount(); i++) {
326  sb.append("#" ).append(i);
327  sb.append("(pid " ).append(event.getPointerId(i));
328  sb.append(")=" ).append((int) event.getX(i));
329  sb.append("," ).append((int) event.getY(i));
330  if (i + 1 < event.getPointerCount())
331  sb.append(";" );
332  }
333  sb.append("]" );
334  Log.d("DoubleDetector", sb.toString());
335  }
336  */
337 
338  private class GestureHandler extends Handler
339  {
340  GestureHandler()
341  {
342  super();
343  }
344 
345  GestureHandler(Handler handler)
346  {
347  super(handler.getLooper());
348  }
349  }
350 }
void setScaleGestureDetector(ScaleGestureDetector scaleGestureDetector)
DoubleGestureDetector(Context context, Handler handler, OnDoubleGestureListener listener)