1 /*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.communal.ui.compose.extensions
18
19 import androidx.compose.foundation.gestures.awaitEachGesture
20 import androidx.compose.foundation.gestures.awaitFirstDown
21 import androidx.compose.foundation.gestures.waitForUpOrCancellation
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
24 import androidx.compose.ui.input.pointer.PointerEventPass
25 import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
26 import androidx.compose.ui.input.pointer.PointerInputChange
27 import androidx.compose.ui.input.pointer.PointerInputScope
28 import androidx.compose.ui.util.fastAny
29 import androidx.compose.ui.util.fastForEach
30 import kotlinx.coroutines.coroutineScope
31
32 /**
33 * Observe taps without consuming them by default, so child elements can still respond to them. Long
34 * presses are excluded.
35 */
observeTapsnull36 suspend fun PointerInputScope.observeTaps(
37 pass: PointerEventPass = PointerEventPass.Initial,
38 shouldConsume: Boolean = false,
39 onTap: ((Offset) -> Unit)? = null,
40 ) = coroutineScope {
41 if (onTap == null) return@coroutineScope
42 awaitEachGesture {
43 val down = awaitFirstDown(pass = pass)
44 if (shouldConsume) down.consume()
45 val tapTimeout = viewConfiguration.longPressTimeoutMillis
46 val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) }
47 if (up != null) {
48 onTap(up.position)
49 }
50 }
51 }
52
53 /**
54 * Detect long press gesture and calls onLongPress when detected. The callback parameter receives an
55 * Offset representing the position relative to the containing element.
56 */
detectLongPressGesturenull57 suspend fun PointerInputScope.detectLongPressGesture(
58 pass: PointerEventPass = PointerEventPass.Initial,
59 onLongPress: ((Offset) -> Unit),
60 ) = coroutineScope {
61 awaitEachGesture {
62 val down = awaitFirstDown(pass = pass)
63 val longPressTimeout = viewConfiguration.longPressTimeoutMillis
64 // wait for first tap up or long press
65 try {
66 withTimeout(longPressTimeout) { waitForUpOrCancellation(pass = pass) }
67 } catch (_: PointerEventTimeoutCancellationException) {
68 // withTimeout throws exception if timeout has passed before block completes
69 onLongPress.invoke(down.position)
70 consumeUntilUp(pass)
71 }
72 }
73 }
74
75 /**
76 * Consumes all pointer events until nothing is pressed and then returns. This method assumes that
77 * something is currently pressed.
78 */
consumeUntilUpnull79 private suspend fun AwaitPointerEventScope.consumeUntilUp(
80 pass: PointerEventPass = PointerEventPass.Initial
81 ) {
82 do {
83 val event = awaitPointerEvent(pass = pass)
84 event.changes.fastForEach { it.consume() }
85 } while (event.changes.fastAny { it.pressed })
86 }
87
88 /** Consume all gestures on the initial pass so that child elements do not receive them. */
<lambda>null89 suspend fun PointerInputScope.consumeAllGestures() = coroutineScope {
90 awaitEachGesture {
91 awaitPointerEvent(pass = PointerEventPass.Initial)
92 .changes
93 .forEach(PointerInputChange::consume)
94 }
95 }
96