VelocityTrackerFallback.java
/*
* Copyright 2023 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.core.view;
import static androidx.core.view.MotionEventCompat.AXIS_SCROLL;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
/**
* A fallback implementation of {@link android.view.VelocityTracker}. The methods its provide
* mirror the platform's implementation.
*
* <p>It will be used to provide velocity tracking logic for certain axes that may not be
* supported at different API levels, so that {@link VelocityTrackerCompat} can provide compat
* service to its clients.
*
* <p>Currently, it supports AXIS_SCROLL with the default pointer ID.
*/
class VelocityTrackerFallback {
private static final long RANGE_MS = 100L;
private static final int HISTORY_SIZE = 20;
/**
* If there's no data beyond this period of time, we assume that the previous chain of motion
* from the pointer has stopped, and we handle subsequent data points separately.
*/
private static final long ASSUME_POINTER_STOPPED_MS = 40L;
private final float[] mMovements = new float[HISTORY_SIZE];
private final long[] mEventTimes = new long[HISTORY_SIZE];
/** Cached value of the last computed velocity, for a O(1) get operation. */
private float mLastComputedVelocity = 0f;
/** Number of data points that are potential velocity calculation candidates. */
private int mDataPointsBufferSize = 0;
/**
* The last index in the circular buffer where a data point was added. Irrelevant if {@code
* dataPointsBufferSize} == 0.
*/
private int mDataPointsBufferLastUsedIndex = 0;
/** Adds a motion for velocity tracking. */
void addMovement(@NonNull MotionEvent event) {
long eventTime = event.getEventTime();
if (mDataPointsBufferSize != 0
&& (eventTime - mEventTimes[mDataPointsBufferLastUsedIndex]
> ASSUME_POINTER_STOPPED_MS)) {
// There has been at least `ASSUME_POINTER_STOPPED_MS` since the last recorded event.
// When this happens, consider that the pointer has stopped until this new event. Thus,
// clear all past events.
clear();
}
mDataPointsBufferLastUsedIndex = (mDataPointsBufferLastUsedIndex + 1) % HISTORY_SIZE;
// We do not need to increase size if the size is already `HISTORY_SIZE`, since we always
// will have at most `HISTORY_SIZE` data points stored, due to the circular buffer.
if (mDataPointsBufferSize != HISTORY_SIZE) {
mDataPointsBufferSize += 1;
}
mMovements[mDataPointsBufferLastUsedIndex] = event.getAxisValue(AXIS_SCROLL);
mEventTimes[mDataPointsBufferLastUsedIndex] = eventTime;
}
/** Same as {@link #computeCurrentVelocity} with {@link Float#MAX_VALUE} as the max velocity. */
void computeCurrentVelocity(int units) {
computeCurrentVelocity(units, Float.MAX_VALUE);
}
/** Computes the current velocity with the given unit and max velocity. */
void computeCurrentVelocity(int units, float maxVelocity) {
mLastComputedVelocity = getCurrentVelocity() * units;
// Fix the velocity as per the max velocity
// (i.e. clamp it between [-maxVelocity, maxVelocity])
if (mLastComputedVelocity < -Math.abs(maxVelocity)) {
mLastComputedVelocity = -Math.abs(maxVelocity);
} else if (mLastComputedVelocity > Math.abs(maxVelocity)) {
mLastComputedVelocity = Math.abs(maxVelocity);
}
}
/** Returns the computed velocity for the given {@code axis}. */
float getAxisVelocity(int axis) {
if (axis != AXIS_SCROLL) {
return 0;
}
return mLastComputedVelocity;
}
private void clear() {
mDataPointsBufferSize = 0;
mLastComputedVelocity = 0;
}
private float getCurrentVelocity() {
// At least 2 data points needed to get Impulse velocity.
if (mDataPointsBufferSize < 2) {
return 0f;
}
// The first valid index that contains a data point that should be part of the velocity
// calculation, as long as it's within `RANGE_MS` from the latest data point.
int firstValidIndex =
(mDataPointsBufferLastUsedIndex + HISTORY_SIZE - (mDataPointsBufferSize - 1))
% HISTORY_SIZE;
long lastEventTime = mEventTimes[mDataPointsBufferLastUsedIndex];
while (lastEventTime - mEventTimes[firstValidIndex] > RANGE_MS) {
// Decrementing the size is equivalent to practically "removing" this data point.
mDataPointsBufferSize--;
// Increment the `firstValidIndex`, since we just found out that the current
// `firstValidIndex` is not valid (not within `RANGE_MS`).
firstValidIndex = (firstValidIndex + 1) % HISTORY_SIZE;
}
// At least 2 data points needed to get Impulse velocity.
if (mDataPointsBufferSize < 2) {
return 0;
}
if (mDataPointsBufferSize == 2) {
int lastIndex = (firstValidIndex + 1) % HISTORY_SIZE;
if (mEventTimes[firstValidIndex] == mEventTimes[lastIndex]) {
return 0f;
}
return mMovements[lastIndex] / (mEventTimes[lastIndex] - mEventTimes[firstValidIndex]);
}
float work = 0;
int numDataPointsProcessed = 0;
// Loop from the `firstValidIndex`, to the "second to last" valid index. We need to go only
// to the "second to last" element, since the body of the loop checks against the next data
// point, so we cannot go all the way to the end.
for (int i = 0; i < mDataPointsBufferSize - 1; i++) {
int currentIndex = i + firstValidIndex;
long eventTime = mEventTimes[currentIndex % HISTORY_SIZE];
int nextIndex = (currentIndex + 1) % HISTORY_SIZE;
// Duplicate timestamp. Skip this data point.
if (mEventTimes[nextIndex] == eventTime) {
continue;
}
numDataPointsProcessed++;
float vPrev = kineticEnergyToVelocity(work);
float delta = mMovements[nextIndex];
float vCurr = delta / (mEventTimes[nextIndex] - eventTime);
work += (vCurr - vPrev) * Math.abs(vCurr);
// Note that we are intentionally checking against `numDataPointsProcessed`, instead of
// just checking `i` against `firstValidIndex`. This is to cover cases where there are
// multiple data points that have the same timestamp as the one at `firstValidIndex`.
if (numDataPointsProcessed == 1) {
work = work * 0.5f;
}
}
return kineticEnergyToVelocity(work);
}
/** Based on the formula: Kinetic Energy = (0.5 * mass * velocity^2), with mass = 1. */
private static float kineticEnergyToVelocity(float work) {
return (work < 0 ? -1.0f : 1.0f) * (float) Math.sqrt(2f * Math.abs(work));
}
}