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