detectTapGestures
suspend fun PointerInputScope.detectTapGestures(
onDoubleTap: ((Offset) -> Unit)? = null,
onLongPress: ((Offset) -> Unit)? = null,
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
onTap: ((Offset) -> Unit)? = null,
) = coroutineScope {
val pressScope = PressGestureScopeImpl(this@detectTapGestures)
awaitEachGesture {
val down = awaitFirstDown()
down.consume()
var resetJob =
launch(start = coroutineStartForCurrentDispatchBehavior) { pressScope.reset() }
if (onPress !== NoPressGesture)
launchAwaitingReset(resetJob) { pressScope.onPress(down.position) }
val upOrCancel: PointerInputChange?
val cancelOrReleaseJob: Job?
if (onLongPress == null) {
upOrCancel = waitForUpOrCancellation()
} else {
upOrCancel =
when (val longPressResult = waitForLongPress()) {
LongPressResult.Success -> {
onLongPress.invoke(down.position)
consumeUntilUp()
launchAwaitingReset(resetJob) { pressScope.release() }
return@awaitEachGesture
}
is LongPressResult.Released -> longPressResult.finalUpChange
is LongPressResult.Canceled -> null
}
}
if (upOrCancel == null) {
cancelOrReleaseJob =
launchAwaitingReset(resetJob) {
pressScope.cancel()
}
} else {
upOrCancel.consume()
cancelOrReleaseJob = launchAwaitingReset(resetJob) { pressScope.release() }
}
if (upOrCancel != null) {
if (onDoubleTap == null) {
onTap?.invoke(upOrCancel.position) // no need to check for double-tap.
} else {
val secondDown = awaitSecondDown(upOrCancel)
if (secondDown == null) {
onTap?.invoke(upOrCancel.position) // no valid second tap started
} else {
resetJob =
launch(start = coroutineStartForCurrentDispatchBehavior) {
cancelOrReleaseJob.join()
pressScope.reset()
}
if (onPress !== NoPressGesture) {
launchAwaitingReset(resetJob) { pressScope.onPress(secondDown.position) }
}
val secondUp =
if (onLongPress == null) {
waitForUpOrCancellation()
} else {
when (val longPressResult = waitForLongPress()) {
LongPressResult.Success -> {
onLongPress.invoke(secondDown.position)
consumeUntilUp()
launchAwaitingReset(resetJob) { pressScope.release() }
return@awaitEachGesture
}
is LongPressResult.Released -> longPressResult.finalUpChange
is LongPressResult.Canceled -> null
}
}
if (secondUp != null) {
secondUp.consume()
launchAwaitingReset(resetJob) { pressScope.release() }
onDoubleTap(secondUp.position)
} else {
launchAwaitingReset(resetJob) { pressScope.cancel() }
onTap?.invoke(upOrCancel.position)
}
}
}
}
}
}
Detects tap, double-tap, and long press gestures and calls onTap
, onDoubleTap
, and
onLongPress
, respectively, when detected. onPress
is called when the press is detected and
the PressGestureScope.tryAwaitRelease
and PressGestureScope.awaitRelease
can be used to
detect when pointers have released or the gesture was canceled. The first pointer down and final
pointer up are consumed, and in the case of long press, all changes after the long press is
detected are consumed.
Each function parameter receives an Offset
representing the position relative to the containing
element. The Offset
can be outside the actual bounds of the element itself meaning the numbers
can be negative or larger than the element bounds if the touch target is smaller than the
ViewConfiguration.minimumTouchTargetSize
.
When onDoubleTap
is provided, the tap gesture is detected only after the
ViewConfiguration.doubleTapMinTimeMillis
has passed and onDoubleTap
is called if the second
tap is started before ViewConfiguration.doubleTapTimeoutMillis
. If onDoubleTap
is not
provided, then onTap
is called when the pointer up has been received.
After the initial onPress
, if the pointer moves out of the input area, the position change is
consumed, or another gesture consumes the down or up events, the gestures are considered
canceled. That means onDoubleTap
, onLongPress
, and onTap
will not be called after a gesture
has been canceled.
If the first down event is consumed somewhere else, the entire gesture will be skipped, including
onPress
.