1 /*
2 * 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.input.pointer.util
18
19 import androidx.compose.ui.ExperimentalComposeUiApi
20 import androidx.compose.ui.geometry.Offset
21 import androidx.compose.ui.input.pointer.PointerInputChange
22 import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
23 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
24 import androidx.compose.ui.internal.checkPrecondition
25 import androidx.compose.ui.internal.throwIllegalArgumentException
26 import androidx.compose.ui.unit.Velocity
27 import androidx.compose.ui.util.fastCoerceAtLeast
28 import androidx.compose.ui.util.fastForEach
29 import kotlin.math.abs
30 import kotlin.math.sign
31 import kotlin.math.sqrt
32
33 private const val AssumePointerMoveStoppedMilliseconds: Int = 40
34 private const val HistorySize: Int = 20
35
36 // TODO(b/204895043): Keep value in sync with VelocityPathFinder.HorizonMilliSeconds
37 private const val HorizonMilliseconds: Int = 100
38
39 /**
40 * Computes a pointer's velocity.
41 *
42 * The input data is provided by calling [addPosition]. Adding data is cheap.
43 *
44 * To obtain a velocity, call [calculateVelocity]. This will compute the velocity based on the data
45 * added so far. Only call this when you need to use the velocity, as it is comparatively expensive.
46 *
47 * The quality of the velocity estimation will be better if more data points have been received.
48 */
49 @OptIn(ExperimentalVelocityTrackerApi::class)
50 class VelocityTracker {
51
52 private val strategy =
53 if (VelocityTrackerStrategyUseImpulse) {
54 VelocityTracker1D.Strategy.Impulse
55 } else {
56 VelocityTracker1D.Strategy.Lsq2 // non-differential, Lsq2 1D velocity tracker
57 }
58 private val xVelocityTracker = VelocityTracker1D(strategy = strategy)
59 private val yVelocityTracker = VelocityTracker1D(strategy = strategy)
60
61 internal var currentPointerPositionAccumulator = Offset.Zero
62 internal var lastMoveEventTimeStamp = 0L
63
64 /**
65 * Adds a position at the given time to the tracker.
66 *
67 * Call [resetTracking] to remove added [Offset]s.
68 *
69 * @see resetTracking
70 */
71 // TODO(shepshapard): VelocityTracker needs to be updated to be passed vectors instead of
72 // positions. For velocity tracking, the only thing that is important is the change in
73 // position over time.
addPositionnull74 fun addPosition(timeMillis: Long, position: Offset) {
75 xVelocityTracker.addDataPoint(timeMillis, position.x)
76 yVelocityTracker.addDataPoint(timeMillis, position.y)
77 }
78
79 /**
80 * Computes the estimated velocity of the pointer at the time of the last provided data point.
81 *
82 * The velocity calculated will not be limited. Unlike [calculateVelocity(maximumVelocity)] the
83 * resulting velocity won't be limited.
84 *
85 * This can be expensive. Only call this when you need the velocity.
86 */
calculateVelocitynull87 fun calculateVelocity(): Velocity =
88 calculateVelocity(Velocity(Float.MAX_VALUE, Float.MAX_VALUE))
89
90 /**
91 * Computes the estimated velocity of the pointer at the time of the last provided data point.
92 *
93 * The method allows specifying the maximum absolute value for the calculated velocity. If the
94 * absolute value of the calculated velocity exceeds the specified maximum, the return value
95 * will be clamped down to the maximum. For example, if the absolute maximum velocity is
96 * specified as "20", a calculated velocity of "25" will be returned as "20", and a velocity of
97 * "-30" will be returned as "-20".
98 *
99 * @param maximumVelocity the absolute values of the X and Y maximum velocities to be returned
100 * in units/second. `units` is the units of the positions provided to this VelocityTracker.
101 */
102 fun calculateVelocity(maximumVelocity: Velocity): Velocity {
103 checkPrecondition(maximumVelocity.x > 0f && maximumVelocity.y > 0) {
104 "maximumVelocity should be a positive value. You specified=$maximumVelocity"
105 }
106 val velocityX = xVelocityTracker.calculateVelocity(maximumVelocity.x)
107 val velocityY = yVelocityTracker.calculateVelocity(maximumVelocity.y)
108 return Velocity(velocityX, velocityY)
109 }
110
111 /** Clears the tracked positions added by [addPosition]. */
resetTrackingnull112 fun resetTracking() {
113 xVelocityTracker.resetTracking()
114 yVelocityTracker.resetTracking()
115 lastMoveEventTimeStamp = 0L
116 }
117 }
118
119 /**
120 * A velocity tracker calculating velocity in 1 dimension.
121 *
122 * Add displacement data points using [addDataPoint], and obtain velocity using [calculateVelocity].
123 *
124 * Note: for calculating touch-related or other 2 dimensional/planar velocities, please use
125 * [VelocityTracker], which handles velocity tracking across both X and Y dimensions at once.
126 */
127 class VelocityTracker1D
128 internal constructor(
129 // whether the data points added to the tracker represent differential values
130 // (i.e. change in the tracked object's displacement since the previous data point).
131 // If false, it means that the data points added to the tracker will be considered as absolute
132 // values (e.g. positional values).
133 val isDataDifferential: Boolean = false,
134 // The velocity tracking strategy that this instance uses for all velocity calculations.
135 private val strategy: Strategy = Strategy.Lsq2,
136 ) {
137
138 init {
139 if (isDataDifferential && strategy.equals(Strategy.Lsq2)) {
140 throw IllegalStateException("Lsq2 not (yet) supported for differential axes")
141 }
142 }
143
144 /**
145 * Constructor to create a new velocity tracker. It allows to specify whether or not the tracker
146 * should consider the data ponits provided via [addDataPoint] as differential or
147 * non-differential.
148 *
149 * Differential data ponits represent change in displacement. For instance, differential data
150 * points of [2, -1, 5] represent: the object moved by "2" units, then by "-1" units, then by
151 * "5" units. An example use case for differential data points is when tracking velocity for an
152 * object whose displacements (or change in positions) over time are known.
153 *
154 * Non-differential data ponits represent position of the object whose velocity is tracked. For
155 * instance, non-differential data points of [2, -1, 5] represent: the object was at position
156 * "2", then at position "-1", then at position "5". An example use case for non-differential
157 * data points is when tracking velocity for an object whose positions on a geometrical axis
158 * over different instances of time are known.
159 *
160 * @param isDataDifferential [true] if the data ponits provided to the constructed tracker are
161 * differential. [false] otherwise.
162 */
163 constructor(isDataDifferential: Boolean) : this(isDataDifferential, Strategy.Impulse)
164
165 private val minSampleSize: Int =
166 when (strategy) {
167 Strategy.Impulse -> 2
168 Strategy.Lsq2 -> 3
169 }
170
171 /**
172 * A strategy used for velocity calculation. Each strategy has a different philosophy that could
173 * result in notably different velocities than the others, so make careful choice or change of
174 * strategy whenever you want to make one.
175 */
176 internal enum class Strategy {
177 /**
178 * Least squares strategy. Polynomial fit at degree 2. Note that the implementation of this
179 * strategy currently supports only non-differential data points.
180 */
181 Lsq2,
182
183 /**
184 * Impulse velocity tracking strategy, that calculates velocity using the mathematical
185 * relationship between kinetic energy and velocity.
186 */
187 Impulse,
188 }
189
190 // Circular buffer; current sample at index.
191 private val samples: Array<DataPointAtTime?> = arrayOfNulls(HistorySize)
192 private var index: Int = 0
193
194 // Reusable arrays to avoid allocation inside calculateVelocity.
195 private val reusableDataPointsArray = FloatArray(HistorySize)
196 private val reusableTimeArray = FloatArray(HistorySize)
197
198 // Reusable array to minimize allocations inside calculateLeastSquaresVelocity.
199 private val reusableVelocityCoefficients = FloatArray(3)
200
201 /**
202 * Adds a data point for velocity calculation at a given time, [timeMillis]. The data ponit
203 * represents an amount of a change in position (for differential data points), or an absolute
204 * position (for non-differential data points). Whether or not the tracker handles differential
205 * data points is decided by [isDataDifferential], which is set once and finally during the
206 * construction of the tracker.
207 *
208 * Use the same units for the data points provided. For example, having some data points in `cm`
209 * and some in `m` will result in incorrect velocity calculations, as this method (and the
210 * tracker) has no knowledge of the units used.
211 */
addDataPointnull212 fun addDataPoint(timeMillis: Long, dataPoint: Float) {
213 index = (index + 1) % HistorySize
214 samples.set(index, timeMillis, dataPoint)
215 }
216
217 /**
218 * Computes the estimated velocity at the time of the last provided data point.
219 *
220 * The units of velocity will be `units/second`, where `units` is the units of the data points
221 * provided via [addDataPoint].
222 *
223 * This can be expensive. Only call this when you need the velocity.
224 */
calculateVelocitynull225 fun calculateVelocity(): Float {
226 val dataPoints = reusableDataPointsArray
227 val time = reusableTimeArray
228 var sampleCount = 0
229 var index: Int = index
230
231 // The sample at index is our newest sample. If it is null, we have no samples so return.
232 val newestSample: DataPointAtTime = samples[index] ?: return 0f
233
234 var previousSample: DataPointAtTime = newestSample
235
236 // Starting with the most recent PointAtTime sample, iterate backwards while
237 // the samples represent continuous motion.
238 do {
239 val sample: DataPointAtTime = samples[index] ?: break
240
241 val age: Float = (newestSample.time - sample.time).toFloat()
242 val delta: Float = abs(sample.time - previousSample.time).toFloat()
243 previousSample =
244 if (strategy == Strategy.Lsq2 || isDataDifferential) {
245 sample
246 } else {
247 newestSample
248 }
249 if (age > HorizonMilliseconds || delta > AssumePointerMoveStoppedMilliseconds) {
250 break
251 }
252
253 dataPoints[sampleCount] = sample.dataPoint
254 time[sampleCount] = -age
255 index = (if (index == 0) HistorySize else index) - 1
256
257 sampleCount += 1
258 } while (sampleCount < HistorySize)
259
260 if (sampleCount >= minSampleSize) {
261 // Choose computation logic based on strategy.
262 return when (strategy) {
263 Strategy.Impulse -> {
264 calculateImpulseVelocity(dataPoints, time, sampleCount, isDataDifferential)
265 }
266 Strategy.Lsq2 -> {
267 calculateLeastSquaresVelocity(dataPoints, time, sampleCount)
268 }
269 } * 1000 // Multiply by "1000" to convert from units/ms to units/s
270 }
271
272 // We're unable to make a velocity estimate but we did have at least one
273 // valid pointer position.
274 return 0f
275 }
276
277 /**
278 * Computes the estimated velocity at the time of the last provided data point.
279 *
280 * The method allows specifying the maximum absolute value for the calculated velocity. If the
281 * absolute value of the calculated velocity exceeds the specified maximum, the return value
282 * will be clamped down to the maximum. For example, if the absolute maximum velocity is
283 * specified as "20", a calculated velocity of "25" will be returned as "20", and a velocity of
284 * "-30" will be returned as "-20".
285 *
286 * @param maximumVelocity the absolute value of the maximum velocity to be returned in
287 * units/second, where `units` is the units of the positions provided to this VelocityTracker.
288 */
calculateVelocitynull289 fun calculateVelocity(maximumVelocity: Float): Float {
290 checkPrecondition(maximumVelocity > 0f) {
291 "maximumVelocity should be a positive value. You specified=$maximumVelocity"
292 }
293 val velocity = calculateVelocity()
294
295 return if (velocity == 0.0f || velocity.isNaN()) {
296 0.0f
297 } else if (velocity > 0) {
298 velocity.coerceAtMost(maximumVelocity)
299 } else {
300 velocity.coerceAtLeast(-maximumVelocity)
301 }
302 }
303
304 /** Clears data points added by [addDataPoint]. */
resetTrackingnull305 fun resetTracking() {
306 samples.fill(element = null)
307 index = 0
308 }
309
310 /**
311 * Calculates velocity based on [Strategy.Lsq2]. The provided [time] entries are in "ms", and
312 * should be provided in reverse chronological order. The returned velocity is in "units/ms",
313 * where "units" is unit of the [dataPoints].
314 */
calculateLeastSquaresVelocitynull315 private fun calculateLeastSquaresVelocity(
316 dataPoints: FloatArray,
317 time: FloatArray,
318 sampleCount: Int
319 ): Float {
320 // The 2nd coefficient is the derivative of the quadratic polynomial at
321 // x = 0, and that happens to be the last timestamp that we end up
322 // passing to polyFitLeastSquares.
323 return try {
324 polyFitLeastSquares(time, dataPoints, sampleCount, 2, reusableVelocityCoefficients)[1]
325 } catch (exception: IllegalArgumentException) {
326 0f
327 }
328 }
329 }
330
331 /**
332 * Extension to simplify either creating a new [DataPointAtTime] at an array index (if the index was
333 * never populated), or to update an existing [DataPointAtTime] (if the index had an existing
334 * element). This helps to have zero allocations on average, and avoid performance hit that can be
335 * caused by creating lots of objects.
336 */
Arraynull337 private fun Array<DataPointAtTime?>.set(index: Int, time: Long, dataPoint: Float) {
338 val currentEntry = this[index]
339 if (currentEntry == null) {
340 this[index] = DataPointAtTime(time, dataPoint)
341 } else {
342 currentEntry.time = time
343 currentEntry.dataPoint = dataPoint
344 }
345 }
346
347 /**
348 * Track the positions and timestamps inside this event change.
349 *
350 * For optimal tracking, this should be called for the DOWN event and all MOVE events, including any
351 * touch-slop-captured MOVE event.
352 *
353 * Since Compose uses relative positions inside PointerInputChange, this should be taken into
354 * consideration when using this method. Right now, we use the first down to initialize an
355 * accumulator and use subsequent deltas to simulate an actual movement from relative positions in
356 * PointerInputChange. This is required because VelocityTracker requires data that can be fit into a
357 * curve, which might not happen with relative positions inside a moving target for instance.
358 *
359 * @param event Pointer change to track.
360 */
361 @OptIn(ExperimentalComposeUiApi::class)
addPointerInputChangenull362 fun VelocityTracker.addPointerInputChange(event: PointerInputChange) {
363 if (VelocityTrackerAddPointsFix) {
364 addPointerInputChangeWithFix(event)
365 } else {
366 addPointerInputChangeLegacy(event)
367 }
368 }
369
370 @OptIn(ExperimentalComposeUiApi::class)
VelocityTrackernull371 private fun VelocityTracker.addPointerInputChangeLegacy(event: PointerInputChange) {
372
373 // Register down event as the starting point for the accumulator
374 if (event.changedToDownIgnoreConsumed()) {
375 currentPointerPositionAccumulator = event.position
376 resetTracking()
377 }
378
379 // To calculate delta, for each step we want to do currentPosition - previousPosition.
380 // Initially the previous position is the previous position of the current event
381 var previousPointerPosition = event.previousPosition
382 @OptIn(ExperimentalComposeUiApi::class)
383 event.historical.fastForEach {
384 // Historical data happens within event.position and event.previousPosition
385 // That means, event.previousPosition < historical data < event.position
386 // Initially, the first delta will happen between the previousPosition and
387 // the first position in historical delta. For subsequent historical data, the
388 // deltas happen between themselves. That's why we need to update previousPointerPosition
389 // everytime.
390 val historicalDelta = it.position - previousPointerPosition
391 previousPointerPosition = it.position
392
393 // Update the current position with the historical delta and add it to the tracker
394 currentPointerPositionAccumulator += historicalDelta
395 addPosition(it.uptimeMillis, currentPointerPositionAccumulator)
396 }
397
398 // For the last position in the event
399 // If there's historical data, the delta is event.position - lastHistoricalPoint
400 // If there's no historical data, the delta is event.position - event.previousPosition
401 val delta = event.position - previousPointerPosition
402 currentPointerPositionAccumulator += delta
403 addPosition(event.uptimeMillis, currentPointerPositionAccumulator)
404 }
405
VelocityTrackernull406 private fun VelocityTracker.addPointerInputChangeWithFix(event: PointerInputChange) {
407 // If this is ACTION_DOWN: Reset the tracking.
408 if (event.changedToDownIgnoreConsumed()) {
409 resetTracking()
410 }
411
412 // If this is not ACTION_UP event: Add events to the tracker as per the platform implementation.
413 // In the platform implementation the historical events array is used, they store the current
414 // event data in the position HistoricalArray.Size. Our historical array doesn't have access
415 // to the final position, but we can get that information from the original event data X and Y
416 // coordinates.
417 if (!event.changedToUpIgnoreConsumed()) {
418 event.historical.fastForEach { addPosition(it.uptimeMillis, it.originalEventPosition) }
419 addPosition(event.uptimeMillis, event.originalEventPosition)
420 }
421
422 // If this is ACTION_UP. Fix for b/238654963. If there's been enough time after the last MOVE
423 // event, reset the tracker.
424 if (event.changedToUpIgnoreConsumed() && (event.uptimeMillis - lastMoveEventTimeStamp) > 40L) {
425 resetTracking()
426 }
427 lastMoveEventTimeStamp = event.uptimeMillis
428 }
429
430 internal data class DataPointAtTime(var time: Long, var dataPoint: Float)
431
432 /**
433 * TODO (shepshapard): If we want to support varying weights for each position, we could accept a
434 * 3rd FloatArray of weights for each point and use them instead of the [DefaultWeight].
435 */
436 private const val DefaultWeight = 1f
437
438 /**
439 * Fits a polynomial of the given degree to the data points.
440 *
441 * If the [degree] is larger than or equal to the number of points, a polynomial will be returned
442 * with coefficients of the value 0 for all degrees larger than or equal to the number of points.
443 * For example, if 2 data points are provided and a quadratic polynomial (degree of 2) is requested,
444 * the resulting polynomial ax^2 + bx + c is guaranteed to have a = 0;
445 *
446 * Throws an IllegalArgumentException if:
447 * <ul>
448 * <li>[degree] is not a positive integer.
449 * <li>[sampleCount] is zero.
450 * </ul>
451 */
polyFitLeastSquaresnull452 internal fun polyFitLeastSquares(
453 /** The x-coordinates of each data point. */
454 x: FloatArray,
455 /** The y-coordinates of each data point. */
456 y: FloatArray,
457 /** number of items in each array */
458 sampleCount: Int,
459 degree: Int,
460 coefficients: FloatArray = FloatArray((degree + 1).coerceAtLeast(0))
461 ): FloatArray {
462 if (degree < 1) {
463 throwIllegalArgumentException("The degree must be at positive integer")
464 }
465 if (sampleCount == 0) {
466 throwIllegalArgumentException("At least one point must be provided")
467 }
468
469 val truncatedDegree =
470 if (degree >= sampleCount) {
471 sampleCount - 1
472 } else {
473 degree
474 }
475
476 // Shorthands for the purpose of notation equivalence to original C++ code.
477 val m = sampleCount
478 val n = truncatedDegree + 1
479
480 // Expand the X vector to a matrix A, pre-multiplied by the weights.
481 val a = Matrix(n, m)
482 for (h in 0 until m) {
483 a[0, h] = DefaultWeight
484 for (i in 1 until n) {
485 a[i, h] = a[i - 1, h] * x[h]
486 }
487 }
488
489 // Apply the Gram-Schmidt process to A to obtain its QR decomposition.
490
491 // Orthonormal basis, column-major order.
492 val q = Matrix(n, m)
493 // Upper triangular matrix, row-major order.
494 val r = Matrix(n, n)
495 for (j in 0 until n) {
496 val w = q[j]
497 a[j].copyInto(w, 0, 0, m)
498
499 for (i in 0 until j) {
500 val z = q[i]
501 val dot = w.dot(z)
502 for (h in 0 until m) {
503 w[h] -= dot * z[h]
504 }
505 }
506
507 val inverseNorm = 1.0f / w.norm().fastCoerceAtLeast(1e-6f)
508 for (h in 0 until m) {
509 w[h] *= inverseNorm
510 }
511
512 val v = r[j]
513 for (i in 0 until n) {
514 v[i] = if (i < j) 0.0f else w.dot(a[i])
515 }
516 }
517
518 // Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
519 // We just work from bottom-right to top-left calculating B's coefficients.
520 var wy = y
521
522 // NOTE: DefaultWeight is currently always set to 1.0f, there's no need to allocate a new
523 // array and to perform several multiplications for no reason
524 @Suppress("KotlinConstantConditions")
525 if (DefaultWeight != 1.0f) {
526 // TODO: Even when we pass the test above, this allocation is likely unnecessary.
527 // We could just modify wy (y) in place instead. This would need to be documented
528 // to avoid surprises for the caller though.
529 wy = FloatArray(m)
530 for (h in 0 until m) {
531 wy[h] = y[h] * DefaultWeight
532 }
533 }
534
535 for (i in n - 1 downTo 0) {
536 var c = q[i].dot(wy)
537 val ri = r[i]
538 for (j in n - 1 downTo i + 1) {
539 c -= ri[j] * coefficients[j]
540 }
541 coefficients[i] = c / ri[i]
542 }
543
544 return coefficients
545 }
546
547 /**
548 * Calculates velocity based on the Impulse strategy. The provided [time] entries are in "ms", and
549 * should be provided in reverse chronological order. The returned velocity is in "units/ms", where
550 * "units" is unit of the [dataPoints].
551 *
552 * Calculates the resulting velocity based on the total immpulse provided by the data ponits.
553 *
554 * The moving object in these calculations is the touchscreen (if we are calculating touch
555 * velocity), or any input device from which the data points are generated. We refer to this object
556 * as the "subject" below.
557 *
558 * Initial condition is discussed below, but for now suppose that v(t=0) = 0
559 *
560 * The kinetic energy of the object at the release is E=0.5*m*v^2 Then vfinal = sqrt(2E/m). The goal
561 * is to calculate E.
562 *
563 * The kinetic energy at the release is equal to the total work done on the object by the finger.
564 * The total work W is the sum of all dW along the path.
565 *
566 * dW = F*dx, where dx is the piece of path traveled. Force is change of momentum over time, F =
567 * dp/dt = m dv/dt. Then substituting: dW = m (dv/dt) * dx = m * v * dv
568 *
569 * Summing along the path, we get: W = sum(dW) = sum(m * v * dv) = m * sum(v * dv) Since the mass
570 * stays constant, the equation for final velocity is: vfinal = sqrt(2*sum(v * dv))
571 *
572 * Here, dv : change of velocity = (v[i+1]-v[i]) dx : change of distance = (x[i+1]-x[i]) dt : change
573 * of time = (t[i+1]-t[i]) v : instantaneous velocity = dx/dt
574 *
575 * The final formula is: vfinal = sqrt(2) * sqrt(sum((v[i]-v[i-1])*|v[i]|)) for all i The absolute
576 * value is needed to properly account for the sign. If the velocity over a particular segment
577 * descreases, then this indicates braking, which means that negative work was done. So for two
578 * positive, but decreasing, velocities, this contribution would be negative and will cause a
579 * smaller final velocity.
580 *
581 * Initial condition There are two ways to deal with initial condition:
582 * 1) Assume that v(0) = 0, which would mean that the subject is initially at rest. This is not
583 * entirely accurate. We are only taking the past X ms of touch data, where X is currently equal
584 * to 100. However, a touch event that created a fling probably lasted for longer than that,
585 * which would mean that the user has already been interacting with the subject, and it has
586 * probably already been moving.
587 * 2) Assume that the subject has already been moving at a certain velocity, calculate this initial
588 * velocity and the equivalent energy, and start with this initial energy. Consider an example
589 * where we have the following data, consisting of 3 points: time: t0, t1, t2 x : x0, x1, x2 v :
590 * 0, v1, v2 Here is what will happen in each of these scenarios:
591 * 1) By directly applying the formula above with the v(0) = 0 boundary condition, we will get
592 * vfinal = sqrt(2*(|v1|*(v1-v0) + |v2|*(v2-v1))). This can be simplified since v0=0 vfinal =
593 * sqrt(2*(|v1|*v1 + |v2|*(v2-v1))) = sqrt(2*(v1^2 + |v2|*(v2 - v1))) since velocity is a real
594 * number
595 * 2) If we treat the subject as already moving, then it must already have an energy (per mass)
596 * equal to 1/2*v1^2. Then the initial energy should be 1/2*v1*2, and only the second segment
597 * will contribute to the total kinetic energy (since we can effectively consider that v0=v1).
598 * This will give the following expression for the final velocity: vfinal = sqrt(2*(1/2*v1^2 +
599 * |v2|*(v2-v1))) This analysis can be generalized to an arbitrary number of samples.
600 *
601 * Comparing the two equations above, we see that the only mathematical difference is the factor of
602 * 1/2 in front of the first velocity term. This boundary condition would allow for the "proper"
603 * calculation of the case when all of the samples are equally spaced in time and distance, which
604 * should suggest a constant velocity.
605 *
606 * Note that approach 2) is sensitive to the proper ordering of the data in time, since the boundary
607 * condition must be applied to the oldest sample to be accurate.
608 *
609 * NOTE: [sampleCount] MUST be >= 2
610 */
calculateImpulseVelocitynull611 private fun calculateImpulseVelocity(
612 dataPoints: FloatArray,
613 time: FloatArray,
614 sampleCount: Int,
615 isDataDifferential: Boolean
616 ): Float {
617 var work = 0f
618 val start = sampleCount - 1
619 var nextTime = time[start]
620 for (i in start downTo 1) {
621 val currentTime = nextTime
622 nextTime = time[i - 1]
623 if (currentTime == nextTime) {
624 continue
625 }
626 val dataPointsDelta =
627 if (isDataDifferential) -dataPoints[i - 1] else dataPoints[i] - dataPoints[i - 1]
628 val vCurr = dataPointsDelta / (currentTime - nextTime)
629 val vPrev = kineticEnergyToVelocity(work)
630 work += (vCurr - vPrev) * abs(vCurr)
631 if (i == start) {
632 work = (work * 0.5f)
633 }
634 }
635 return kineticEnergyToVelocity(work)
636 }
637
638 /**
639 * Calculates the velocity for a given [kineticEnergy], using the formula: Kinetic Energy = 0.5 *
640 * mass * (velocity)^2 where a mass of "1" is used.
641 */
642 @Suppress("NOTHING_TO_INLINE")
kineticEnergyToVelocitynull643 private inline fun kineticEnergyToVelocity(kineticEnergy: Float): Float {
644 return sign(kineticEnergy) * sqrt(2 * abs(kineticEnergy))
645 }
646
647 private typealias Vector = FloatArray
648
FloatArraynull649 private fun FloatArray.dot(a: FloatArray): Float {
650 var result = 0.0f
651 for (i in indices) {
652 result += this[i] * a[i]
653 }
654 return result
655 }
656
normnull657 @Suppress("NOTHING_TO_INLINE") private inline fun FloatArray.norm(): Float = sqrt(this.dot(this))
658
659 private typealias Matrix = Array<FloatArray>
660
661 @Suppress("NOTHING_TO_INLINE")
662 private inline fun Matrix(rows: Int, cols: Int) = Array(rows) { Vector(cols) }
663
664 @Suppress("NOTHING_TO_INLINE")
getnull665 private inline operator fun Matrix.get(row: Int, col: Int): Float = this[row][col]
666
667 @Suppress("NOTHING_TO_INLINE")
668 private inline operator fun Matrix.set(row: Int, col: Int, value: Float) {
669 this[row][col] = value
670 }
671
672 /**
673 * A flag to indicate that we'll use the fix of how we add points to the velocity tracker.
674 *
675 * This is an experiment flag and will be removed once the experiments with the fix a finished. The
676 * final goal is that we will use the true path once the flag is removed. If you find any issues
677 * with the new fix, flip this flag to false to confirm they are newly introduced then file a bug.
678 * Tracking bug: (b/318621681)
679 */
680 @Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
681 @get:Suppress("GetterSetterNames")
682 @get:ExperimentalComposeUiApi
683 @set:ExperimentalComposeUiApi
684 @ExperimentalComposeUiApi
685 var VelocityTrackerAddPointsFix: Boolean = true
686
687 /**
688 * Selecting flag to enable impulse strategy for the velocity trackers. This is an experiment flag
689 * and will be removed once the experiments with the fix a finished. The final goal is that we will
690 * use the true path once the flag is removed. If you find any issues with the new fix, flip this
691 * flag to false to confirm they are newly introduced then file a bug. Tracking bug: (b/318621681)
692 */
693 @Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
694 @get:Suppress("GetterSetterNames")
695 @get:ExperimentalVelocityTrackerApi
696 @set:ExperimentalVelocityTrackerApi
697 @ExperimentalVelocityTrackerApi
698 var VelocityTrackerStrategyUseImpulse = false
699
700 @RequiresOptIn(
701 "This an opt-in flag to test the Velocity Tracker strategy algorithm used " +
702 "for calculating gesture velocities in Compose."
703 )
704 @Retention(AnnotationRetention.BINARY)
705 annotation class ExperimentalVelocityTrackerApi
706