1 /*
<lambda>null2 * Copyright 2019 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 androidx.compose.ui.benchmark.input.pointer
18
19 import android.view.InputDevice.SOURCE_TOUCHSCREEN
20 import android.view.MotionEvent
21 import android.view.MotionEvent.PointerCoords
22 import android.view.MotionEvent.PointerProperties
23 import android.view.View
24
25 internal const val DefaultPointerInputTimeDelta = 100
26 internal const val DefaultPointerInputMoveAmountPx = 10f
27
28 internal data class BenchmarkSimplifiedPointerInputPointer(val id: Int, val x: Float, val y: Float)
29
30 /**
31 * Creates a simple [MotionEvent].
32 *
33 * @param dispatchTarget The [View] that the [MotionEvent] is going to be dispatched to. This
34 * guarantees that the MotionEvent is created correctly for both Compose (which relies on raw
35 * coordinates being correct) and Android (which requires that local coordinates are correct).
36 */
37 internal fun MotionEvent(
38 eventTime: Int,
39 action: Int,
40 numPointers: Int,
41 actionIndex: Int,
42 pointerProperties: Array<MotionEvent.PointerProperties>,
43 pointerCoords: Array<MotionEvent.PointerCoords>,
44 dispatchTarget: View
45 ): MotionEvent {
46
47 // It's important we get the absolute coordinates first for the construction of the MotionEvent,
48 // and after it is created, adjust it back to the local coordinates. This way there is a history
49 // of the absolute coordinates for developers who rely on that (ViewGroup does this as well).
50 val locationOnScreen = IntArray(2) { 0 }
51 dispatchTarget.getLocationOnScreen(locationOnScreen)
52
53 pointerCoords.forEach {
54 it.x += locationOnScreen[0]
55 it.y += locationOnScreen[1]
56 }
57
58 val motionEvent =
59 MotionEvent.obtain(
60 0,
61 eventTime.toLong(),
62 action + (actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
63 numPointers,
64 pointerProperties,
65 pointerCoords,
66 0,
67 0,
68 0f,
69 0f,
70 0,
71 0,
72 SOURCE_TOUCHSCREEN, // Required for offsetLocation() to work correctly
73 0
74 )
75 .apply {
76 offsetLocation(-locationOnScreen[0].toFloat(), -locationOnScreen[1].toFloat())
77 }
78
79 pointerCoords.forEach {
80 it.x -= locationOnScreen[0]
81 it.y -= locationOnScreen[1]
82 }
83
84 return motionEvent
85 }
86
87 @Suppress("RemoveRedundantQualifierName")
<lambda>null88 internal fun PointerProperties(id: Int) = MotionEvent.PointerProperties().apply { this.id = id }
89
90 @Suppress("RemoveRedundantQualifierName")
PointerCoordsnull91 internal fun PointerCoords(x: Float, y: Float) =
92 MotionEvent.PointerCoords().apply {
93 this.x = x
94 this.y = y
95 }
96
97 /**
98 * Creates an array of down [MotionEvent]s meaning the first event is a [MotionEvent.ACTION_DOWN]
99 * and all following events (if there are any) are [MotionEvent.ACTION_POINTER_DOWN]s. These will
100 * usually be paired with a [MotionEvent] up events.
101 *
102 * @param initialX Starting x coordinate for the first [MotionEvent]
103 * @param initialTime Starting time for the first [MotionEvent]
104 * @param y - Y used for all [MotionEvent]s (only x is updated for each moves).
105 * @param rootView - [View] that the [MotionEvent] is dispatched to.
106 * @param numberOfEvents Number of [MotionEvent]s to create.
107 * @param timeDelta - Time between each [MotionEvent] in milliseconds.
108 * @param moveDelta - Amount to move in pixels for each [MotionEvent]
109 */
createDownsnull110 internal fun createDowns(
111 initialX: Float,
112 initialTime: Int,
113 y: Float, // Same Y used for all moves
114 rootView: View,
115 numberOfEvents: Int = 1,
116 timeDelta: Int = DefaultPointerInputTimeDelta,
117 moveDelta: Float = DefaultPointerInputMoveAmountPx
118 ): Array<MotionEvent> {
119 if (numberOfEvents < 1) {
120 return emptyArray()
121 }
122
123 var time = initialTime
124 var x = initialX
125
126 val pointerProperties = mutableListOf<PointerProperties>()
127 val pointerCoords = mutableListOf<PointerCoords>()
128
129 val downMotionEvents =
130 Array(numberOfEvents) { index ->
131 // Add pointers as we create down events
132 pointerProperties.add(index, PointerProperties(index))
133 pointerCoords.add(index, PointerCoords(x, y))
134
135 val down =
136 MotionEvent(
137 time,
138 if (index == 0) {
139 MotionEvent.ACTION_DOWN
140 } else {
141 MotionEvent.ACTION_POINTER_DOWN
142 },
143 index + 1,
144 index, // Used in conjunction with ACTION_POINTER_DOWN/UP
145 pointerProperties.toTypedArray(),
146 pointerCoords.toTypedArray(),
147 rootView
148 )
149
150 time += timeDelta
151 x += moveDelta
152
153 down // return down event
154 }
155 return downMotionEvents
156 }
157
158 /**
159 * Creates an array of up [MotionEvent]s meaning alls up events are [MotionEvent.ACTION_POINTER_UP]s
160 * minus the last one (which is a [MotionEvent.ACTION_UP]). These will usually be paired with a
161 * [MotionEvent] down events.
162 *
163 * @param initialTime Starting time for the first [MotionEvent]
164 * @param initialPointers All pointers to create set of up events
165 * @param rootView - [View] that the [MotionEvent] is dispatched to.
166 * @param timeDelta - Time between each [MotionEvent] in milliseconds.
167 */
createUpsnull168 internal fun createUps(
169 initialTime: Int,
170 initialPointers: Array<BenchmarkSimplifiedPointerInputPointer>,
171 rootView: View,
172 timeDelta: Int = DefaultPointerInputTimeDelta
173 ): Array<MotionEvent> {
174 if (initialPointers.isEmpty()) {
175 return emptyArray()
176 }
177
178 var time = initialTime
179
180 val pointerProperties = mutableListOf<PointerProperties>()
181 val pointerCoords = mutableListOf<PointerCoords>()
182
183 // Convert simplified pointers to actual PointerProperties and PointerCoords.
184 for ((index, initialPointer) in initialPointers.withIndex()) {
185 pointerProperties.add(index, PointerProperties(initialPointer.id))
186 pointerCoords.add(index, PointerCoords(initialPointer.x, initialPointer.y))
187 }
188
189 val upMotionEvents =
190 Array(initialPointers.size) { index ->
191 // Only the last element should be an ACTION_UP
192 val action =
193 if (index == initialPointers.size - 1) {
194 MotionEvent.ACTION_UP
195 } else {
196 MotionEvent.ACTION_POINTER_UP
197 }
198
199 val numberOfPointers = initialPointers.size - index
200
201 val up =
202 MotionEvent(
203 time,
204 action,
205 numberOfPointers,
206 numberOfPointers - 1, // Used with ACTION_POINTER_DOWN/UP
207 pointerProperties.toTypedArray(),
208 pointerCoords.toTypedArray(),
209 rootView
210 )
211
212 // Update time for next ACTION_UP/ACTION_POINTER_UP
213 time += timeDelta
214
215 // The next ACTION_UP/ACTION_POINTER_UP will have one less pointer, so we remove the
216 // last element from both lists.
217 if (pointerProperties.isNotEmpty()) {
218 pointerProperties.removeAt(pointerProperties.size - 1)
219 }
220
221 if (pointerCoords.isNotEmpty()) {
222 pointerCoords.removeAt(pointerCoords.size - 1)
223 }
224 up // return up event
225 }
226 return upMotionEvents
227 }
228
229 /**
230 * Creates an array of subsequent [MotionEvent.ACTION_MOVE]s to pair with a
231 * [MotionEvent.ACTION_DOWN] and a [MotionEvent.ACTION_UP] to recreate a user input sequence. Note:
232 * We offset pointers/events by time and x only (y stays the same).
233 *
234 * @param initialTime Starting time for the first [MotionEvent.ACTION_MOVE]
235 * @param initialPointers Starting coordinates for all [MotionEvent.ACTION_MOVE] pointers
236 * @param rootView - [View] that the [MotionEvent] is dispatched to.
237 * @param numberOfMoveEvents Number of [MotionEvent.ACTION_MOVE]s to create.
238 * @param enableFlingStyleHistory - Adds a history of [MotionEvent.ACTION_MOVE]s to each
239 * [MotionEvent.ACTION_MOVE] to mirror a fling event (where you will get more
240 * [MotionEvent.ACTION_MOVE]s than the refresh rate of the phone).
241 * @param timeDelta - Time between each [MotionEvent.ACTION_MOVE] in milliseconds.
242 * @param moveDelta - Amount to move in pixels for each [MotionEvent.ACTION_MOVE]
243 */
createMoveMotionEventsnull244 internal fun createMoveMotionEvents(
245 initialTime: Int,
246 initialPointers: Array<BenchmarkSimplifiedPointerInputPointer>,
247 rootView: View,
248 numberOfMoveEvents: Int,
249 enableFlingStyleHistory: Boolean = false,
250 timeDelta: Int = DefaultPointerInputTimeDelta,
251 moveDelta: Float = DefaultPointerInputMoveAmountPx
252 ): Array<MotionEvent> {
253
254 var time = initialTime
255
256 // Creates a list of pointer properties and coordinates from initialPointers to represent
257 // all pointers/fingers in the [MotionEvent] we create.
258 val pointerProperties = mutableListOf<PointerProperties>()
259 val pointerCoords = mutableListOf<PointerCoords>()
260
261 for ((index, initialPointer) in initialPointers.withIndex()) {
262 pointerProperties.add(index, PointerProperties(initialPointer.id))
263 pointerCoords.add(index, PointerCoords(initialPointer.x, initialPointer.y))
264 }
265
266 val moveMotionEvents =
267 Array(numberOfMoveEvents) { index ->
268 val move =
269 if (enableFlingStyleHistory) {
270 val historicalEventCount = numberOfHistoricalEventsBasedOnArrayLocation(index)
271
272 // Set the time to the previous event time (either a down or move event) and
273 // offset it, so it doesn't conflict with that previous event.
274 var historicalTime: Int = time - timeDelta + 10
275
276 var accountForMoveOffset = -1
277
278 // Creates starting x values for all historical pointers (takes ending x of
279 // all pointers and subtracts a delta and offset).
280 val historicalPointerCoords = mutableListOf<PointerCoords>()
281
282 for ((historicalIndex, historicalPointer) in pointerCoords.withIndex()) {
283 if (moveDelta > 0) {
284 // accountForMoveOffset stays -1 (to account for +1 offset [below])
285 historicalPointerCoords.add(
286 historicalIndex,
287 PointerCoords(
288 historicalPointer.x - moveDelta + 1,
289 historicalPointer.y
290 )
291 )
292 } else {
293 // accountForMoveOffset changes to 1 (to account for -1 offset [below])
294 accountForMoveOffset = 1
295 historicalPointerCoords.add(
296 historicalIndex,
297 PointerCoords(
298 historicalPointer.x - moveDelta - 1,
299 historicalPointer.y
300 )
301 )
302 }
303 }
304
305 val historicalTimeDelta: Int = (timeDelta - 10) / historicalEventCount
306 val historicalXDelta: Float =
307 (moveDelta + accountForMoveOffset) / historicalEventCount
308
309 // Next section of code creates "historical" events by
310 // 1. Creating [MotionEvent] with oldest historical event.
311 // 2. Adding each subsequent historical event one at a time via `addBatch()`.
312 // 3. Finishes by adding the main/end pointers via `addBatch()`.
313
314 // Executes step 1 -> Creates [MotionEvent] with oldest historical event.
315 val moveWithHistory =
316 MotionEvent(
317 historicalTime,
318 MotionEvent.ACTION_MOVE,
319 pointerProperties.size,
320 0,
321 pointerProperties.toTypedArray(), // ids always the same
322 historicalPointerCoords.toTypedArray(),
323 rootView
324 )
325
326 // Executes step 2 -> Adds each subsequent historical event one at a time via
327 // `addBatch()`.
328 // Starts with the second historical event (1), since we've already added the
329 // first when we created the [MotionEvent] above.
330 for (historyIndex in 1 until historicalEventCount) {
331 // Update historical time
332 historicalTime += historicalTimeDelta
333
334 // Update historical x
335 for (historicalPointerCoord in historicalPointerCoords) {
336 historicalPointerCoord.x += historicalXDelta
337 }
338
339 // Add to [MotionEvent] history
340 moveWithHistory.addBatch(
341 historicalTime.toLong(),
342 historicalPointerCoords.toTypedArray(),
343 0
344 )
345 }
346
347 // Executes step 3 -> Finishes by adding the main/end pointers via `addBatch()`,
348 // so it will show up as the current event for the [MotionEvent].
349 moveWithHistory.addBatch(time.toLong(), pointerCoords.toTypedArray(), 0)
350 // return move event with history added
351 moveWithHistory
352 } else {
353 MotionEvent(
354 time,
355 MotionEvent.ACTION_MOVE,
356 pointerProperties.size,
357 0,
358 pointerProperties.toTypedArray(),
359 pointerCoords.toTypedArray(),
360 rootView
361 )
362 }
363
364 time += timeDelta
365 // Update all pointer's x by move delta for next iteration
366 for (pointerCoord in pointerCoords) {
367 pointerCoord.x += moveDelta
368 }
369 move
370 }
371 return moveMotionEvents
372 }
373
374 /*
375 * Based on traces of fling events, the first events in a series of "MOVES" have more
376 * historical [MotionEvent]s than the subsequent events.
377 *
378 * Remember, historical events within a [MotionEvent] represent extra [MotionEvent]s
379 * that occurred faster than the refresh rate of the phone. A fling will have many more events
380 * in the beginning (and between the refresh rate since they are happening so quick) than in
381 * the end.
382 */
numberOfHistoricalEventsBasedOnArrayLocationnull383 internal fun numberOfHistoricalEventsBasedOnArrayLocation(index: Int): Int {
384 return when (index) {
385 0 -> 12
386 1 -> 9
387 2,
388 3 -> 4
389 else -> 2
390 }
391 }
392