1 /*
2  * Copyright 2023 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.core.view;
18 
19 import static androidx.core.view.MotionEventCompat.AXIS_SCROLL;
20 
21 import android.view.MotionEvent;
22 
23 import org.jspecify.annotations.NonNull;
24 
25 /**
26  * A fallback implementation of {@link android.view.VelocityTracker}. The methods its provide
27  * mirror the platform's implementation.
28  *
29  * <p>It will be used to provide velocity tracking logic for certain axes that may not be
30  * supported at different API levels, so that {@link VelocityTrackerCompat} can provide compat
31  * service to its clients.
32  *
33  * <p>Currently, it supports AXIS_SCROLL with the default pointer ID.
34  */
35 class VelocityTrackerFallback {
36     private static final long RANGE_MS = 100L;
37     private static final int HISTORY_SIZE = 20;
38     /**
39      * If there's no data beyond this period of time, we assume that the previous chain of motion
40      * from the pointer has stopped, and we handle subsequent data points separately.
41      */
42     private static final long ASSUME_POINTER_STOPPED_MS = 40L;
43 
44     private final float[] mMovements = new float[HISTORY_SIZE];
45     private final long[] mEventTimes = new long[HISTORY_SIZE];
46 
47     /** Cached value of the last computed velocity, for a O(1) get operation. */
48     private float mLastComputedVelocity = 0f;
49 
50     /** Number of data points that are potential velocity calculation candidates. */
51     private int mDataPointsBufferSize = 0;
52     /**
53      * The last index in the circular buffer where a data point was added. Irrelevant if {@code
54      * dataPointsBufferSize} == 0.
55      */
56     private int mDataPointsBufferLastUsedIndex = 0;
57 
58     /** Adds a motion for velocity tracking. */
addMovement(@onNull MotionEvent event)59     void addMovement(@NonNull MotionEvent event) {
60         long eventTime = event.getEventTime();
61         if (mDataPointsBufferSize != 0
62                 && (eventTime - mEventTimes[mDataPointsBufferLastUsedIndex]
63                 > ASSUME_POINTER_STOPPED_MS)) {
64             // There has been at least `ASSUME_POINTER_STOPPED_MS` since the last recorded event.
65             // When this happens, consider that the pointer has stopped until this new event. Thus,
66             // clear all past events.
67             clear();
68         }
69 
70         mDataPointsBufferLastUsedIndex = (mDataPointsBufferLastUsedIndex + 1) % HISTORY_SIZE;
71         // We do not need to increase size if the size is already `HISTORY_SIZE`, since we always
72         // will  have at most `HISTORY_SIZE` data points stored, due to the circular buffer.
73         if (mDataPointsBufferSize != HISTORY_SIZE) {
74             mDataPointsBufferSize += 1;
75         }
76 
77         mMovements[mDataPointsBufferLastUsedIndex] = event.getAxisValue(AXIS_SCROLL);
78         mEventTimes[mDataPointsBufferLastUsedIndex] = eventTime;
79     }
80 
81     /** Same as {@link #computeCurrentVelocity} with {@link Float#MAX_VALUE} as the max velocity. */
computeCurrentVelocity(int units)82     void computeCurrentVelocity(int units) {
83         computeCurrentVelocity(units, Float.MAX_VALUE);
84     }
85 
86     /** Computes the current velocity with the given unit and max velocity. */
computeCurrentVelocity(int units, float maxVelocity)87     void computeCurrentVelocity(int units, float maxVelocity) {
88         mLastComputedVelocity = getCurrentVelocity() * units;
89 
90         // Fix the velocity as per the max velocity
91         // (i.e. clamp it between [-maxVelocity, maxVelocity])
92         if (mLastComputedVelocity < -Math.abs(maxVelocity)) {
93             mLastComputedVelocity = -Math.abs(maxVelocity);
94         } else if (mLastComputedVelocity > Math.abs(maxVelocity)) {
95             mLastComputedVelocity = Math.abs(maxVelocity);
96         }
97     }
98 
99     /** Returns the computed velocity for the given {@code axis}. */
getAxisVelocity(int axis)100     float getAxisVelocity(int axis) {
101         if (axis != AXIS_SCROLL) {
102             return 0;
103         }
104         return mLastComputedVelocity;
105     }
106 
clear()107     private void clear() {
108         mDataPointsBufferSize = 0;
109         mLastComputedVelocity = 0;
110     }
111 
getCurrentVelocity()112     private float getCurrentVelocity() {
113         // At least 2 data points needed to get Impulse velocity.
114         if (mDataPointsBufferSize < 2) {
115             return 0f;
116         }
117 
118         // The first valid index that contains a data point that should be part of the velocity
119         // calculation, as long as it's within `RANGE_MS` from the latest data point.
120         int firstValidIndex =
121                 (mDataPointsBufferLastUsedIndex + HISTORY_SIZE - (mDataPointsBufferSize - 1))
122                         % HISTORY_SIZE;
123         long lastEventTime = mEventTimes[mDataPointsBufferLastUsedIndex];
124         while (lastEventTime - mEventTimes[firstValidIndex] > RANGE_MS) {
125             // Decrementing the size is equivalent to practically "removing" this data point.
126             mDataPointsBufferSize--;
127             // Increment the `firstValidIndex`, since we just found out that the current
128             // `firstValidIndex` is not valid (not within `RANGE_MS`).
129             firstValidIndex = (firstValidIndex + 1) % HISTORY_SIZE;
130         }
131 
132         // At least 2 data points needed to get Impulse velocity.
133         if (mDataPointsBufferSize < 2) {
134             return 0;
135         }
136 
137         if (mDataPointsBufferSize == 2) {
138             int lastIndex = (firstValidIndex + 1) % HISTORY_SIZE;
139             if (mEventTimes[firstValidIndex] == mEventTimes[lastIndex]) {
140                 return 0f;
141             }
142             return mMovements[lastIndex] / (mEventTimes[lastIndex] - mEventTimes[firstValidIndex]);
143         }
144 
145         float work = 0;
146         int numDataPointsProcessed = 0;
147         // Loop from the `firstValidIndex`, to the "second to last" valid index. We need to go only
148         // to the "second to last" element, since the body of the loop checks against the next data
149         // point, so we cannot go all the way to the end.
150         for (int i = 0; i < mDataPointsBufferSize - 1; i++) {
151             int currentIndex = i + firstValidIndex;
152             long eventTime = mEventTimes[currentIndex % HISTORY_SIZE];
153             int nextIndex = (currentIndex + 1) % HISTORY_SIZE;
154 
155             // Duplicate timestamp. Skip this data point.
156             if (mEventTimes[nextIndex] == eventTime) {
157                 continue;
158             }
159 
160             numDataPointsProcessed++;
161             float vPrev = kineticEnergyToVelocity(work);
162             float delta = mMovements[nextIndex];
163             float vCurr = delta / (mEventTimes[nextIndex] - eventTime);
164 
165             work += (vCurr - vPrev) * Math.abs(vCurr);
166 
167             // Note that we are intentionally checking against `numDataPointsProcessed`, instead of
168             // just checking `i` against `firstValidIndex`. This is to cover cases where there are
169             // multiple data points that have the same timestamp as the one at `firstValidIndex`.
170             if (numDataPointsProcessed == 1) {
171                 work = work * 0.5f;
172             }
173         }
174 
175         return kineticEnergyToVelocity(work);
176     }
177 
178     /** Based on the formula: Kinetic Energy = (0.5 * mass * velocity^2), with mass = 1. */
kineticEnergyToVelocity(float work)179     private static float kineticEnergyToVelocity(float work) {
180         return (work < 0 ? -1.0f : 1.0f) * (float) Math.sqrt(2f * Math.abs(work));
181     }
182 }
183