Status Update
Comments
ma...@google.com <ma...@google.com> #2
Thanks for a detailed issue overview.
This is by design. We've designed detect*
methods as a standalone, non combinable methods to be used for higher levels. You still can combine some of them that make sense together using multiply instances of Modifier.pointerInput
modifiers.
You were absolutely right when you defined a bunch of your own extension functions on a scope; It's the intended way for developers to implement more sophisticated or domain specific gestures. We opened building blocks for detect*
methods that might be hard to write 100% correct, such as drag
function or forEachGesture
.
Let me know if you are missing a particular building block that is hard to write yourself.
In the meantime, Im' closing the issue as WAI, thanks!
da...@danielzfranklin.org <da...@danielzfranklin.org> #3
Thank you for going into depth on your thought processes. I think awaitLongPressOrCancellation
would be a useful building block to open up.
Copying awaitLongPressOrCancellation
and it's private/internal dependencies to my project led to me copying about 40 lines of code (excluding comments/blank). (In comparison, if forEachGesture
was private, I would have had to copy about 20 lines to use it.)
If I hadn't seen awaitLongPressOrCancellation
's implementation while reading the source code of another function I suspect I would have incorrectly assumed I could solve the problem with a simpler approach, and had to figure out some bugs (such as around pointer events out of bounds).
Here's what I copied into my project:
// Copied from Compose
private suspend fun PointerInputScope.awaitLongPressOrCancellation(
initialDown: PointerInputChange
): PointerInputChange? {
var longPress: PointerInputChange? = null
var currentDown = initialDown
val longPressTimeout = viewConfiguration.longPressTimeoutMillis
return try {
// wait for first tap up or long press
withTimeout(longPressTimeout) {
awaitPointerEventScope {
var finished = false
while (!finished) {
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) {
// All pointers are up
finished = true
}
if (
event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }
) {
finished = true // Canceled
}
// Check for cancel by position consumption. We can look on the Final pass of
// the existing pointer event because it comes after the Main pass we checked
// above.
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) {
finished = true
}
if (!event.isPointerUp(currentDown.id)) {
longPress = event.changes.firstOrNull { it.id == currentDown.id }
} else {
val newPressed = event.changes.fastFirstOrNull { it.pressed }
if (newPressed != null) {
currentDown = newPressed
longPress = currentDown
} else {
// should technically never happen as we checked it above
finished = true
}
}
}
}
}
null
} catch (_: TimeoutCancellationException) {
longPress ?: initialDown
}
}
// Copied from Compose
private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
changes.firstOrNull { it.id == pointerId }?.pressed != true
ma...@google.com <ma...@google.com> #4
Filed awaitLongPressOrCancellation
separately, thanks!
ap...@google.com <ap...@google.com> #5
Branch: androidx-main
commit 4192f52e326cf6e81e6e30202ff00496743b6af1
Author: Jeremy Walker <jewalker@google.com>
Date: Wed Aug 10 15:18:18 2022
Exposing long press await fun as building block for gesture detection.
Bug: 181577176
Test: Manually tested.
Relnote: "Exposed `AwaitPointerEventScope#awaitLongPressOrCancellation` as additional building block for more complex gesture detection"
Change-Id: I04374246211122f86e1235a82a98fa4484381e69
M compose/foundation/foundation/api/public_plus_experimental_current.txt
M compose/foundation/foundation/api/current.txt
M compose/foundation/foundation/api/restricted_current.txt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
je...@google.com <je...@google.com> #6
awaitLongPressOrCancellation()
is now public per
Description
I had code that required detecting a number of different gestures that were possible only in certain states. I started off using the gesture detecting functions and global variables like so:
I eventually traced a number of bugs down to updating the state non-atomically, so I was planning to add a mutex. That reminded me of how coroutines can often be used to solve concurrency issues, and I came up with
I defined a bunch of my own functions on
PointerInputScope
, and the approach solved my concurrency issues in a relatively clean way.The problem is that most of the framework detectors block forever, so they can't be combined. I ended up copying a framework function, editing it to remove the line
forEachGesture {
, and copying over all their private/internal dependencies.I'm suggesting that framework gesture detector functions should (when it makes sense) delegate to another public function that detects only one gesture.
I'm also suggesting that more of the private helpers you use in gesture detection be made public, such as
awaitLongPressOrCancellation
, to enable users to write code that combines small suspending functions to detect complex gestures as cleanly as the framework.