Reduce tech debt in InputDispatcher and improve docs
Simplifies timeline related code and improves documentation by stripping
out obsolete references and simplifying public facing text.
Fix: 192053863
Test: ./gradlew compose:ui:ui-test:cC
Change-Id: I16a817aa4fc480580ed316ee407aeba26e9d0de7
Relnote: N/A
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 4559dc2..260a98d 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -169,6 +169,9 @@
property public abstract boolean isIdleNow;
}
+ public final class InputDispatcherKt {
+ }
+
public final class KeyInputHelpersKt {
method public static boolean performKeyPress-S8GO8FU(androidx.compose.ui.test.SemanticsNodeInteraction, android.view.KeyEvent keyEvent);
}
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index ba65508..bcbd1b9 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -180,6 +180,9 @@
property public abstract boolean isIdleNow;
}
+ public final class InputDispatcherKt {
+ }
+
@kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
}
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 4559dc2..260a98d 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -169,6 +169,9 @@
property public abstract boolean isIdleNow;
}
+ public final class InputDispatcherKt {
+ }
+
public final class KeyInputHelpersKt {
method public static boolean performKeyPress-S8GO8FU(androidx.compose.ui.test.SemanticsNodeInteraction, android.view.KeyEvent keyEvent);
}
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
index 58f0643..d28ba19 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt
@@ -35,7 +35,7 @@
root: RootForTest
): InputDispatcher {
require(root is ViewRootForTest) {
- "InputDispatcher currently only supports dispatching to ViewRootForTest, not to " +
+ "InputDispatcher only supports dispatching to ViewRootForTest, not to " +
root::class.java.simpleName
}
val view = root.view
@@ -49,12 +49,9 @@
) : InputDispatcher(testContext, root) {
private val batchLock = Any()
- // Batched events are generated just-in-time, given the "lateness" of the dispatching (see
- // sendAllSynchronous), so enqueue generators rather than instantiated events
private var batchedEvents = mutableListOf<MotionEvent>()
private var acceptEvents = true
- private var firstEventTime = Long.MAX_VALUE
- private val previousLastEventTime = partialGesture?.lastEventTime
+ private var lastEventTime = currentTime
override val now: Long get() = SystemClock.uptimeMillis()
@@ -91,7 +88,7 @@
val entries = lastPositions.entries.sortedBy { it.key }
batchMotionEvent(
downTime,
- lastEventTime,
+ currentTime,
action,
actionIndex,
List(entries.size) { entries[it].value },
@@ -121,8 +118,8 @@
"coordinates=$coordinates" +
"), events have already been (or are being) dispatched or disposed"
}
- if (firstEventTime == Long.MAX_VALUE) {
- firstEventTime = eventTime
+ if (lastEventTime == TimeNotSet) {
+ lastEventTime = eventTime
}
val positionInScreen = if (root != null) {
val array = intArrayOf(0, 0)
@@ -167,13 +164,12 @@
testContext.testOwner.runOnUiThread {
checkAndStopAcceptingEvents()
- var lastEventTime = (previousLastEventTime ?: firstEventTime)
+ var currentEventTime = lastEventTime
batchedEvents.forEach { event ->
// Before injecting the next event, pump the clock
// by the difference between this and the last event
- pumpClock(
- event.eventTime - lastEventTime.also { lastEventTime = event.eventTime }
- )
+ pumpClock(event.eventTime - currentEventTime)
+ currentEventTime = event.eventTime
sendAndRecycleEvent(event)
}
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
index d180bc8..7cf6995 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
@@ -58,24 +58,24 @@
* The receiver scope for injecting gestures on the [semanticsNode] identified by the
* corresponding [SemanticsNodeInteraction]. Gestures can be injected by calling methods defined
* on [GestureScope], such as [click] or [swipe]. The [SemanticsNodeInteraction] can be found by
- * one of the finder methods such as [ComposeTestRule.onNode].
+ * one of the finder methods such as
+ * [ComposeTestRule.onNode][androidx.compose.ui.test.junit4.ComposeTestRule.onNode].
*
* The functions in [GestureScope] can roughly be divided into two groups: full gestures and
- * partial gestures. Partial gestures are the ones that send individual touch events: [down],
- * [move], [up] and [cancel]. Full gestures are all the other functions, like [click],
- * [doubleClick], [swipe], etc. See the documentation of [down] for more information about
- * partial gestures. Normally, if you accidentally try to execute a full gesture while in the
- * middle of a partial gesture, an [IllegalStateException] or [IllegalArgumentException] will be
- * thrown. However, you might want to do this on purpose, for testing multi-touch gestures, where
- * one finger might tap the screen while another is making a gesture. In that case, make sure the
- * partial gesture uses a non-default pointer id.
+ * individual touch events. The individual touch events are: [down], [move] and friends, [up]
+ * and [cancel]. Full gestures are all the other functions, like [click], [doubleClick],
+ * [swipe], etc. See the documentation of [down] for more information about individual events. If
+ * you execute a full gesture while in the middle of another gesture, an [IllegalStateException]
+ * or [IllegalArgumentException] can be thrown when the pointerId is unintentionally used for
+ * both gestures. If you want to perform e.g. a click during a partially performed gesture, make
+ * sure they use different pointer ids.
*
* Note that all events generated by the gesture methods are batched together and sent as a whole
* after [performGesture] has executed its code block.
*
* Next to the functions, [GestureScope] also exposes several properties that allow you to get
* [coordinates][Offset] within a node, like the [top left corner][topLeft], its [center], or
- * 20% to the left of the right edge and 10% below the top edge ([percentOffset]).
+ * some percentage of the size ([percentOffset]).
*
* Example usage:
* ```
@@ -107,7 +107,7 @@
private var _semanticsNode: SemanticsNode? = node
internal val semanticsNode
get() = checkNotNull(_semanticsNode) {
- "Can't query SemanticsNode, (Partial)GestureScope has already been disposed"
+ "Can't query SemanticsNode, GestureScope has already been disposed"
}
// TODO(b/133217292): Better error: explain which gesture couldn't be performed
@@ -115,7 +115,7 @@
createInputDispatcher(testContext, checkNotNull(semanticsNode.root))
internal val inputDispatcher
get() = checkNotNull(_inputDispatcher) {
- "Can't send gesture, (Partial)GestureScope has already been disposed"
+ "Can't send gesture, GestureScope has already been disposed"
}
/**
@@ -675,11 +675,11 @@
* node. The [position] is in the node's local coordinate system, where (0, 0) is
* the top left corner of the node.
*
- * If no pointers are down yet, this will start a new partial gesture. If a partial gesture is
+ * If no pointers are down yet, this will start a new gesture. If a gesture is
* already in progress, this event is sent with at the same timestamp as the last event. If the
* given pointer is already down, an [IllegalArgumentException] will be thrown.
*
- * This gesture is considered _partial_, because the entire gesture can be spread over several
+ * Subsequent events for this or other gestures can be spread out over both this and future
* invocations of [performGesture]. An entire gesture starts with a [down][down] event,
* followed by several down, move or up events, and ends with an [up][up] or a
* [cancel][cancel] event. Movement can be expressed with [moveTo] and [moveBy] to
@@ -694,7 +694,7 @@
* cancel event will contain the up to date position of all pointers. Move and cancel events will
* advance the event time by 10 milliseconds.
*
- * Because partial gestures don't have to be defined all in the same [performGesture] block,
+ * Because gestures don't have to be defined all in the same [performGesture] block,
* keep in mind that while the gesture is not complete, all code you execute in between
* blocks that progress the gesture, will be executed while imaginary fingers are actively
* touching the screen.
@@ -715,7 +715,7 @@
* [position] is in the node's local coordinate system, where (0, 0) is the top left
* corner of the node. The default pointer has `pointerId = 0`.
*
- * If no pointers are down yet, this will start a new partial gesture. If a partial gesture is
+ * If no pointers are down yet, this will start a new gesture. If a gesture is
* already in progress, this event is sent with at the same timestamp as the last event. If the
* default pointer is already down, an [IllegalArgumentException] will be thrown.
*
@@ -839,7 +839,7 @@
}
/**
- * Sends a cancel event to cancel the current partial gesture. The cancel event contains the
+ * Sends a cancel event to cancel the current gesture. The cancel event contains the
* current position of all active pointers.
*/
fun GestureScope.cancel() {
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
index 06ac3c0..78b6fb2 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
@@ -27,6 +27,11 @@
): InputDispatcher
/**
+ * Indicates that [InputDispatcher.currentTime] is not set
+ */
+internal const val TimeNotSet = -1L
+
+/**
* Dispatcher to inject full and partial gestures. An [InputDispatcher] is created at the
* beginning of [performGesture], and disposed at the end of that method. If there is still a
* [gesture going on][isGestureInProgress] when the dispatcher is disposed, the state of the
@@ -66,27 +71,18 @@
*/
var eventPeriodMillis = 10L
internal set
-
- /**
- * Indicates that [nextDownTime] is not set
- */
- private const val DownTimeNotSet = -1L
}
/**
- * The down time of the next gesture, if a gesture will follow the one that is currently in
- * progress. If [DownTimeNotSet], the down time will be set to the current time when the
- * first down event is enqueued for the next gesture. It will only be [DownTimeNotSet] if
- * this is the first of one or more chained gestures, which is the usual case. A chained
- * gesture is for example a double click, which consists of a click, a delay and another
- * click.
+ * The eventTime of the next event. If [TimeNotSet], no gesture has been started yet, and
+ * enqueuing anything other than a down event will fail.
*/
- protected var nextDownTime = DownTimeNotSet
+ protected var currentTime = TimeNotSet
/**
* The state of the current gesture in progress. If `null`, no gesture is in progress. This
- * state contains the current position of all pointer ids, the current time of the event, and
- * whether or not pointers have moved without having enqueued the corresponding move event.
+ * state contains the current position of all pointer ids and whether or not pointers have
+ * moved without having enqueued the corresponding move event.
*/
protected var partialGesture: PartialGesture? = null
@@ -98,14 +94,14 @@
get() = partialGesture != null
/**
- * The current time, in the time scale used by gesture events.
+ * The current wall clock time, in the time scale used by gesture events.
*/
protected abstract val now: Long
init {
val state = testContext.states.remove(root)
if (state?.partialGesture != null) {
- nextDownTime = state.nextDownTime
+ currentTime = state.currentTime
partialGesture = state.partialGesture
}
}
@@ -114,49 +110,36 @@
if (root != null) {
testContext.states[root] =
InputDispatcherState(
- nextDownTime,
+ currentTime,
partialGesture
)
}
}
/**
- * Generates the downTime of the next gesture with the given [durationMillis]. The gesture's
- * [durationMillis] is necessary to facilitate chaining of gestures: if another gesture is made
- * after the next one, it will start exactly [durationMillis] after the start of the next
- * gesture. Always use this method to determine the downTime of the [down event][enqueueDown]
- * of a gesture.
+ * Returns the time to use for the next downTime. If no event has been enqueued yet, will
+ * return the current wall clock time. Otherwise, will return the
+ * [current eventTime][currentTime].
*
- * If the duration is unknown when calling this method, use a duration of zero and update
- * with [moveNextDownTime] when the duration is known, or use [moveNextDownTime]
- * incrementally if the gesture unfolds gradually.
+ * Call [increaseEventTime] each time the gesture progresses forward in time to make sure the
+ * current eventTime stays accurate.
*/
- private fun generateDownTime(durationMillis: Long): Long {
- val downTime = if (nextDownTime == DownTimeNotSet) {
- now
+ private fun generateDownTime(): Long {
+ return if (currentTime == TimeNotSet) {
+ now.also { currentTime = it }
} else {
- nextDownTime
+ currentTime
}
- nextDownTime = downTime + durationMillis
- return downTime
}
/**
- * Moves the start time of the next gesture ahead by the given [durationMillis]. Does not affect
- * any event time from the current gesture. Use this when the expected duration passed to
- * [generateDownTime] has changed.
+ * Moves the eventTime for the next event ahead by the given [durationMillis].
*/
- private fun moveNextDownTime(durationMillis: Long) {
- generateDownTime(durationMillis)
- }
-
- /**
- * Increases the eventTime with the given [time]. Also pushes the downTime for the next
- * chained gesture by the same amount to facilitate chaining.
- */
- private fun PartialGesture.increaseEventTime(time: Long = eventPeriodMillis) {
- moveNextDownTime(time)
- lastEventTime += time
+ private fun increaseEventTime(durationMillis: Long) {
+ check(currentTime != TimeNotSet) {
+ "Can't adjust current event time when no gesture is in progress."
+ }
+ currentTime += durationMillis
}
/**
@@ -313,15 +296,10 @@
}
/**
- * Adds a delay between the end of the last full or current partial gesture of the given
- * [durationMillis]. Guarantees that the first event time of the next gesture will be exactly
- * [durationMillis] later then if that gesture would be injected without this delay, provided
- * that the next gesture is started using the same [InputDispatcher] instance as the one used to
- * end the last gesture.
- *
- * Note: this does not affect the time of the next event for the _current_ partial gesture,
- * using [enqueueMove], [enqueueUp] and [enqueueCancel], but it will affect the time of the
- * _next_ gesture (including partial gestures started with [enqueueDown]).
+ * Adds an extra delay of [durationMillis] between the last and the next event. The delay is
+ * added on top of the delay that would already be added between the two events. The normal
+ * delay depends on the type of the next event: for [enqueueMove] and [enqueueCancel] move
+ * the eventTime by 10ms, and all other methods don't move the eventTime.
*
* @param durationMillis The duration of the delay. Must be positive
*/
@@ -329,7 +307,7 @@
require(durationMillis >= 0) {
"duration of a delay can only be positive, not $durationMillis"
}
- moveNextDownTime(durationMillis)
+ increaseEventTime(durationMillis)
}
/**
@@ -339,8 +317,8 @@
* is enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous] is called
* at the end of [performGesture].
*
- * It is possible to mix partial gestures with full gestures (e.g. generate a [click]
- * [enqueueClick] during a partial gesture), as long as you make sure that the default
+ * It is possible to mix partial gestures with full gestures (e.g. generate a
+ * [click][enqueueClick] during a partial gesture), as long as you make sure that the default
* pointer id (id=0) is free to be used by the full gesture.
*
* A full gesture starts with a down event at some position (with this method) that indicates
@@ -379,7 +357,7 @@
// Start a new gesture, or add the pointerId to the existing gesture
if (gesture == null) {
- gesture = PartialGesture(generateDownTime(0), position, pointerId)
+ gesture = PartialGesture(generateDownTime(), position, pointerId)
partialGesture = gesture
} else {
gesture.lastPositions[pointerId] = position
@@ -409,7 +387,7 @@
"Cannot send MOVE event with a delay of $delay ms"
}
- gesture.increaseEventTime(delay)
+ increaseEventTime(delay)
gesture.enqueueMove()
gesture.hasPointerUpdates = false
}
@@ -477,7 +455,7 @@
}
gesture.flushPointerUpdates()
- gesture.increaseEventTime(delay)
+ increaseEventTime(delay)
// First send the UP event
gesture.enqueueUp(pointerId)
@@ -512,14 +490,13 @@
"Cannot send CANCEL event with a delay of $delay ms"
}
- gesture.increaseEventTime(delay)
+ increaseEventTime(delay)
gesture.enqueueCancel()
partialGesture = null
}
/**
- * Sends all enqueued events and blocks while they are dispatched. Will suspend before
- * dispatching an event until [now] is at least that event's timestamp. If an exception is
+ * Sends all enqueued events and blocks while they are dispatched. If an exception is
* thrown during the process, all events that haven't yet been dispatched will be dropped.
*/
abstract fun sendAllSynchronous()
@@ -555,14 +532,15 @@
/**
* The state of the current gesture. Contains the current position of all pointers and the
- * current time of the gesture.
+ * down time (start time) of the gesture. Does not contain the
+ * [current time][InputDispatcher.currentTime], as the current time's lifecycle can span multiple
+ * (chained) gestures.
*
* @param downTime The time of the first down event of this gesture
* @param startPosition The position of the first down event of this gesture
* @param pointerId The pointer id of the first down event of this gesture
*/
internal class PartialGesture(val downTime: Long, startPosition: Offset, pointerId: Int) {
- var lastEventTime: Long = downTime
val lastPositions = mutableMapOf(Pair(pointerId, startPosition))
var hasPointerUpdates: Boolean = false
}
@@ -571,13 +549,14 @@
* The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored
* when the [GestureScope] is recreated.
*
- * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
- * This property will only be restored if an incomplete gesture was in progress when the
- * state of the [InputDispatcher] was saved.
+ * @param currentTime The current event time. Usually this is when the last event was injected,
+ * unless [InputDispatcher.enqueueDelay] has been used after the last event. This property will
+ * only be restored if an incomplete gesture was in progress when the state of the
+ * [InputDispatcher] was saved.
* @param partialGesture The state of an incomplete gesture. If no gesture was in progress
* when the state of the [InputDispatcher] was saved, this will be `null`.
*/
internal data class InputDispatcherState(
- val nextDownTime: Long,
+ val currentTime: Long,
val partialGesture: PartialGesture?
)
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.desktop.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.desktop.kt
index d7941b3..020c3ee 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.desktop.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.desktop.kt
@@ -65,7 +65,7 @@
}
private fun PartialGesture.pointerInputEvent(down: Boolean): List<TestPointerInputEventData> {
- val time = lastEventTime
+ val time = currentTime
val offset = lastPositions[lastPositions.keys.sorted()[0]]!!
val event = listOf(
TestPointerInputEventData(