Android - Detect doubletap AND tripletap on view
Asked Answered
R

4

16

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 ??

Robyn answered 3/1, 2015 at 16:45 Comment(2)
HAve you seen this? #15862138Baird
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 <TAP_THRESHOLD just like it happens with the android native double tap functionality. thanks for your suggestion thoughRobyn
N
44

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;
    }
});
Nerland answered 3/1, 2015 at 18:11 Comment(4)
Using this checkstyle plugin suggests that: onTouch should call View#performClick when a click is detectedXeres
Actually you need to use this line after case MotionEvent.ACTION_UP: v.performClick();Xeres
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 bugsFoetus
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 developer.android.com/reference/android/view/… ). 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.Ellingston
E
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
         }
     }
}
Ellingston answered 24/7, 2018 at 1:6 Comment(0)
D
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());
                }
        }
    }

}
Dungeon answered 5/12, 2016 at 11:57 Comment(0)
L
-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

Lavalley answered 3/1, 2015 at 17:3 Comment(2)
that has nothing to do with what i askedRobyn
its called logic, that's what he was relating you to, and he was also trying to help @Robyn .. this would have been a comment if he had enough repsMic

© 2022 - 2024 — McMap. All rights reserved.