Reputation: 414
I have a composable function as shown below and I want to perform a unique action when a tap gesture is detected: for instance,
But everytime I double tap, the Press function gets called as well, and I get a log like this:
- I am press
- I am press
- Ć am Double Tapped
Box(modifier = modifier.fillMaxSize()) {
Column(
modifier = modifier
.size(200.dp)
.align(Alignment.Center)
.pointerInput(interactionSource) {
detectTapGestures(
onPress = {
Timber.d("I am press")
},
onDoubleTap = {
Timber.d("Ć am Double Tapped")
},
)
}){
Icon(
modifier = modifier.size(100.dp),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_launcher),
tint = Color.White,
contentDescription = "Press to detect"
)
}
Upvotes: 3
Views: 4907
Reputation: 28839
In my case I had this:
val interactionSource = remember { MutableInteractionSource() }
AsyncImage(
modifier = modifier
// 1) Either this listener...
.combinedClickable(
interactionSource = interactionSource,
indication = null,
onClick = { },
onDoubleClick = { ... },
)
// 2) ... or this
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = { ... }
)
}
.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown()
// ACTION_DOWN here
do {
//This PointerEvent contains details including
// event, id, position and more
val event: PointerEvent = awaitPointerEvent()
// ACTION_MOVE loop
...
// Consuming event prevents other gestures or scroll to intercept
// --- Comment 3 line below to make onDoubleClick work ---
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consume()
}
} while (event.changes.any { it.pressed })
// ACTION_UP is here
}
},
onDoubleClick
didn't receive events. Then I removed 3 lines that were meant in the code.
Upvotes: 2
Reputation: 67149
You need to write your own tap, press gesture for this kind of behavior. Because onPress
is invoked at first awaitFirstDown
and followed by tap even if you move your finger inside the composable and lift it inside it no matter how much time has passed. Default tap is not actually a tap rather an onUp
gesture compared to onTouchEvent
for Views. Tap does not get invoked after the time out threshold if you also observe onLongPress
. After the threshold onLongPress is invoked instead of tap.
If you observe onDoubleTap
, tap changes again. It's not invoked if it's a double tap.
onPress
-> onDoubleTap
events get invoked this time. Also another exception here, if second tap is long press then long press is invoked if that's available.
In the source code below as you can see
suspend fun PointerInputScope.detectTapGestures(
onDoubleTap: ((Offset) -> Unit)? = null,
onLongPress: ((Offset) -> Unit)? = null,
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
onTap: ((Offset) -> Unit)? = null
) = coroutineScope {
// special signal to indicate to the sending side that it shouldn't intercept and consume
// cancel/up events as we're only require down events
val pressScope = PressGestureScopeImpl(this@detectTapGestures)
awaitEachGesture {
// š„ This is first contact of First Pointer
val down = awaitFirstDown()
down.consume()
launch {
pressScope.reset()
}
// š„ onPress is invoked here before further checks
if (onPress !== NoPressGesture) launch {
pressScope.onPress(down.position)
}
val longPressTimeout = onLongPress?.let {
viewConfiguration.longPressTimeoutMillis
} ?: (Long.MAX_VALUE / 2)
var upOrCancel: PointerInputChange? = null
try {
// wait for first tap up or long press
upOrCancel = withTimeout(longPressTimeout) {
waitForUpOrCancellation()
}
if (upOrCancel == null) {
launch {
pressScope.cancel() // tap-up was canceled
}
} else {
upOrCancel.consume()
launch {
pressScope.release()
}
}
} catch (_: PointerEventTimeoutCancellationException) {
onLongPress?.invoke(down.position)
consumeUntilUp()
launch {
pressScope.release()
}
}
if (upOrCancel != null) {
// tap was successful.
if (onDoubleTap == null) {
onTap?.invoke(upOrCancel.position) // no need to check for double-tap.
} else {
// check for second tap
val secondDown = awaitSecondDown(upOrCancel)
if (secondDown == null) {
onTap?.invoke(upOrCancel.position) // no valid second tap started
} else {
// Second tap down detected
launch {
pressScope.reset()
}
if (onPress !== NoPressGesture) {
launch { pressScope.onPress(secondDown.position) }
}
try {
// Might have a long second press as the second tap
withTimeout(longPressTimeout) {
val secondUp = waitForUpOrCancellation()
if (secondUp != null) {
secondUp.consume()
launch {
pressScope.release()
}
onDoubleTap(secondUp.position)
} else {
launch {
pressScope.cancel()
}
onTap?.invoke(upOrCancel.position)
}
}
} catch (e: PointerEventTimeoutCancellationException) {
// The first tap was valid, but the second tap is a long press.
// notify for the first tap
onTap?.invoke(upOrCancel.position)
// notify for the long press
onLongPress?.invoke(secondDown.position)
consumeUntilUp()
launch {
pressScope.release()
}
}
}
}
}
}
}
Upvotes: 3
Reputation: 93668
That's expected. Taps come in immediately. So it will send the tap message on the first tap, then again when the second happens, then it will figure it was a double tap. There's two ways to fix this. The first is to make it so that the tap does X, and the double tap does X and Y. Then you can do X in onTap, and Y in onDoubleTap. The other is not to use onDoubleTap. Set a timer in onTap for as long as you think a double tap should take. Wait for the timer to finish. If you see a second tap by then, its a double tap. Otherwise it's a single. Note that this will make your app appear less responsive, as you have to wait after single taps to see if its part of a double.
Upvotes: 1