
commonMain.androidx.compose.ui.input.pointer.util.VelocityTracker.kt Maven / Gradle / Ivy
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.input.pointer.util
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.internal.checkPrecondition
import androidx.compose.ui.internal.throwIllegalArgumentException
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
import kotlin.math.sign
import kotlin.math.sqrt
private const val AssumePointerMoveStoppedMilliseconds: Int = 40
internal expect val HistorySize: Int
// TODO(b/204895043): Keep value in sync with VelocityPathFinder.HorizonMilliSeconds
private const val HorizonMilliseconds: Int = 100
/**
* Computes a pointer's velocity.
*
* The input data is provided by calling [addPosition]. Adding data is cheap.
*
* To obtain a velocity, call [calculateVelocity]. This will compute the velocity
* based on the data added so far. Only call this when you need to use the velocity,
* as it is comparatively expensive.
*
* The quality of the velocity estimation will be better if more data points
* have been received.
*/
class VelocityTracker {
@OptIn(ExperimentalComposeUiApi::class)
private val strategy = if (VelocityTrackerStrategyUseImpulse) {
VelocityTracker1D.Strategy.Impulse
} else {
VelocityTracker1D.Strategy.Lsq2 // non-differential, Lsq2 1D velocity tracker
}
private val xVelocityTracker = VelocityTracker1D(strategy = strategy)
private val yVelocityTracker = VelocityTracker1D(strategy = strategy)
internal var currentPointerPositionAccumulator = Offset.Zero
internal var lastMoveEventTimeStamp = 0L
/**
* Adds a position at the given time to the tracker.
*
* Call [resetTracking] to remove added [Offset]s.
*
* @see resetTracking
*/
// TODO(shepshapard): VelocityTracker needs to be updated to be passed vectors instead of
// positions. For velocity tracking, the only thing that is important is the change in
// position over time.
fun addPosition(timeMillis: Long, position: Offset) {
xVelocityTracker.addDataPoint(timeMillis, position.x)
yVelocityTracker.addDataPoint(timeMillis, position.y)
}
/**
* Computes the estimated velocity of the pointer at the time of the last provided data point.
*
* The velocity calculated will not be limited. Unlike [calculateVelocity(maximumVelocity)]
* the resulting velocity won't be limited.
*
* This can be expensive. Only call this when you need the velocity.
*/
fun calculateVelocity(): Velocity =
calculateVelocity(Velocity(Float.MAX_VALUE, Float.MAX_VALUE))
/**
* Computes the estimated velocity of the pointer at the time of the last provided data point.
*
* The method allows specifying the maximum absolute value for the calculated
* velocity. If the absolute value of the calculated velocity exceeds the specified
* maximum, the return value will be clamped down to the maximum. For example, if
* the absolute maximum velocity is specified as "20", a calculated velocity of "25"
* will be returned as "20", and a velocity of "-30" will be returned as "-20".
*
* @param maximumVelocity the absolute values of the X and Y maximum velocities to
* be returned in units/second. `units` is the units of the positions provided to this
* VelocityTracker.
*/
fun calculateVelocity(maximumVelocity: Velocity): Velocity {
checkPrecondition(maximumVelocity.x > 0f && maximumVelocity.y > 0) {
"maximumVelocity should be a positive value. You specified=$maximumVelocity"
}
val velocityX = xVelocityTracker.calculateVelocity(maximumVelocity.x)
val velocityY = yVelocityTracker.calculateVelocity(maximumVelocity.y)
return Velocity(velocityX, velocityY)
}
/**
* Clears the tracked positions added by [addPosition].
*/
fun resetTracking() {
xVelocityTracker.resetTracking()
yVelocityTracker.resetTracking()
lastMoveEventTimeStamp = 0L
}
}
/**
* A velocity tracker calculating velocity in 1 dimension.
*
* Add displacement data points using [addDataPoint], and obtain velocity using [calculateVelocity].
*
* Note: for calculating touch-related or other 2 dimensional/planar velocities, please use
* [VelocityTracker], which handles velocity tracking across both X and Y dimensions at once.
*/
class VelocityTracker1D internal constructor(
// whether the data points added to the tracker represent differential values
// (i.e. change in the tracked object's displacement since the previous data point).
// If false, it means that the data points added to the tracker will be considered as absolute
// values (e.g. positional values).
val isDataDifferential: Boolean = false,
// The velocity tracking strategy that this instance uses for all velocity calculations.
private val strategy: Strategy = Strategy.Lsq2,
) {
init {
if (isDataDifferential && strategy.equals(Strategy.Lsq2)) {
throw IllegalStateException("Lsq2 not (yet) supported for differential axes")
}
}
/**
* Constructor to create a new velocity tracker. It allows to specify whether or not the tracker
* should consider the data points provided via [addDataPoint] as differential or
* non-differential.
*
* Differential data points represent change in displacement. For instance, differential data
* points of [2, -1, 5] represent: the object moved by "2" units, then by "-1" units, then by
* "5" units. An example use case for differential data points is when tracking velocity for an
* object whose displacements (or change in positions) over time are known.
*
* Non-differential data points represent position of the object whose velocity is tracked. For
* instance, non-differential data points of [2, -1, 5] represent: the object was at position
* "2", then at position "-1", then at position "5". An example use case for non-differential
* data points is when tracking velocity for an object whose positions on a geometrical axis
* over different instances of time are known.
*
* @param isDataDifferential [true] if the data points provided to the constructed tracker
* are differential. [false] otherwise.
*/
constructor(isDataDifferential: Boolean) : this(isDataDifferential, Strategy.Impulse)
private val minSampleSize: Int = when (strategy) {
Strategy.Impulse -> 2
Strategy.Lsq2 -> 3
}
/**
* A strategy used for velocity calculation. Each strategy has a different philosophy that could
* result in notably different velocities than the others, so make careful choice or change of
* strategy whenever you want to make one.
*/
internal enum class Strategy {
/**
* Least squares strategy. Polynomial fit at degree 2.
* Note that the implementation of this strategy currently supports only non-differential
* data points.
*/
Lsq2,
/**
* Impulse velocity tracking strategy, that calculates velocity using the mathematical
* relationship between kinetic energy and velocity.
*/
Impulse,
}
// Circular buffer; current sample at index.
private val samples: Array = arrayOfNulls(HistorySize)
private var index: Int = 0
// Reusable arrays to avoid allocation inside calculateVelocity.
private val reusableDataPointsArray = FloatArray(HistorySize)
private val reusableTimeArray = FloatArray(HistorySize)
// Reusable array to minimize allocations inside calculateLeastSquaresVelocity.
private val reusableVelocityCoefficients = FloatArray(3)
/**
* Adds a data point for velocity calculation at a given time, [timeMillis]. The data ponit
* represents an amount of a change in position (for differential data points), or an absolute
* position (for non-differential data points). Whether or not the tracker handles differential
* data points is decided by [isDataDifferential], which is set once and finally during
* the construction of the tracker.
*
* Use the same units for the data points provided. For example, having some data points in `cm`
* and some in `m` will result in incorrect velocity calculations, as this method (and the
* tracker) has no knowledge of the units used.
*/
fun addDataPoint(timeMillis: Long, dataPoint: Float) {
index = (index + 1) % HistorySize
samples.set(index, timeMillis, dataPoint)
}
/**
* Computes the estimated velocity at the time of the last provided data point.
*
* The units of velocity will be `units/second`, where `units` is the units of the data
* points provided via [addDataPoint].
*
* This can be expensive. Only call this when you need the velocity.
*/
fun calculateVelocity(): Float {
val dataPoints = reusableDataPointsArray
val time = reusableTimeArray
var sampleCount = 0
var index: Int = index
// The sample at index is our newest sample. If it is null, we have no samples so return.
val newestSample: DataPointAtTime = samples[index] ?: return 0f
var previousSample: DataPointAtTime = newestSample
var afterPointerStop = false
// Starting with the most recent PointAtTime sample, iterate backwards while
// the samples represent continuous motion.
do {
val sample: DataPointAtTime = samples[index] ?: break
val age: Float = (newestSample.time - sample.time).toFloat()
val delta: Float =
abs(sample.time - previousSample.time).toFloat()
previousSample = if (strategy == Strategy.Lsq2 || isDataDifferential) {
sample
} else {
newestSample
}
if (delta > AssumePointerMoveStoppedMilliseconds) {
afterPointerStop = true
break
}
if (age > HorizonMilliseconds) {
break
}
dataPoints[sampleCount] = sample.dataPoint
time[sampleCount] = -age
index = (if (index == 0) HistorySize else index) - 1
sampleCount += 1
} while (sampleCount < HistorySize)
if (sampleCount >= minSampleSize && shouldUseDataPoints(
dataPoints,
time,
sampleCount,
afterPointerStop
)
) {
// Choose computation logic based on strategy.
return when (strategy) {
Strategy.Impulse -> {
calculateImpulseVelocity(dataPoints, time, sampleCount, isDataDifferential)
}
Strategy.Lsq2 -> {
calculateLeastSquaresVelocity(dataPoints, time, sampleCount)
}
} * 1000 // Multiply by "1000" to convert from units/ms to units/s
}
// We're unable to make a velocity estimate but we did have at least one
// valid pointer position.
return 0f
}
/**
* Computes the estimated velocity at the time of the last provided data point.
*
* The method allows specifying the maximum absolute value for the calculated
* velocity. If the absolute value of the calculated velocity exceeds the specified
* maximum, the return value will be clamped down to the maximum. For example, if
* the absolute maximum velocity is specified as "20", a calculated velocity of "25"
* will be returned as "20", and a velocity of "-30" will be returned as "-20".
*
* @param maximumVelocity the absolute value of the maximum velocity to be returned in
* units/second, where `units` is the units of the positions provided to this VelocityTracker.
*/
fun calculateVelocity(maximumVelocity: Float): Float {
checkPrecondition(maximumVelocity > 0f) {
"maximumVelocity should be a positive value. You specified=$maximumVelocity"
}
val velocity = calculateVelocity()
return if (velocity == 0.0f || velocity.isNaN()) {
0.0f
} else if (velocity > 0) {
velocity.coerceAtMost(maximumVelocity)
} else {
velocity.coerceAtLeast(-maximumVelocity)
}
}
/**
* Clears data points added by [addDataPoint].
*/
fun resetTracking() {
samples.fill(element = null)
index = 0
}
/**
* Calculates velocity based on [Strategy.Lsq2]. The provided [time] entries are in "ms", and
* should be provided in reverse chronological order. The returned velocity is in "units/ms",
* where "units" is unit of the [dataPoints].
*/
private fun calculateLeastSquaresVelocity(
dataPoints: FloatArray,
time: FloatArray,
sampleCount: Int
): Float {
// The 2nd coefficient is the derivative of the quadratic polynomial at
// x = 0, and that happens to be the last timestamp that we end up
// passing to polyFitLeastSquares.
return try {
polyFitLeastSquares(
time,
dataPoints,
sampleCount,
2,
reusableVelocityCoefficients
)[1]
} catch (exception: IllegalArgumentException) {
0f
}
}
}
/**
* Extension to simplify either creating a new [DataPointAtTime] at an array index (if the index
* was never populated), or to update an existing [DataPointAtTime] (if the index had an existing
* element). This helps to have zero allocations on average, and avoid performance hit that can be
* caused by creating lots of objects.
*/
private fun Array.set(index: Int, time: Long, dataPoint: Float) {
val currentEntry = this[index]
if (currentEntry == null) {
this[index] = DataPointAtTime(time, dataPoint)
} else {
currentEntry.time = time
currentEntry.dataPoint = dataPoint
}
}
/**
* Some platforms (e.g. iOS) ignore certain events during velocity calculation.
*/
internal expect fun VelocityTracker1D.shouldUseDataPoints(
points: FloatArray,
times: FloatArray,
count: Int,
afterPointerStop: Boolean
): Boolean
/**
* Track the positions and timestamps inside this event change.
*
* For optimal tracking, this should be called for the DOWN event and all MOVE
* events, including any touch-slop-captured MOVE event.
*
* Since Compose uses relative positions inside PointerInputChange, this should be
* taken into consideration when using this method. Right now, we use the first down
* to initialize an accumulator and use subsequent deltas to simulate an actual movement
* from relative positions in PointerInputChange. This is required because VelocityTracker
* requires data that can be fit into a curve, which might not happen with relative positions
* inside a moving target for instance.
*
* @param event Pointer change to track.
*/
@OptIn(ExperimentalComposeUiApi::class)
fun VelocityTracker.addPointerInputChange(event: PointerInputChange) {
if (VelocityTrackerAddPointsFix) {
addPointerInputChangeWithFix(event)
} else {
addPointerInputChangeLegacy(event)
}
}
@OptIn(ExperimentalComposeUiApi::class)
private fun VelocityTracker.addPointerInputChangeLegacy(event: PointerInputChange) {
// Register down event as the starting point for the accumulator
if (event.changedToDownIgnoreConsumed()) {
currentPointerPositionAccumulator = event.position
resetTracking()
}
// To calculate delta, for each step we want to do currentPosition - previousPosition.
// Initially the previous position is the previous position of the current event
var previousPointerPosition = event.previousPosition
@OptIn(ExperimentalComposeUiApi::class)
event.historical.fastForEach {
// Historical data happens within event.position and event.previousPosition
// That means, event.previousPosition < historical data < event.position
// Initially, the first delta will happen between the previousPosition and
// the first position in historical delta. For subsequent historical data, the
// deltas happen between themselves. That's why we need to update previousPointerPosition
// everytime.
val historicalDelta = it.position - previousPointerPosition
previousPointerPosition = it.position
// Update the current position with the historical delta and add it to the tracker
currentPointerPositionAccumulator += historicalDelta
addPosition(it.uptimeMillis, currentPointerPositionAccumulator)
}
// For the last position in the event
// If there's historical data, the delta is event.position - lastHistoricalPoint
// If there's no historical data, the delta is event.position - event.previousPosition
val delta = event.position - previousPointerPosition
currentPointerPositionAccumulator += delta
addPosition(event.uptimeMillis, currentPointerPositionAccumulator)
}
private fun VelocityTracker.addPointerInputChangeWithFix(event: PointerInputChange) {
// If this is ACTION_DOWN: Reset the tracking.
if (event.changedToDownIgnoreConsumed()) {
resetTracking()
}
// If this is not ACTION_UP event: Add events to the tracker as per the platform implementation.
// In the platform implementation the historical events array is used, they store the current
// event data in the position HistoricalArray.Size. Our historical array doesn't have access
// to the final position, but we can get that information from the original event data X and Y
// coordinates.
@OptIn(ExperimentalComposeUiApi::class)
if (!event.changedToUpIgnoreConsumed()) {
event.historical.fastForEach {
addPosition(it.uptimeMillis, it.originalEventPosition)
}
addPosition(event.uptimeMillis, event.originalEventPosition)
}
// If this is ACTION_UP. Fix for b/238654963. If there's been enough time after the last MOVE
// event, reset the tracker.
if (event.changedToUpIgnoreConsumed() && (event.uptimeMillis - lastMoveEventTimeStamp) > 40L) {
resetTracking()
}
lastMoveEventTimeStamp = event.uptimeMillis
}
internal data class DataPointAtTime(var time: Long, var dataPoint: Float)
/**
* TODO (shepshapard): If we want to support varying weights for each position, we could accept a
* 3rd FloatArray of weights for each point and use them instead of the [DefaultWeight].
*/
private const val DefaultWeight = 1f
/**
* Fits a polynomial of the given degree to the data points.
*
* If the [degree] is larger than or equal to the number of points, a polynomial will be returned
* with coefficients of the value 0 for all degrees larger than or equal to the number of points.
* For example, if 2 data points are provided and a quadratic polynomial (degree of 2) is requested,
* the resulting polynomial ax^2 + bx + c is guaranteed to have a = 0;
*
* Throws an IllegalArgumentException if:
*
* - [degree] is not a positive integer.
*
- [sampleCount] is zero.
*
*
*/
internal fun polyFitLeastSquares(
/** The x-coordinates of each data point. */
x: FloatArray,
/** The y-coordinates of each data point. */
y: FloatArray,
/** number of items in each array */
sampleCount: Int,
degree: Int,
coefficients: FloatArray = FloatArray((degree + 1).coerceAtLeast(0))
): FloatArray {
if (degree < 1) {
throwIllegalArgumentException("The degree must be at positive integer")
}
if (sampleCount == 0) {
throwIllegalArgumentException("At least one point must be provided")
}
val truncatedDegree =
if (degree >= sampleCount) {
sampleCount - 1
} else {
degree
}
// Shorthands for the purpose of notation equivalence to original C++ code.
val m = sampleCount
val n = truncatedDegree + 1
// Expand the X vector to a matrix A, pre-multiplied by the weights.
val a = Matrix(n, m)
for (h in 0 until m) {
a[0, h] = DefaultWeight
for (i in 1 until n) {
a[i, h] = a[i - 1, h] * x[h]
}
}
// Apply the Gram-Schmidt process to A to obtain its QR decomposition.
// Orthonormal basis, column-major order.
val q = Matrix(n, m)
// Upper triangular matrix, row-major order.
val r = Matrix(n, n)
for (j in 0 until n) {
val w = q[j]
a[j].copyInto(w, 0, 0, m)
for (i in 0 until j) {
val z = q[i]
val dot = w.dot(z)
for (h in 0 until m) {
w[h] -= dot * z[h]
}
}
val inverseNorm = 1.0f / w.norm().fastCoerceAtLeast(1e-6f)
for (h in 0 until m) {
w[h] *= inverseNorm
}
val v = r[j]
for (i in 0 until n) {
v[i] = if (i < j) 0.0f else w.dot(a[i])
}
}
// Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
// We just work from bottom-right to top-left calculating B's coefficients.
var wy = y
// NOTE: DefaultWeight is currently always set to 1.0f, there's no need to allocate a new
// array and to perform several multiplications for no reason
@Suppress("KotlinConstantConditions")
if (DefaultWeight != 1.0f) {
// TODO: Even when we pass the test above, this allocation is likely unnecessary.
// We could just modify wy (y) in place instead. This would need to be documented
// to avoid surprises for the caller though.
wy = FloatArray(m)
for (h in 0 until m) {
wy[h] = y[h] * DefaultWeight
}
}
for (i in n - 1 downTo 0) {
var c = q[i].dot(wy)
val ri = r[i]
for (j in n - 1 downTo i + 1) {
c -= ri[j] * coefficients[j]
}
coefficients[i] = c / ri[i]
}
return coefficients
}
/**
* Calculates velocity based on the Impulse strategy. The provided [time] entries are in "ms", and
* should be provided in reverse chronological order. The returned velocity is in "units/ms",
* where "units" is unit of the [dataPoints].
*
* Calculates the resulting velocity based on the total impulse provided by the data points.
*
* The moving object in these calculations is the touchscreen (if we are calculating touch
* velocity), or any input device from which the data points are generated. We refer to this
* object as the "subject" below.
*
* Initial condition is discussed below, but for now suppose that v(t=0) = 0
*
* The kinetic energy of the object at the release is E=0.5*m*v^2
* Then vfinal = sqrt(2E/m). The goal is to calculate E.
*
* The kinetic energy at the release is equal to the total work done on the object by the finger.
* The total work W is the sum of all dW along the path.
*
* dW = F*dx, where dx is the piece of path traveled.
* Force is change of momentum over time, F = dp/dt = m dv/dt.
* Then substituting:
* dW = m (dv/dt) * dx = m * v * dv
*
* Summing along the path, we get:
* W = sum(dW) = sum(m * v * dv) = m * sum(v * dv)
* Since the mass stays constant, the equation for final velocity is:
* vfinal = sqrt(2*sum(v * dv))
*
* Here,
* dv : change of velocity = (v[i+1]-v[i])
* dx : change of distance = (x[i+1]-x[i])
* dt : change of time = (t[i+1]-t[i])
* v : instantaneous velocity = dx/dt
*
* The final formula is:
* vfinal = sqrt(2) * sqrt(sum((v[i]-v[i-1])*|v[i]|)) for all i
* The absolute value is needed to properly account for the sign. If the velocity over a
* particular segment decreases, then this indicates braking, which means that negative
* work was done. So for two positive, but decreasing, velocities, this contribution would be
* negative and will cause a smaller final velocity.
*
* Initial condition
* There are two ways to deal with initial condition:
* 1) Assume that v(0) = 0, which would mean that the subject is initially at rest.
* This is not entirely accurate. We are only taking the past X ms of touch data, where X is
* currently equal to 100. However, a touch event that created a fling probably lasted for longer
* than that, which would mean that the user has already been interacting with the subject, and
* it has probably already been moving.
* 2) Assume that the subject has already been moving at a certain velocity, calculate this
* initial velocity and the equivalent energy, and start with this initial energy.
* Consider an example where we have the following data, consisting of 3 points:
* time: t0, t1, t2
* x : x0, x1, x2
* v : 0, v1, v2
* Here is what will happen in each of these scenarios:
* 1) By directly applying the formula above with the v(0) = 0 boundary condition, we will get
* vfinal = sqrt(2*(|v1|*(v1-v0) + |v2|*(v2-v1))). This can be simplified since v0=0
* vfinal = sqrt(2*(|v1|*v1 + |v2|*(v2-v1))) = sqrt(2*(v1^2 + |v2|*(v2 - v1)))
* since velocity is a real number
* 2) If we treat the subject as already moving, then it must already have an energy (per mass)
* equal to 1/2*v1^2. Then the initial energy should be 1/2*v1*2, and only the second segment
* will contribute to the total kinetic energy (since we can effectively consider that v0=v1).
* This will give the following expression for the final velocity:
* vfinal = sqrt(2*(1/2*v1^2 + |v2|*(v2-v1)))
* This analysis can be generalized to an arbitrary number of samples.
*
*
* Comparing the two equations above, we see that the only mathematical difference
* is the factor of 1/2 in front of the first velocity term.
* This boundary condition would allow for the "proper" calculation of the case when all of the
* samples are equally spaced in time and distance, which should suggest a constant velocity.
*
* Note that approach 2) is sensitive to the proper ordering of the data in time, since
* the boundary condition must be applied to the oldest sample to be accurate.
*
* NOTE: [sampleCount] MUST be >= 2
*/
private fun calculateImpulseVelocity(
dataPoints: FloatArray,
time: FloatArray,
sampleCount: Int,
isDataDifferential: Boolean
): Float {
var work = 0f
val start = sampleCount - 1
var nextTime = time[start]
for (i in start downTo 1) {
val currentTime = nextTime
nextTime = time[i - 1]
if (currentTime == nextTime) {
continue
}
val dataPointsDelta =
if (isDataDifferential) -dataPoints[i - 1]
else dataPoints[i] - dataPoints[i - 1]
val vCurr = dataPointsDelta / (currentTime - nextTime)
val vPrev = kineticEnergyToVelocity(work)
work += (vCurr - vPrev) * abs(vCurr)
if (i == start) {
work = (work * 0.5f)
}
}
return kineticEnergyToVelocity(work)
}
/**
* Calculates the velocity for a given [kineticEnergy], using the formula:
* Kinetic Energy = 0.5 * mass * (velocity)^2
* where a mass of "1" is used.
*/
@Suppress("NOTHING_TO_INLINE")
private inline fun kineticEnergyToVelocity(kineticEnergy: Float): Float {
return sign(kineticEnergy) * sqrt(2 * abs(kineticEnergy))
}
private typealias Vector = FloatArray
private fun FloatArray.dot(a: FloatArray): Float {
var result = 0.0f
for (i in indices) {
result += this[i] * a[i]
}
return result
}
@Suppress("NOTHING_TO_INLINE")
private inline fun FloatArray.norm(): Float = sqrt(this.dot(this))
private typealias Matrix = Array
@Suppress("NOTHING_TO_INLINE")
private inline fun Matrix(rows: Int, cols: Int) = Array(rows) { Vector(cols) }
@Suppress("NOTHING_TO_INLINE")
private inline operator fun Matrix.get(row: Int, col: Int): Float = this[row][col]
@Suppress("NOTHING_TO_INLINE")
private inline operator fun Matrix.set(row: Int, col: Int, value: Float) {
this[row][col] = value
}
/**
* A flag to indicate that we'll use the fix of how we add points to the velocity tracker.
*
* This is an experiment flag and will be removed once the experiments with the fix a finished. The
* final goal is that we will use the true path once the flag is removed. If you find any issues
* with the new fix, flip this flag to false to confirm they are newly introduced then file a bug.
* Tracking bug: (b/318621681)
*/
@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
@get:Suppress("GetterSetterNames")
@get:ExperimentalComposeUiApi
@set:ExperimentalComposeUiApi
@ExperimentalComposeUiApi
var VelocityTrackerAddPointsFix: Boolean = true
/**
* Selecting flag to enable impulse strategy for the velocity trackers.
* This is an experiment flag and will be removed once the experiments with the fix a finished. The
* final goal is that we will use the true path once the flag is removed. If you find any issues
* with the new fix, flip this flag to false to confirm they are newly introduced then file a bug.
* Tracking bug: (b/318621681)
*/
@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
@get:Suppress("GetterSetterNames")
@get:ExperimentalComposeUiApi
@set:ExperimentalComposeUiApi
@ExperimentalComposeUiApi
var VelocityTrackerStrategyUseImpulse = false
© 2015 - 2025 Weber Informatics LLC | Privacy Policy