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