[go: nahoru, domu]

blob: 8fc5e32579d40222875a90d27d4cb2407848c742 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.input.pointer
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastAll
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.Continuation
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.RestrictsSuspension
import kotlin.coroutines.createCoroutine
import kotlin.coroutines.resume
/**
* Receiver scope for awaiting pointer events in a call to [PointerInputScope.awaitPointerEventScope].
*
* This is a restricted suspension scope. Code in this scope is always called undispatched and
* may only suspend for calls to [awaitPointerEvent]. These functions
* resume synchronously and the caller may mutate the result **before** the next await call to
* affect the next stage of the input processing pipeline.
*/
@RestrictsSuspension
interface AwaitPointerEventScope : Density {
/**
* The measured size of the pointer input region. Input events will be reported with
* a coordinate space of (0, 0) to (size.width, size,height) as the input region, with
* (0, 0) indicating the upper left corner.
*/
val size: IntSize
/**
* The [PointerEvent] from the most recent touch event.
*/
val currentEvent: PointerEvent
/**
* The [ViewConfiguration] used to tune gesture detectors.
*/
val viewConfiguration: ViewConfiguration
/**
* Suspend until a [PointerEvent] is reported to the specified input [pass].
* [pass] defaults to [PointerEventPass.Main].
*
* [awaitPointerEvent] resumes **synchronously** in the restricted suspension scope. This
* means that callers can react immediately to input after [awaitPointerEvent] returns
* and affect both the current frame and the next handler or phase of the input processing
* pipeline. Callers should mutate the returned [PointerEvent] before awaiting
* another event to consume aspects of the event before the next stage of input processing runs.
*/
suspend fun awaitPointerEvent(
pass: PointerEventPass = PointerEventPass.Main
): PointerEvent
}
/**
* Receiver scope for [Modifier.pointerInput] that permits
* [handling pointer input][awaitPointerEventScope].
*/
// Design note: this interface does _not_ implement CoroutineScope, even though doing so
// would more easily permit the use of launch {} inside Modifier.pointerInput {} blocks without
// requiring an additional coroutineScope {} layer of nesting. As it is encouraged to define
// gesture detectors as suspending extensions with a PointerInputScope receiver, also making this
// interface implement CoroutineScope would be an invitation to break structured concurrency in
// these extensions, leaving other launched coroutines running in the calling scope.
interface PointerInputScope : Density {
/**
* The measured size of the pointer input region. Input events will be reported with
* a coordinate space of (0, 0) to (size.width, size,height) as the input region, with
* (0, 0) indicating the upper left corner.
*/
val size: IntSize
/**
* The [ViewConfiguration] used to tune gesture detectors.
*/
val viewConfiguration: ViewConfiguration
/**
* Suspend and install a pointer input [block] that can await input events and respond to
* them immediately. A call to [awaitPointerEventScope] will resume with [block]'s result after
* it completes.
*
* More than one [awaitPointerEventScope] can run concurrently in the same [PointerInputScope] by
* using [kotlinx.coroutines.launch]. [block]s are dispatched to in the order in which they
* were installed.
*/
suspend fun <R> awaitPointerEventScope(
block: suspend AwaitPointerEventScope.() -> R
): R
}
/**
* Create a modifier for processing pointer input within the region of the modified element.
*
* [pointerInput] [block]s may call [PointerInputScope.awaitPointerEventScope] to install a pointer
* input handler that can [AwaitPointerEventScope.awaitPointerEvent] to receive and consume
* pointer input events. Extension functions on [PointerInputScope] or [AwaitPointerEventScope]
* may be defined to perform higher-level gesture detection.
*/
fun Modifier.pointerInput(
block: suspend PointerInputScope.() -> Unit
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "pointerInput"
this.properties["block"] = block
}
) {
val density = AmbientDensity.current
val viewConfiguration = AmbientViewConfiguration.current
remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply {
LaunchedEffect(this) {
block()
}
}
}
private val DownChangeConsumed = ConsumedData(downChange = true)
/**
* Implementation notes:
* This class does a lot of lifting. It is both a [PointerInputModifier] and that modifier's
* own [pointerInputFilter]. It is returned by way of a [Modifier.composed] from
* the [Modifier.pointerInput] builder and is always 1-1 with an instance of application to
* a LayoutNode.
*
* [SuspendingPointerInputFilter] implements the [PointerInputScope] used to offer the
* [Modifier.pointerInput] DSL and carries the [Density] from [AmbientDensity] at the point of
* the modifier's materialization. Even if this value were returned to the [PointerInputFilter]
* callbacks, we would still need the value at composition time in order for [Modifier.pointerInput]
* to begin its internal [LaunchedEffect] for the provided code block.
*/
// TODO: Suppressing deprecation for synchronized; need to move to atomicfu wrapper
@Suppress("DEPRECATION_ERROR")
internal class SuspendingPointerInputFilter(
override val viewConfiguration: ViewConfiguration,
density: Density = Density(1f)
) : PointerInputFilter(),
PointerInputModifier,
PointerInputScope,
Density by density {
override val pointerInputFilter: PointerInputFilter
get() = this
private var currentEvent: PointerEvent? = null
override fun onInit(customEventDispatcher: CustomEventDispatcher) {
}
/**
* Actively registered input handlers from currently ongoing calls to [awaitPointerEventScope].
* Must use `synchronized(pointerHandlers)` to access.
*/
private val pointerHandlers = mutableVectorOf<PointerEventHandlerCoroutine<*>>()
/**
* Scratch list for dispatching to handlers for a particular phase.
* Used to hold a copy of the contents of [pointerHandlers] during dispatch so that
* resumed continuations may add/remove handlers without affecting the current dispatch pass.
* Must only access on the UI thread.
*/
private val dispatchingPointerHandlers = mutableVectorOf<PointerEventHandlerCoroutine<*>>()
/**
* The last pointer event we saw where at least one pointer was currently down; null otherwise.
* Used to synthesize a fake "all pointers changed to up/all changes to down-state consumed"
* event for propagating cancellation. This synthetic event corresponds to Android's
* `MotionEvent.ACTION_CANCEL`.
*/
private var lastPointerEvent: PointerEvent? = null
/**
* The size of the bounds of this input filter. Normally [PointerInputFilter.size] can
* be used, but for tests, it is better to not rely on something set to an `internal`
* method.
*/
private var boundsSize: IntSize = IntSize.Zero
/**
* Snapshot the current [pointerHandlers] and run [block] on each one.
* May not be called reentrant or concurrent with itself.
*
* Dispatches from first to last registered for [PointerEventPass.Initial] and
* [PointerEventPass.Final]; dispatches from last to first for [PointerEventPass.Main].
* This corresponds to the down/up/down dispatch behavior of each of these passes along
* the hit test path through the Compose UI layout hierarchy.
*/
private inline fun forEachCurrentPointerHandler(
pass: PointerEventPass,
block: (PointerEventHandlerCoroutine<*>) -> Unit
) {
// Copy handlers to avoid mutating the collection during dispatch
synchronized(pointerHandlers) {
dispatchingPointerHandlers.addAll(pointerHandlers)
}
try {
when (pass) {
PointerEventPass.Initial, PointerEventPass.Final ->
dispatchingPointerHandlers.forEach(block)
PointerEventPass.Main ->
dispatchingPointerHandlers.forEachReversed(block)
}
} finally {
dispatchingPointerHandlers.clear()
}
}
/**
* Dispatch [pointerEvent] for [pass] to all [pointerHandlers] currently registered when
* the call begins.
*/
private fun dispatchPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass
) {
forEachCurrentPointerHandler(pass) {
it.offerPointerEvent(pointerEvent, pass)
}
}
override fun onPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass,
bounds: IntSize
) {
boundsSize = bounds
if (pass == PointerEventPass.Initial) {
currentEvent = pointerEvent
}
dispatchPointerEvent(pointerEvent, pass)
lastPointerEvent = pointerEvent.takeIf { event ->
!event.changes.fastAll { it.changedToUpIgnoreConsumed() }
}
}
override fun onCancel() {
// Synthesize a cancel event for whatever state we previously saw, if one is applicable.
// A cancel event is one where all previously down pointers are now up, the change in
// down-ness is consumed, and we omit any pointers that previously went up entirely.
val lastEvent = lastPointerEvent ?: return
val newChanges = lastEvent.changes.mapNotNull { old ->
if (old.pressed) {
old.copy(
currentPressed = false,
previousPosition = old.position,
previousTime = old.uptimeMillis,
previousPressed = old.pressed,
consumed = DownChangeConsumed
)
} else null
}
val cancelEvent = PointerEvent(newChanges)
// Dispatch the synthetic cancel for all three passes
dispatchPointerEvent(cancelEvent, PointerEventPass.Initial)
dispatchPointerEvent(cancelEvent, PointerEventPass.Main)
dispatchPointerEvent(cancelEvent, PointerEventPass.Final)
lastPointerEvent = null
}
override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {
}
override suspend fun <R> awaitPointerEventScope(
block: suspend AwaitPointerEventScope.() -> R
): R = suspendCancellableCoroutine { continuation ->
val handlerCoroutine = PointerEventHandlerCoroutine(continuation)
synchronized(pointerHandlers) {
pointerHandlers += handlerCoroutine
// NOTE: We resume the new continuation while holding this lock.
// We do this since it runs in a RestrictsSuspension scope and therefore
// will only suspend when awaiting a new event. We don't release this
// synchronized lock until we know it has an awaiter and any future dispatch
// would succeed.
// We also create the coroutine with both a receiver and a completion continuation
// of the handlerCoroutine itself; we don't use our currently available suspended
// continuation as the resume point because handlerCoroutine needs to remove the
// ContinuationInterceptor from the supplied CoroutineContext to have undispatched
// behavior in our restricted suspension scope. This is required so that we can
// process event-awaits synchronously and affect the next stage in the pipeline
// without running too late due to dispatch.
block.createCoroutine(handlerCoroutine, handlerCoroutine).resume(Unit)
}
// Restricted suspension handler coroutines can't propagate structured job cancellation
// automatically as the context must be EmptyCoroutineContext; do it manually instead.
continuation.invokeOnCancellation { handlerCoroutine.cancel(it) }
}
/**
* Implementation of the inner coroutine created to run a single call to
* [awaitPointerEventScope].
*
* [PointerEventHandlerCoroutine] implements [AwaitPointerEventScope] to provide the
* input handler DSL, and [Continuation] so that it can wrap [completion] and remove the
* [ContinuationInterceptor] from the calling context and run undispatched.
*/
private inner class PointerEventHandlerCoroutine<R>(
private val completion: Continuation<R>,
) : AwaitPointerEventScope, Density by this@SuspendingPointerInputFilter, Continuation<R> {
private var pointerAwaiter: CancellableContinuation<PointerEvent>? = null
private var awaitPass: PointerEventPass = PointerEventPass.Main
override val currentEvent: PointerEvent
get() = checkNotNull(this@SuspendingPointerInputFilter.currentEvent) {
"cannot access currentEvent outside of input dispatch"
}
override val size: IntSize
get() = this@SuspendingPointerInputFilter.boundsSize
override val viewConfiguration: ViewConfiguration
get() = this@SuspendingPointerInputFilter.viewConfiguration
fun offerPointerEvent(event: PointerEvent, pass: PointerEventPass) {
if (pass == awaitPass) {
pointerAwaiter?.run {
pointerAwaiter = null
resume(event)
}
}
}
// Called to run any finally blocks in the awaitPointerEventScope block
fun cancel(cause: Throwable?) {
pointerAwaiter?.cancel(cause)
pointerAwaiter = null
}
// context must be EmptyCoroutineContext for restricted suspension coroutines
override val context: CoroutineContext = EmptyCoroutineContext
// Implementation of Continuation; clean up and resume our wrapped continuation.
override fun resumeWith(result: Result<R>) {
synchronized(pointerHandlers) {
pointerHandlers -= this
}
completion.resumeWith(result)
}
override suspend fun awaitPointerEvent(
pass: PointerEventPass
): PointerEvent = suspendCancellableCoroutine { continuation ->
awaitPass = pass
pointerAwaiter = continuation
}
}
}