Skip to content

Commit f229c07

Browse files
Yeabkal WubshitGerrit Code Review
Yeabkal Wubshit
authored and
Gerrit Code Review
committed
Merge "Refactor VelocityTracker Logic Into One Dimension" into androidx-main
2 parents 3ffc4d5 + e0cd398 commit f229c07

File tree

1 file changed

+78
-46
lines changed
  • compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util

1 file changed

+78
-46
lines changed

compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt

+78-46
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ private const val MinSampleSize: Int = 3
4646
*/
4747
class VelocityTracker {
4848

49-
// Circular buffer; current sample at index.
50-
private val samples: Array<PointAtTime?> = Array(HistorySize) { null }
51-
private var index: Int = 0
49+
private val xVelocityTracker = VelocityTracker1D()
50+
private val yVelocityTracker = VelocityTracker1D()
51+
5252
internal var currentPointerPositionAccumulator = Offset.Zero
5353

5454
/**
@@ -62,8 +62,8 @@ class VelocityTracker {
6262
// positions. For velocity tracking, the only thing that is important is the change in
6363
// position over time.
6464
fun addPosition(timeMillis: Long, position: Offset) {
65-
index = (index + 1) % HistorySize
66-
samples[index] = PointAtTime(position, timeMillis)
65+
xVelocityTracker.addDataPoint(timeMillis, position.x)
66+
yVelocityTracker.addDataPoint(timeMillis, position.y)
6767
}
6868

6969
/**
@@ -72,42 +72,85 @@ class VelocityTracker {
7272
* This can be expensive. Only call this when you need the velocity.
7373
*/
7474
fun calculateVelocity(): Velocity {
75-
val estimate = getVelocityEstimate().pixelsPerSecond
76-
return Velocity(estimate.x, estimate.y)
75+
return Velocity(xVelocityTracker.calculateVelocity(), yVelocityTracker.calculateVelocity())
7776
}
7877

7978
/**
8079
* Clears the tracked positions added by [addPosition].
8180
*/
81+
fun resetTracking() {
82+
xVelocityTracker.resetTracking()
83+
yVelocityTracker.resetTracking()
84+
}
85+
}
86+
87+
/**
88+
* A velocity tracker calculating velocity in 1 dimension.
89+
*
90+
* Add displacement data points using [addDataPoint], and obtain velocity using [calculateVelocity].
91+
*/
92+
private class VelocityTracker1D {
93+
// Circular buffer; current sample at index.
94+
private val samples: Array<DataPointAtTime?> = Array(HistorySize) { null }
95+
private var index: Int = 0
96+
97+
/**
98+
* Adds a data point for velocity calculation. A data point should represent a position along
99+
* the tracked axis at a given time, [timeMillis].
100+
*
101+
* Use the same units for the data points provided. For example, having some data points in `cm`
102+
* and some in `m` will result in incorrect velocity calculations, as this method (and the
103+
* tracker) has no knowledge of the units used.
104+
*/
105+
fun addDataPoint(timeMillis: Long, dataPoint: Float) {
106+
index = (index + 1) % HistorySize
107+
samples[index] = DataPointAtTime(dataPoint, timeMillis)
108+
}
109+
110+
/**
111+
* Computes the estimated velocity at the time of the last provided data point. The units of
112+
* velocity will be `units/second`, where `units` is the units of the data points provided via
113+
* [addDataPoint].
114+
*
115+
* This can be expensive. Only call this when you need the velocity.
116+
*/
117+
fun calculateVelocity(): Float {
118+
return getVelocityEstimate().velocity
119+
}
120+
121+
/**
122+
* Clears the tracked positions added by [addDataPoint].
123+
*/
82124
fun resetTracking() {
83125
samples.fill(element = null)
126+
index = 0
84127
}
85128

86129
/**
87130
* Returns an estimate of the velocity of the object being tracked by the
88131
* tracker given the current information available to the tracker.
89132
*
90-
* Information is added using [addPosition].
133+
* Information is added using [addDataPoint].
91134
*
92-
* Returns null if there is no data on which to base an estimate.
135+
* Returns an estimate of 0 velocity if there is no data on which to base an estimate.
93136
*/
137+
94138
private fun getVelocityEstimate(): VelocityEstimate {
95-
val x: MutableList<Float> = mutableListOf()
96-
val y: MutableList<Float> = mutableListOf()
139+
val dataPoints: MutableList<Float> = mutableListOf()
97140
val time: MutableList<Float> = mutableListOf()
98141
var sampleCount = 0
99142
var index: Int = index
100143

101144
// The sample at index is our newest sample. If it is null, we have no samples so return.
102-
val newestSample: PointAtTime = samples[index] ?: return VelocityEstimate.None
145+
val newestSample: DataPointAtTime = samples[index] ?: return VelocityEstimate.None
103146

104-
var previousSample: PointAtTime = newestSample
105-
var oldestSample: PointAtTime = newestSample
147+
var previousSample: DataPointAtTime = newestSample
148+
var oldestSample: DataPointAtTime = newestSample
106149

107150
// Starting with the most recent PointAtTime sample, iterate backwards while
108151
// the samples represent continuous motion.
109152
do {
110-
val sample: PointAtTime = samples[index] ?: break
153+
val sample: DataPointAtTime = samples[index] ?: break
111154

112155
val age: Float = (newestSample.time - sample.time).toFloat()
113156
val delta: Float =
@@ -118,9 +161,7 @@ class VelocityTracker {
118161
}
119162

120163
oldestSample = sample
121-
val position: Offset = sample.point
122-
x.add(position.x)
123-
y.add(position.y)
164+
dataPoints.add(sample.dataPoint)
124165
time.add(-age)
125166
index = (if (index == 0) HistorySize else index) - 1
126167

@@ -129,23 +170,17 @@ class VelocityTracker {
129170

130171
if (sampleCount >= MinSampleSize) {
131172
try {
132-
val xFit: PolynomialFit = polyFitLeastSquares(time, x, 2)
133-
val yFit: PolynomialFit = polyFitLeastSquares(time, y, 2)
134-
173+
val fit = polyFitLeastSquares(time, dataPoints, 2)
135174
// The 2nd coefficient is the derivative of the quadratic polynomial at
136175
// x = 0, and that happens to be the last timestamp that we end up
137-
// passing to polyFitLeastSquares for both x and y.
138-
val xSlope = xFit.coefficients[1]
139-
val ySlope = yFit.coefficients[1]
176+
// passing to polyFitLeastSquares.
177+
val slope = fit.coefficients[1]
140178
return VelocityEstimate(
141-
pixelsPerSecond = Offset(
142-
// Convert from pixels/ms to pixels/s
143-
(xSlope * 1000),
144-
(ySlope * 1000)
145-
),
146-
confidence = xFit.confidence * yFit.confidence,
179+
// Convert from units/ms to units/s
180+
velocity = slope * 1000,
181+
confidence = fit.confidence,
147182
durationMillis = newestSample.time - oldestSample.time,
148-
offset = newestSample.point - oldestSample.point
183+
offset = newestSample.dataPoint - oldestSample.dataPoint
149184
)
150185
} catch (exception: IllegalArgumentException) {
151186
// TODO(b/129494918): Is catching an exception here something we really want to do?
@@ -156,10 +191,10 @@ class VelocityTracker {
156191
// We're unable to make a velocity estimate but we did have at least one
157192
// valid pointer position.
158193
return VelocityEstimate(
159-
pixelsPerSecond = Offset.Zero,
194+
velocity = 0f,
160195
confidence = 1.0f,
161196
durationMillis = newestSample.time - oldestSample.time,
162-
offset = newestSample.point - oldestSample.point
197+
offset = newestSample.dataPoint - oldestSample.dataPoint
163198
)
164199
}
165200
}
@@ -214,26 +249,23 @@ fun VelocityTracker.addPointerInputChange(event: PointerInputChange) {
214249
addPosition(event.uptimeMillis, currentPointerPositionAccumulator)
215250
}
216251

217-
private data class PointAtTime(val point: Offset, val time: Long)
252+
private data class DataPointAtTime(val dataPoint: Float, val time: Long)
218253

219254
/**
220-
* A two dimensional velocity estimate.
255+
* A velocity estimate.
221256
*
222-
* VelocityEstimates are computed by [VelocityTracker.getImpulseVelocity]. An
223-
* estimate's [confidence] measures how well the velocity tracker's position
257+
* An estimate's [confidence] measures how well the velocity tracker's position
224258
* data fit a straight line, [durationMillis] is the time that elapsed between the
225259
* first and last position sample used to compute the velocity, and [offset]
226260
* is similarly the difference between the first and last positions.
227261
*
228262
* See also:
229263
*
230264
* * VelocityTracker, which computes [VelocityEstimate]s.
231-
* * Velocity, which encapsulates (just) a velocity vector and provides some
232-
* useful velocity operations.
233265
*/
234266
private data class VelocityEstimate(
235-
/** The number of pixels per second of velocity in the x and y directions. */
236-
val pixelsPerSecond: Offset,
267+
/** The velocity, in units per second. */
268+
val velocity: Float,
237269
/**
238270
* A value between 0.0 and 1.0 that indicates how well [VelocityTracker]
239271
* was able to fit a straight line to its position data.
@@ -243,17 +275,17 @@ private data class VelocityEstimate(
243275
val confidence: Float,
244276
/**
245277
* The time that elapsed between the first and last position sample used
246-
* to compute [pixelsPerSecond].
278+
* to compute [velocity].
247279
*/
248280
val durationMillis: Long,
249281
/**
250-
* The difference between the first and last position sample used
251-
* to compute [pixelsPerSecond].
282+
* The difference between the first and last datapoint used
283+
* to compute [velocity].
252284
*/
253-
val offset: Offset
285+
val offset: Float
254286
) {
255287
companion object {
256-
val None = VelocityEstimate(Offset.Zero, 1f, 0, Offset.Zero)
288+
val None = VelocityEstimate(0f, 1f, 0, 0f)
257289
}
258290
}
259291

0 commit comments

Comments
 (0)