Skip to content

Instantly share code, notes, and snippets.

@dees91
Created January 21, 2022 14:57
Show Gist options
  • Select an option

  • Save dees91/d9c26d1365592de8ec47a168bfaa358b to your computer and use it in GitHub Desktop.

Select an option

Save dees91/d9c26d1365592de8ec47a168bfaa358b to your computer and use it in GitHub Desktop.
MultiTapDetector
MultiTapDetector(someView) { number, isFinal ->
}
import android.annotation.SuppressLint
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
*/
@SuppressLint("ClickableViewAccessibility")
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
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment