PageRenderTime 52ms CodeModel.GetById 23ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/ime/latinime/src/com/googlecode/eyesfree/inputmethod/MultitouchGestureDetector.java

http://eyes-free.googlecode.com/
Java | 526 lines | 316 code | 83 blank | 127 comment | 73 complexity | 6d6a03e3906de20097feff4624eaef8f MD5 | raw file
  1/*
  2 * Copyright (C) 2011 Google Inc.
  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 com.googlecode.eyesfree.inputmethod;
 18
 19import android.content.Context;
 20import android.os.Handler;
 21import android.os.Message;
 22import android.view.MotionEvent;
 23import android.view.VelocityTracker;
 24import android.view.View;
 25import android.view.ViewConfiguration;
 26import android.view.accessibility.AccessibilityManager;
 27
 28import com.googlecode.eyesfree.utils.compat.AccessibilityManagerCompatUtils;
 29import com.googlecode.eyesfree.utils.compat.InputDeviceCompatUtils;
 30import com.googlecode.eyesfree.utils.compat.MotionEventCompatUtils;
 31
 32/**
 33 * Detects various gestures and events using the supplied {@link MotionEvent}s.
 34 * The {@link MultitouchGestureListener} callback will notify users when a
 35 * particular motion event has occurred. This class should only be used with
 36 * {@link MotionEvent}s reported via touch (don't use for trackball events). To
 37 * use this class:
 38 * <ul>
 39 * <li>Create an instance of the {@code GestureDetector} for your {@link View}
 40 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
 41 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback will
 42 * be executed when the events occur.
 43 * </ul>
 44 */
 45public class MultitouchGestureDetector {
 46    public interface MultitouchGestureListener {
 47        public boolean onDown(MotionEvent ev);
 48
 49        public boolean onTap(MotionEvent ev);
 50
 51        public boolean onDoubleTap(MotionEvent ev);
 52
 53        public boolean onLongPress(MotionEvent ev);
 54
 55        public boolean onSlideTap(MotionEvent ev);
 56
 57        public boolean onMove(MotionEvent ev);
 58
 59        public boolean onFlick(MotionEvent e1, MotionEvent e2);
 60    }
 61
 62    private int mTouchSlopSquare;
 63    private int mDoubleTapSlopSquare;
 64
 65    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
 66    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
 67    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
 68
 69    /** The maximum duration of a flick gesture in milliseconds. */
 70    private static final int FLICK_TIMEOUT = 250;
 71
 72    // constants for Message.what used by GestureHandler below
 73    private static final int LONG_PRESS = 1;
 74    private static final int FIRST_TAP = 2;
 75    private static final int SLIDE_TAP = 3;
 76    private static final int FLICK = 4;
 77    private static final int SHOW_PRESS = 5;
 78
 79    private final Handler mHandler;
 80    private final AccessibilityManager mAccessibilityManager;
 81
 82    private MultitouchGestureListener mListener;
 83
 84    private boolean mInLongPress;
 85    private boolean mAlwaysInTapRegion;
 86    private boolean mAlwaysInDoubleTapRegion;
 87
 88    private MotionEvent mCurrentDownEvent;
 89    private MotionEvent mPreviousUpEvent;
 90
 91    /**
 92     * The first UP (or POINTER_UP) event to occur after a DOWN (or
 93     * POINTER_DOWN) event. This gets reset to null on DOWN events.
 94     */
 95    private MotionEvent mFirstUpEvent;
 96
 97    /**
 98     * True when the user is still touching for the second tap (down, move, and
 99     * up events). Can only be true if there is a double tap listener attached.
100     */
101    private boolean mIsDoubleTapping;
102
103    /** Whether the user is still holding a finger on the screen. */
104    private boolean mIsStillDown;
105
106    /** The event time from the last received event. */
107    private long mLastEventTime;
108
109    private boolean mIsLongPressEnabled;
110    private boolean mIsDoubleTapEnabled;
111    private int mDoubleTapMinFingers;
112
113    /**
114     * Determines speed during touch scrolling
115     */
116    private VelocityTracker mVelocityTracker;
117
118    private class MultitouchGestureHandler extends Handler {
119        MultitouchGestureHandler() {
120            super();
121        }
122
123        MultitouchGestureHandler(Handler handler) {
124            super(handler.getLooper());
125        }
126
127        @Override
128        public void handleMessage(Message msg) {
129            switch (msg.what) {
130                case SHOW_PRESS:
131                    mListener.onDown(mCurrentDownEvent);
132                    break;
133                case FIRST_TAP:
134                    if (!mIsStillDown) {
135                        mListener.onTap(mCurrentDownEvent);
136                    }
137                    break;
138                case LONG_PRESS:
139                    mHandler.removeMessages(FIRST_TAP);
140                    mInLongPress = true;
141                    mListener.onLongPress(mCurrentDownEvent);
142                    break;
143                case FLICK:
144                    break;
145            }
146        }
147    }
148
149    /**
150     * Creates a GestureDetector with no listener. You may only use this
151     * constructor from a UI thread (this is the usual situation).
152     *
153     * @see android.os.Handler#Handler()
154     * @param context the application's context
155     */
156    public MultitouchGestureDetector(Context context) {
157        this(context, null, null);
158    }
159
160    /**
161     * Creates a GestureDetector with the supplied listener. You may only use
162     * this constructor from a UI thread (this is the usual situation).
163     *
164     * @see android.os.Handler#Handler()
165     * @param context the application's context
166     * @param listener the listener invoked for all the callbacks
167     */
168    public MultitouchGestureDetector(Context context, MultitouchGestureListener listener) {
169        this(context, listener, null);
170    }
171
172    /**
173     * Creates a GestureDetector with the supplied listener. You may only use
174     * this constructor from a UI thread (this is the usual situation).
175     *
176     * @see android.os.Handler#Handler()
177     * @param context the application's context
178     * @param listener the listener invoked for all the callbacks
179     * @param handler the handler to use
180     */
181    public MultitouchGestureDetector(Context context, MultitouchGestureListener listener,
182            Handler handler) {
183        if (handler != null) {
184            mHandler = new MultitouchGestureHandler(handler);
185        } else {
186            mHandler = new MultitouchGestureHandler();
187        }
188
189        mAccessibilityManager =
190                (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
191        mListener = listener;
192
193        init(context);
194    }
195
196    /**
197     * Sets the listener.
198     *
199     * @param listener The listener invoked for all the callbacks.
200     */
201    public void setListener(MultitouchGestureListener listener) {
202        mListener = listener;
203    }
204
205    private void init(Context context) {
206        mIsLongPressEnabled = true;
207        mIsDoubleTapEnabled = true;
208        mDoubleTapMinFingers = 1;
209
210        final ViewConfiguration configuration = ViewConfiguration.get(context);
211        final int touchSlop = (int) (0.5 * configuration.getScaledTouchSlop());
212        final int doubleTapSlop = configuration.getScaledDoubleTapSlop();
213
214        mTouchSlopSquare = touchSlop * touchSlop;
215        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
216    }
217
218    /**
219     * Sets whether double tap is enabled. If this is enabled, when a user
220     * quickly taps twice you get a double tap event and nothing further. If
221     * it's disabled, you get two tap events. By default, double tap is enabled.
222     *
223     * @param isDoubleTapEnabled Whether double tap should be enabled.
224     */
225    public void setIsDoubleTapEnabled(boolean isDoubleTapEnabled) {
226        mIsDoubleTapEnabled = isDoubleTapEnabled;
227    }
228
229    /**
230     * Sets the minimum number of fingers required for a double-tap event. By
231     * default, this number is one.
232     *
233     * @param count The minimum number of fingers required for a double-tap.
234     * @see #setIsDoubleTapEnabled(boolean)
235     */
236    public void setDoubleTapMinFingers(int count) {
237        mDoubleTapMinFingers = count;
238    }
239
240    /**
241     * Sets whether long press is enabled. If this is enabled, when a user
242     * presses and holds down you get a long press event and nothing further. If
243     * it's disabled, the user can press and hold down and then later move their
244     * finger and you will get scroll events. By default, long press is enabled.
245     *
246     * @param isLongPressEnabled Whether long press should be enabled.
247     */
248    public void setIsLongPressEnabled(boolean isLongPressEnabled) {
249        mIsLongPressEnabled = isLongPressEnabled;
250    }
251
252    /**
253     * @return true if long press is enabled, else false.
254     */
255    public boolean isLongPressEnabled() {
256        return mIsLongPressEnabled;
257    }
258
259    /**
260     * Analyzes the given motion event and if applicable triggers the
261     * appropriate callbacks on the {@link MultitouchGestureListener} supplied.
262     *
263     * @param ev The current motion event.
264     * @return true if the {@link MultitouchGestureListener} consumed the event,
265     *         else false.
266     */
267    public boolean onTouchEvent(MotionEvent ev) {
268        if (mListener == null) {
269            return false;
270        }
271
272        // If touch exploration is enabled, use the following workarounds:
273        // 1. Detect two-finger scroll and adjust the pointer count.
274        // 2. Detect hover to touch transition and drop extra up event.
275        if (AccessibilityManagerCompatUtils.isTouchExplorationEnabled(mAccessibilityManager)) {
276            handleTouchExploration(ev);
277        }
278
279        mLastEventTime = ev.getEventTime();
280
281        final int action = ev.getAction();
282        final float y = ev.getY();
283        final float x = ev.getX();
284
285        if (mVelocityTracker == null) {
286            mVelocityTracker = VelocityTracker.obtain();
287        }
288        mVelocityTracker.addMovement(ev);
289
290        boolean handled = false;
291
292        switch (action & MotionEvent.ACTION_MASK) {
293            case MotionEvent.ACTION_DOWN:
294                if (mIsDoubleTapEnabled) {
295                    boolean withinFirstTapTimeout = mHandler.hasMessages(FIRST_TAP);
296                    mHandler.removeMessages(FIRST_TAP);
297
298                    if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
299                            && withinFirstTapTimeout
300                            && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
301                        mIsDoubleTapping = true;
302                        mHandler.removeMessages(SHOW_PRESS);
303                    } else {
304                        mHandler.sendEmptyMessageAtTime(FIRST_TAP, ev.getEventTime()
305                                + DOUBLE_TAP_TIMEOUT);
306                    }
307                }
308
309                setCurrentDownEvent(ev);
310                setFirstUpEvent(null);
311
312                mAlwaysInTapRegion = true;
313                mAlwaysInDoubleTapRegion = true;
314                mInLongPress = false;
315                mIsStillDown = true;
316
317                if (mIsLongPressEnabled) {
318                    mHandler.removeMessages(LONG_PRESS);
319                    mHandler.sendEmptyMessageAtTime(LONG_PRESS, ev.getDownTime() + TAP_TIMEOUT
320                            + LONGPRESS_TIMEOUT);
321                }
322
323                mHandler.sendEmptyMessageAtTime(FLICK, ev.getDownTime() + FLICK_TIMEOUT);
324                mHandler.sendEmptyMessageAtTime(SHOW_PRESS, ev.getDownTime() + TAP_TIMEOUT);
325
326                handled = true;
327
328                break;
329            case MotionEvent.ACTION_POINTER_DOWN:
330                setCurrentDownEvent(ev);
331                setFirstUpEvent(null);
332
333                // If this is a second finger, start the SLIDE_PRESS timeout.
334                if (!mAlwaysInTapRegion && ev.getPointerCount() == 2) {
335                    mHandler.sendEmptyMessageAtTime(SLIDE_TAP, ev.getEventTime() + TAP_TIMEOUT);
336                }
337
338                handled = true;
339
340                break;
341            case MotionEvent.ACTION_MOVE:
342                if (mAlwaysInTapRegion) {
343                    // We might still be in the tap region, so check pointer
344                    // distances.
345                    int distance = maxPointerDistanceSquared(ev, mCurrentDownEvent);
346                    if (distance > mTouchSlopSquare) {
347                        mAlwaysInTapRegion = false;
348                        mHandler.removeMessages(SLIDE_TAP);
349                        mHandler.removeMessages(FIRST_TAP);
350                        mHandler.removeMessages(LONG_PRESS);
351                        mHandler.removeMessages(SHOW_PRESS);
352                    }
353                    if (distance > mDoubleTapSlopSquare) {
354                        mAlwaysInDoubleTapRegion = false;
355                    }
356                    handled = true;
357                } else if (mHandler.hasMessages(FLICK)) {
358                    handled = true;
359                } else {
360                    // We're outside the tap region and there are no other
361                    // options.
362                    handled |= mListener.onMove(ev);
363                }
364                break;
365            case MotionEvent.ACTION_POINTER_UP:
366                if (mFirstUpEvent == null) {
367                    setFirstUpEvent(ev);
368                }
369
370                // If this is a second finger, check for the SLIDE_PRESS
371                // timeout.
372                if (!mAlwaysInTapRegion && ev.getPointerCount() == 2
373                        && mHandler.hasMessages(SLIDE_TAP)) {
374                    setCurrentDownEvent(ev);
375                    handled |= mListener.onSlideTap(ev);
376                } else {
377                    handled = true;
378                }
379
380                mHandler.removeMessages(SLIDE_TAP);
381
382                break;
383            case MotionEvent.ACTION_UP:
384                if (mFirstUpEvent == null) {
385                    setFirstUpEvent(ev);
386                }
387
388                if (mPreviousUpEvent != null) {
389                    mPreviousUpEvent.recycle();
390                }
391                mPreviousUpEvent = MotionEvent.obtain(ev);
392
393                // TODO(alanv): This could be replaced with a check for
394                // hasMessage(LONG_PRESS)
395                mIsStillDown = false;
396
397                if (mIsDoubleTapping) {
398                    handled |= mListener.onDoubleTap(mCurrentDownEvent);
399                    mIsDoubleTapping = false;
400                } else if (mInLongPress) {
401                    mHandler.removeMessages(FIRST_TAP);
402                    mInLongPress = false;
403                } else if (!mIsDoubleTapEnabled && mAlwaysInTapRegion) {
404                    handled |= mListener.onTap(mCurrentDownEvent);
405                } else if (!mAlwaysInTapRegion && mHandler.hasMessages(FLICK)) {
406                    mHandler.removeMessages(FIRST_TAP);
407                    handled |= mListener.onFlick(mCurrentDownEvent, mFirstUpEvent);
408                } else if (mHandler.hasMessages(FIRST_TAP)) {
409                    // If we don't have enough fingers down for a double-tap
410                    // event, just go ahead and send the tap event.
411                    if (mCurrentDownEvent.getPointerCount() < mDoubleTapMinFingers) {
412                        mHandler.removeMessages(FIRST_TAP);
413                        mListener.onTap(mCurrentDownEvent);
414                    }
415                    handled = true;
416                }
417
418                mHandler.removeMessages(FLICK);
419                mHandler.removeMessages(LONG_PRESS);
420                mHandler.removeMessages(SHOW_PRESS);
421                break;
422            case MotionEvent.ACTION_CANCEL:
423                cancel();
424                break;
425            case MotionEvent.ACTION_OUTSIDE:
426                // Consume and ignore this action.
427                handled = true;
428                break;
429        }
430
431        return handled;
432    }
433
434    private void handleTouchExploration(MotionEvent ev) {
435        // Cancel duplicate events, these are touch exploration bugs.
436        if (ev.getEventTime() == mLastEventTime) {
437            ev.setAction(MotionEvent.ACTION_OUTSIDE);
438            return;
439        }
440
441        // Adjust the pointer count on single-touch events.
442        if ((ev.getPointerCount() == 1)
443                && (MotionEventCompatUtils.getSource(ev) == InputDeviceCompatUtils.SOURCE_TOUCHSCREEN)) {
444            SimpleMultitouchGestureListener.setFakePointerCount(ev, 2);
445        }
446    }
447
448    private void setFirstUpEvent(MotionEvent ev) {
449        if (mFirstUpEvent != null) {
450            mFirstUpEvent.recycle();
451        }
452
453        if (ev != null) {
454            mFirstUpEvent = MotionEvent.obtain(ev);
455        } else {
456            mFirstUpEvent = null;
457        }
458    }
459
460    private void setCurrentDownEvent(MotionEvent ev) {
461        if (mCurrentDownEvent != null) {
462            mCurrentDownEvent.recycle();
463        }
464
465        mCurrentDownEvent = MotionEvent.obtain(ev);
466    }
467
468    private int maxPointerDistanceSquared(MotionEvent e1, MotionEvent e2) {
469        // Ensure that the events have the same number of pointers.
470        if (e1.getPointerCount() != e2.getPointerCount()) {
471            return Integer.MAX_VALUE;
472        }
473
474        int maxDistance = 0;
475        int pointerCount = e1.getPointerCount();
476
477        for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
478            // Ensure that the events have the same pointer index mapping.
479            if (e1.getPointerId(pointerIndex) != e2.getPointerId(pointerIndex)) {
480                return Integer.MAX_VALUE;
481            }
482
483            int deltaX = (int) (e1.getX(pointerIndex) - e2.getX(pointerIndex));
484            int deltaY = (int) (e1.getY(pointerIndex) - e2.getY(pointerIndex));
485            int distance = (deltaX * deltaX + deltaY * deltaY);
486
487            if (distance > maxDistance) {
488                maxDistance = distance;
489            }
490        }
491
492        return maxDistance;
493    }
494
495    private void cancel() {
496        // Remove all pending messages.
497        mHandler.removeMessages(SHOW_PRESS);
498        mHandler.removeMessages(FIRST_TAP);
499        mHandler.removeMessages(LONG_PRESS);
500        mHandler.removeMessages(FLICK);
501        
502        // Clear the velocity tracker.
503        mVelocityTracker.recycle();
504        mVelocityTracker = null;
505        
506        // Clear the current state.
507        mIsDoubleTapping = false;
508        mInLongPress = false;
509        mIsStillDown = false;
510    }
511
512    private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
513            MotionEvent secondDown) {
514        if (!mAlwaysInDoubleTapRegion) {
515            return false;
516        }
517
518        if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
519            return false;
520        }
521
522        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
523        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
524        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
525    }
526}