Home | History | Annotate | Download | only in chromoting
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.chromoting;
      6 
      7 import android.content.Context;
      8 import android.graphics.PointF;
      9 import android.os.Handler;
     10 import android.os.Message;
     11 import android.util.SparseArray;
     12 import android.view.MotionEvent;
     13 import android.view.ViewConfiguration;
     14 
     15 /**
     16  * This class detects multi-finger tap and long-press events. This is provided since the stock
     17  * Android gesture-detectors only detect taps/long-presses made with one finger.
     18  */
     19 public class TapGestureDetector {
     20     /** The listener for receiving notifications of tap gestures. */
     21     public interface OnTapListener {
     22         /**
     23          * Notified when a tap event occurs.
     24          *
     25          * @param pointerCount The number of fingers that were tapped.
     26          * @return True if the event is consumed.
     27          */
     28         boolean onTap(int pointerCount);
     29 
     30         /**
     31          * Notified when a long-touch event occurs.
     32          *
     33          * @param pointerCount The number of fingers held down.
     34          */
     35         void onLongPress(int pointerCount);
     36     }
     37 
     38     /** The listener to which notifications are sent. */
     39     private OnTapListener mListener;
     40 
     41     /** Handler used for posting tasks to be executed in the future. */
     42     private Handler mHandler;
     43 
     44     /** The maximum number of fingers seen in the gesture. */
     45     private int mPointerCount = 0;
     46 
     47     /**
     48      * Stores the location of each down MotionEvent (by pointer ID), for detecting motion of any
     49      * pointer beyond the TouchSlop region.
     50      */
     51     private SparseArray<PointF> mInitialPositions = new SparseArray<PointF>();
     52 
     53     /**
     54      * Threshold squared-distance, in pixels, to use for motion-detection. If a finger moves less
     55      * than this distance, the gesture is still eligible to be a tap event.
     56      */
     57     private int mTouchSlopSquare;
     58 
     59     /** Set to true whenever motion is detected in the gesture, or a long-touch is triggered. */
     60     private boolean mTapCancelled = false;
     61 
     62     private class EventHandler extends Handler {
     63         @Override
     64         public void handleMessage(Message message) {
     65             mListener.onLongPress(mPointerCount);
     66             mTapCancelled = true;
     67         }
     68     }
     69 
     70     public TapGestureDetector(Context context, OnTapListener listener) {
     71         mListener = listener;
     72         mHandler = new EventHandler();
     73         ViewConfiguration config = ViewConfiguration.get(context);
     74         int touchSlop = config.getScaledTouchSlop();
     75         mTouchSlopSquare = touchSlop * touchSlop;
     76     }
     77 
     78     /** Analyzes the touch event to determine whether to notify the listener. */
     79     public boolean onTouchEvent(MotionEvent event) {
     80         boolean handled = false;
     81         switch (event.getActionMasked()) {
     82             case MotionEvent.ACTION_DOWN:
     83                 reset();
     84                 // Cause a long-press notification to be triggered after the timeout.
     85                 mHandler.sendEmptyMessageDelayed(0, ViewConfiguration.getLongPressTimeout());
     86                 trackDownEvent(event);
     87                 mPointerCount = 1;
     88                 break;
     89 
     90             case MotionEvent.ACTION_POINTER_DOWN:
     91                 trackDownEvent(event);
     92                 mPointerCount = Math.max(mPointerCount, event.getPointerCount());
     93                 break;
     94 
     95             case MotionEvent.ACTION_MOVE:
     96                 if (!mTapCancelled) {
     97                     if (trackMoveEvent(event)) {
     98                         cancelLongTouchNotification();
     99                         mTapCancelled = true;
    100                     }
    101                 }
    102                 break;
    103 
    104             case MotionEvent.ACTION_UP:
    105                 cancelLongTouchNotification();
    106                 if (!mTapCancelled) {
    107                     handled = mListener.onTap(mPointerCount);
    108                 }
    109                 break;
    110 
    111             case MotionEvent.ACTION_POINTER_UP:
    112                 cancelLongTouchNotification();
    113                 trackUpEvent(event);
    114                 break;
    115 
    116             case MotionEvent.ACTION_CANCEL:
    117                 cancelLongTouchNotification();
    118                 break;
    119 
    120             default:
    121                 break;
    122         }
    123         return handled;
    124     }
    125 
    126     /** Stores the location of the ACTION_DOWN or ACTION_POINTER_DOWN event. */
    127     private void trackDownEvent(MotionEvent event) {
    128         int pointerIndex = 0;
    129         if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
    130             pointerIndex = event.getActionIndex();
    131         }
    132         int pointerId = event.getPointerId(pointerIndex);
    133         mInitialPositions.put(pointerId,
    134                 new PointF(event.getX(pointerIndex), event.getY(pointerIndex)));
    135     }
    136 
    137     /** Removes the ACTION_UP or ACTION_POINTER_UP event from the stored list. */
    138     private void trackUpEvent(MotionEvent event) {
    139         int pointerIndex = 0;
    140         if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
    141             pointerIndex = event.getActionIndex();
    142         }
    143         int pointerId = event.getPointerId(pointerIndex);
    144         mInitialPositions.remove(pointerId);
    145     }
    146 
    147     /**
    148      * Processes an ACTION_MOVE event and returns whether a pointer moved beyond the TouchSlop
    149      * threshold.
    150      *
    151      * @return True if motion was detected.
    152      */
    153     private boolean trackMoveEvent(MotionEvent event) {
    154         int pointerCount = event.getPointerCount();
    155         for (int i = 0; i < pointerCount; i++) {
    156             int pointerId = event.getPointerId(i);
    157             float currentX = event.getX(i);
    158             float currentY = event.getY(i);
    159             PointF downPoint = mInitialPositions.get(pointerId);
    160             if (downPoint == null) {
    161                 // There was no corresponding DOWN event, so add it. This is an inconsistency
    162                 // which shouldn't normally occur.
    163                 mInitialPositions.put(pointerId, new PointF(currentX, currentY));
    164                 continue;
    165             }
    166             float deltaX = currentX - downPoint.x;
    167             float deltaY = currentY - downPoint.y;
    168             if (deltaX * deltaX + deltaY * deltaY > mTouchSlopSquare) {
    169                 return true;
    170             }
    171         }
    172         return false;
    173     }
    174 
    175     /** Cleans up any stored data for the gesture. */
    176     private void reset() {
    177         cancelLongTouchNotification();
    178         mPointerCount = 0;
    179         mInitialPositions.clear();
    180         mTapCancelled = false;
    181     }
    182 
    183     /** Cancels any pending long-touch notifications from the message-queue. */
    184     private void cancelLongTouchNotification() {
    185         mHandler.removeMessages(0);
    186     }
    187 }
    188