14

I've been trying to build a tap detector that can detect both double and tripe tap. After my efforts failed I searched a long time on the net to find something ready to use but no luck! It's strange that libraries for something like this are so scarce. Any help ??

  • HAve you seen this? http://stackoverflow.com/questions/15861638/identify-triple-tap-on-custom-view – Mou Jan 03 '15 at 16:55
  • yes I have..in this example if the view is touched a tap is registered no matter what. a tap should be only registered if it's duration is –  Jan 03 '15 at 17:42

4 Answers4

43

You can try something like this.

Though I would generally recommend against using triple taps as a pattern as it is not something users are generally used to, so unless it's properly communicated to them, most might never know they can triple tap a view. Same goes for double taping actually on mobile devices, it's not always an intuitive way to interact in that environment.

view.setOnTouchListener(new View.OnTouchListener() {
    Handler handler = new Handler();

    int numberOfTaps = 0;
    long lastTapTimeMs = 0;
    long touchDownMs = 0;

    @Override
    public boolean onTouch(View v, MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchDownMs = System.currentTimeMillis();
                break;
            case MotionEvent.ACTION_UP:
                handler.removeCallbacksAndMessages(null);

                if ((System.currentTimeMillis() - touchDownMs) > ViewConfiguration.getTapTimeout()) {
                    //it was not a tap

                    numberOfTaps = 0;
                    lastTapTimeMs = 0;
                    break;
                }

                if (numberOfTaps > 0 
                        && (System.currentTimeMillis() - lastTapTimeMs) < ViewConfiguration.getDoubleTapTimeout()) {
                    numberOfTaps += 1;
                } else {
                    numberOfTaps = 1;
                }

                lastTapTimeMs = System.currentTimeMillis();

                if (numberOfTaps == 3) {
                    Toast.makeText(getApplicationContext(), "triple", Toast.LENGTH_SHORT).show();
                    //handle triple tap
                } else if (numberOfTaps == 2) {
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            //handle double tap
                            Toast.makeText(getApplicationContext(), "double", Toast.LENGTH_SHORT).show();
                        }
                    }, ViewConfiguration.getDoubleTapTimeout());
                }
        }

        return true;
    }
});
Valentin Iorgu
  • 521
  • 4
  • 5
  • Using this checkstyle plugin suggests that: onTouch should call View#performClick when a click is detected – zygimantus Feb 02 '17 at 21:07
  • Actually you need to use this line after `case MotionEvent.ACTION_UP`: `v.performClick();` – zygimantus Apr 27 '17 at 19:05
  • You should really only call currentTimeMillis() ONCE at the beginning of your method, set it to a variable, and use it throughout. Calling it multiple times may result in slightly different values each time time you use it and lead to subtle bugs – Michael Peterson Aug 14 '17 at 13:34
  • This was helpful, but note that `getTapTimeout()` is not intended as a maximum duration for a tap, but rather "the duration in milliseconds we will wait to see if a touch event is a tap or a scroll. If the user does not move within this interval, it is considered to be a tap." (quoted from https://developer.android.com/reference/android/view/ViewConfiguration.html#getTapTimeout() ). I found that the code above made it difficult to reliably detect taps because `getTapTimeout()` is too small to use as maximum tap duration. Using `getLongPressTimeout()` works much better for me. – James Jul 24 '18 at 00:16
5

Here is a Kotlin implementation that can detect an arbitrary number of taps, and respects the various timeout and slop parameters found in the ViewConfiguration class. I have tried to minimise heap allocations in the event handlers.

import android.os.Handler
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import kotlin.math.abs

/*
 * Detects an arbitrary number of taps in rapid succession
 *
 * The passed callback will be called for each tap, with two parameters:
 *  - the number of taps detected in rapid succession so far
 *  - a boolean flag indicating whether this is last tap of the sequence
 */
class MultiTapDetector(view: View, callback: (Int, Boolean) -> Unit) {
    private var numberOfTaps = 0
    private val handler = Handler()

    private val doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout().toLong()
    private val tapTimeout = ViewConfiguration.getTapTimeout().toLong()
    private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong()

    private val viewConfig = ViewConfiguration.get(view.context)

    private var downEvent = Event()
    private var lastTapUpEvent = Event()

    data class Event(var time: Long = 0, var x: Float = 0f, var y: Float = 0f) {
        fun copyFrom(motionEvent: MotionEvent) {
            time = motionEvent.eventTime
            x = motionEvent.x
            y = motionEvent.y
        }

        fun clear() {
            time = 0
        }
    }


    init {
         view.setOnTouchListener { v, event ->
             when(event.action) {
                 MotionEvent.ACTION_DOWN -> {
                     if(event.pointerCount == 1) {
                         downEvent.copyFrom(event)
                     } else {
                         downEvent.clear()
                     }
                 }
                 MotionEvent.ACTION_MOVE -> {
                     // If a move greater than the allowed slop happens before timeout, then this is a scroll and not a tap
                     if(event.eventTime - event.downTime < tapTimeout
                             && abs(event.x - downEvent.x) > viewConfig.scaledTouchSlop
                             && abs(event.y - downEvent.y) > viewConfig.scaledTouchSlop) {
                         downEvent.clear()
                     }
                 }
                 MotionEvent.ACTION_UP -> {
                     val downEvent = this.downEvent
                     val lastTapUpEvent = this.lastTapUpEvent

                     if(downEvent.time > 0 && event.eventTime - event.downTime < longPressTimeout) {
                         // We have a tap
                         if(lastTapUpEvent.time > 0
                                 && event.eventTime - lastTapUpEvent.time < doubleTapTimeout
                                 && abs(event.x - lastTapUpEvent.x) < viewConfig.scaledDoubleTapSlop
                                 && abs(event.y - lastTapUpEvent.y) < viewConfig.scaledDoubleTapSlop) {
                             // Double tap
                             numberOfTaps++
                         } else {
                             numberOfTaps = 1
                         }
                         this.lastTapUpEvent.copyFrom(event)

                         // Send event
                         val taps = numberOfTaps
                         handler.postDelayed({
                             // When this callback runs, we know if it is the final tap of a sequence
                             // if the number of taps has not changed
                             callback(taps, taps == numberOfTaps)
                         }, doubleTapTimeout)
                     }
                 }
             }
             true
         }
     }
}
James
  • 3,597
  • 2
  • 39
  • 38
3

I developed an advanced version of the Iorgu solition that suits better my needs:

public abstract class OnTouchMultipleTapListener implements View.OnTouchListener {
    Handler handler = new Handler();

    private boolean manageInActionDown;
    private float tapTimeoutMultiplier;

    private int numberOfTaps = 0;
    private long lastTapTimeMs = 0;
    private long touchDownMs = 0;


    public OnTouchMultipleTapListener() {
        this(false, 1);
    }


    public OnTouchMultipleTapListener(boolean manageInActionDown, float tapTimeoutMultiplier) {
        this.manageInActionDown = manageInActionDown;
        this.tapTimeoutMultiplier = tapTimeoutMultiplier;
    }

    /**
     *
     * @param e
     * @param numberOfTaps
     */
    public abstract void onMultipleTapEvent(MotionEvent e, int numberOfTaps);


    @Override
    public final boolean onTouch(View v, final MotionEvent event) {
        if (manageInActionDown) {
            onTouchDownManagement(v, event);
        } else {
            onTouchUpManagement(v, event);
        }
        return true;
    }


    private void onTouchDownManagement(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchDownMs = System.currentTimeMillis();

                handler.removeCallbacksAndMessages(null);

                if (numberOfTaps > 0 && (System.currentTimeMillis() - lastTapTimeMs) < ViewConfiguration.getTapTimeout() * tapTimeoutMultiplier) {
                    numberOfTaps += 1;
                } else {
                    numberOfTaps = 1;
                }

                lastTapTimeMs = System.currentTimeMillis();

                if (numberOfTaps > 0) {
                    final MotionEvent finalMotionEvent = MotionEvent.obtain(event); // to avoid side effects
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            onMultipleTapEvent(finalMotionEvent, numberOfTaps);
                        }
                    }, (long) (ViewConfiguration.getDoubleTapTimeout() * tapTimeoutMultiplier));
                }
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
    }


    private void onTouchUpManagement(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchDownMs = System.currentTimeMillis();
                break;
            case MotionEvent.ACTION_UP:
                handler.removeCallbacksAndMessages(null);

                if ((System.currentTimeMillis() - touchDownMs) > ViewConfiguration.getTapTimeout()) {
                    numberOfTaps = 0;
                    lastTapTimeMs = 0;
                    break;
                }

                if (numberOfTaps > 0 && (System.currentTimeMillis() - lastTapTimeMs) < ViewConfiguration.getDoubleTapTimeout()) {
                    numberOfTaps += 1;
                } else {
                    numberOfTaps = 1;
                }

                lastTapTimeMs = System.currentTimeMillis();

                if (numberOfTaps > 0) {
                    final MotionEvent finalMotionEvent = MotionEvent.obtain(event); // to avoid side effects
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            onMultipleTapEvent(finalMotionEvent, numberOfTaps);
                        }
                    }, ViewConfiguration.getDoubleTapTimeout());
                }
        }
    }

}
carlol
  • 2,220
  • 1
  • 16
  • 11
-1

Use the view listener to detect first tap on the view object,then see how to manage twice back pressed to exit an activity on stackoverflow.com (use a handler post delay).

Clicking the back button twice to exit an activity

Community
  • 1
  • 1
Frédéric
  • 77
  • 2
  • that has nothing to do with what i asked –  Jan 03 '15 at 17:15
  • its called logic, that's what he was relating you to, and he was also trying to help @user2484359 .. this would have been a comment if he had enough reps – Elltz Jan 03 '15 at 18:41