How to implement a two-finger double-click in Android?

WOUNDEDStevenJones picture WOUNDEDStevenJones · Sep 13, 2012 · Viewed 9k times · Source

I know how to detect a double-click and a two-finger touch event, but how can I combine these to react so somebody needs to double click with two fingers?

By default, Android has the long press to act as a second form of clicking, but I'm specifically looking for a two-finger double-click.

Answer

Sam picture Sam · Sep 24, 2012

I wanted a simple and reusable interface that listens for two finger double taps and behaves like GestureDetector. So that you could use it like this (all cut & paste runnable code):

public class Example extends Activity {
    SimpleTwoFingerDoubleTapDetector multiTouchListener = new SimpleTwoFingerDoubleTapDetector() {
        @Override
        public void onTwoFingerDoubleTap() {
            // Do what you want here, I used a Toast for demonstration
            Toast.makeText(Example.this, "Two Finger Double Tap", Toast.LENGTH_SHORT).show();
        }
    };

    // Override onCreate() and anything else you want

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(multiTouchListener.onTouchEvent(event))
            return true;
        return super.onTouchEvent(event);
    }
}

I created SimpleTwoFingerDoubleTapDetector. (It's a long name, but it is descriptive. You can rename it as anything you want.) Save this new file inside your project or as a library:

public abstract class SimpleTwoFingerDoubleTapDetector {
    private static final int TIMEOUT = ViewConfiguration.getDoubleTapTimeout() + 100;
    private long mFirstDownTime = 0;
    private boolean mSeparateTouches = false;
    private byte mTwoFingerTapCount = 0;

    private void reset(long time) {
        mFirstDownTime = time;
        mSeparateTouches = false;
        mTwoFingerTapCount = 0;
    }

    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            if(mFirstDownTime == 0 || event.getEventTime() - mFirstDownTime > TIMEOUT) 
                reset(event.getDownTime());
            break;
        case MotionEvent.ACTION_POINTER_UP:
            if(event.getPointerCount() == 2)  
                mTwoFingerTapCount++;
            else 
                mFirstDownTime = 0;
            break;
        case MotionEvent.ACTION_UP:
            if(!mSeparateTouches)
                mSeparateTouches = true;
            else if(mTwoFingerTapCount == 2 && event.getEventTime() - mFirstDownTime < TIMEOUT) {
                onTwoFingerDoubleTap();
                mFirstDownTime = 0;
                return true;
            }
        }               

        return false;
    }

    public abstract void onTwoFingerDoubleTap();
}

First, a few notes about Android (one-touch) GestureDetector:

  • Android's onDoubleTap() event uses a standard timeout value from ViewConfiguration. I refer to the same time.
  • They measure the elapsed time from the first tap's finger-down event to the second tap's finger-down event, and then broadcast onDoubleTap() and onDoubleTapEvent().
    • onDoubleTap() is fired only when the second tap's finger-down event occurs.
    • onDoubleTapEvent() is fired for every action by the second tap: down, move, and up.

A few notes on SimpleTwoFingerDoubleTapDetector:

  • My timeout is measured from the first finger-down event to the last finger-up event to prevent false double-tap notifications. I added a little extra time to the default ViewConfiguration double tap timeout to account for this.
  • Android's GestureDetector measures slop (how far apart the two taps are). I didn't see the need for this here, nor did I check the distance between the two fingers on each tap.
  • I only broadcast one event onTwoFingerDoubleTap().

Final note: You can easily change this to behave like an OnTouchListener:

  1. Change SimpleTwoFingerDoubleTapDetector's definition:

    public abstract class SimpleTwoFingerDoubleTapListener implements OnTouchListener {
    
  2. Add a new class variable:

    private View mFirstView;
    
  3. Change the ACTION_DOWN case:

    case MotionEvent.ACTION_DOWN:
        if(mFirstDownTime == -1 || mFirstView != v || hasTimedOut(event.getEventTime())) {
            mFirstView = v;
            reset(event.getDownTime());
        }
        break;
    
  4. Pass mFirstView inside the ACTION_UP case:

    onTwoFingerDoubleTap(mFirstView);
    
  5. Last, change the onTwoFingerDoubleTap() method to reflect which View was tapped:

    public abstract void onTwoFingerDoubleTap(View v);