1 /* 2 * Copyright 2018 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.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 20 21 import static java.lang.annotation.RetentionPolicy.SOURCE; 22 23 import android.os.Build; 24 import android.view.InputDevice; 25 import android.view.MotionEvent; 26 import android.view.VelocityTracker; 27 28 import androidx.annotation.IntDef; 29 import androidx.annotation.RequiresApi; 30 import androidx.annotation.RestrictTo; 31 32 import org.jspecify.annotations.NonNull; 33 import org.jspecify.annotations.Nullable; 34 35 import java.lang.annotation.Retention; 36 import java.util.Collections; 37 import java.util.Map; 38 import java.util.WeakHashMap; 39 40 /** Helper for accessing features in {@link VelocityTracker}. */ 41 public final class VelocityTrackerCompat { 42 @RestrictTo(LIBRARY_GROUP_PREFIX) 43 @Retention(SOURCE) 44 @IntDef(value = { 45 MotionEvent.AXIS_X, 46 MotionEvent.AXIS_Y, 47 MotionEvent.AXIS_SCROLL 48 }) 49 public @interface VelocityTrackableMotionEventAxis {} 50 51 /** 52 * Mapping of platform velocity trackers to their respective fallback. 53 * 54 * <p>This mapping is used to provide a consistent add/clear/getVelocity experience for axes 55 * that may not be supported at a given Android version. Clients can continue to call the 56 * compat's add/clear/compute/getVelocity with the platform tracker instances, and this class 57 * will assign a "fallback" tracker instance for each unique platform tracker instance to 58 * consistently run these operations just as they would run on the platorm instances. 59 * 60 * <p>Since the compat APIs have been provided statically, we will use a singleton compat 61 * instance to manage the mappings whenever we need a "fallback" handling for velocity. 62 * 63 * <p>High level flow for a compat velocity logic for a platform-unsupported axis "A" looks 64 * as follows: 65 * [1]. add(platformTracker, event): 66 * [a] Create fallback tracker, and associate it with "platformTracker`. 67 * [b] Add `event` to the fallback tracker. 68 * [2]. computeCurrentVelocity(platformTracker, event): 69 * [a] If there is no associated fallback tracker for `platformTracker`, exit. 70 * [b] If there's a fallback, compute current velocity for the fallback. 71 * [3]. getAxisVelocity(platformTracker, axis): 72 * [a] If there is no associated fallback tracker for `platformTracker`, exit. 73 * [b] If there's a fallback, return the velocity from the fallback. 74 * [4]. clear/recycle(platformTracker) 75 * [a] Remove any association between `platformTracker` and a fallback tracker. 76 * 77 */ 78 private static Map<VelocityTracker, VelocityTrackerFallback> sFallbackTrackers = 79 Collections.synchronizedMap(new WeakHashMap<>()); 80 81 /** 82 * Call {@link VelocityTracker#getXVelocity(int)}. 83 * If running on a pre-{@link Build.VERSION_CODES#HONEYCOMB} device, 84 * returns {@link VelocityTracker#getXVelocity()}. 85 * 86 * @deprecated Use {@link VelocityTracker#getXVelocity(int)} directly. 87 */ 88 @androidx.annotation.ReplaceWith(expression = "tracker.getXVelocity(pointerId)") 89 @Deprecated getXVelocity(VelocityTracker tracker, int pointerId)90 public static float getXVelocity(VelocityTracker tracker, int pointerId) { 91 return tracker.getXVelocity(pointerId); 92 } 93 94 /** 95 * Call {@link VelocityTracker#getYVelocity(int)}. 96 * If running on a pre-{@link Build.VERSION_CODES#HONEYCOMB} device, 97 * returns {@link VelocityTracker#getYVelocity()}. 98 * 99 * @deprecated Use {@link VelocityTracker#getYVelocity(int)} directly. 100 */ 101 @androidx.annotation.ReplaceWith(expression = "tracker.getYVelocity(pointerId)") 102 @Deprecated getYVelocity(VelocityTracker tracker, int pointerId)103 public static float getYVelocity(VelocityTracker tracker, int pointerId) { 104 return tracker.getYVelocity(pointerId); 105 } 106 107 /** 108 * Checks whether a given velocity-trackable {@link MotionEvent} axis is supported for velocity 109 * tracking by this {@link VelocityTracker} instance (refer to 110 * {@link #getAxisVelocity(VelocityTracker, int, int)} for a list of potentially 111 * velocity-trackable axes). 112 * 113 * <p>Note that the value returned from this method will stay the same for a given instance, so 114 * a single check for axis support is enough per a {@link VelocityTracker} instance. 115 * 116 * @param tracker The {@link VelocityTracker} for which to check axis support. 117 * @param axis The axis to check for velocity support. 118 * @return {@code true} if {@code axis} is supported for velocity tracking, or {@code false} 119 * otherwise. 120 * @see #getAxisVelocity(VelocityTracker, int, int) 121 * @see #getAxisVelocity(VelocityTracker, int) 122 */ isAxisSupported(@onNull VelocityTracker tracker, @VelocityTrackableMotionEventAxis int axis)123 public static boolean isAxisSupported(@NonNull VelocityTracker tracker, 124 @VelocityTrackableMotionEventAxis int axis) { 125 if (Build.VERSION.SDK_INT >= 34) { 126 return Api34Impl.isAxisSupported(tracker, axis); 127 } 128 return axis == MotionEvent.AXIS_SCROLL // Supported via VelocityTrackerFallback. 129 || axis == MotionEvent.AXIS_X // Supported by platform at all API levels. 130 || axis == MotionEvent.AXIS_Y; // Supported by platform at all API levels. 131 } 132 133 /** 134 * Equivalent to calling {@link #getAxisVelocity(VelocityTracker, int, int)} for {@code axis} 135 * and the active pointer. 136 * 137 * @param tracker The {@link VelocityTracker} from which to get axis velocity. 138 * @param axis Which axis' velocity to return. 139 * @return The previously computed velocity for {@code axis} for the active pointer if 140 * {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not 141 * supported for the axis. 142 * @see #isAxisSupported(VelocityTracker, int) 143 * @see #getAxisVelocity(VelocityTracker, int, int) 144 */ getAxisVelocity(@onNull VelocityTracker tracker, @VelocityTrackableMotionEventAxis int axis)145 public static float getAxisVelocity(@NonNull VelocityTracker tracker, 146 @VelocityTrackableMotionEventAxis int axis) { 147 if (Build.VERSION.SDK_INT >= 34) { 148 return Api34Impl.getAxisVelocity(tracker, axis); 149 } 150 151 // For X and Y axes, use the `get*Velocity` APIs that existed at all API levels. 152 if (axis == MotionEvent.AXIS_X) { 153 return tracker.getXVelocity(); 154 } 155 if (axis == MotionEvent.AXIS_Y) { 156 return tracker.getYVelocity(); 157 } 158 159 // For any other axis before API 34, use the corresponding VelocityTrackerFallback, if any, 160 // to determine the velocity. 161 VelocityTrackerFallback fallback = getFallbackTrackerOrNull(tracker); 162 if (fallback != null) { 163 return fallback.getAxisVelocity(axis); 164 } 165 166 return 0; 167 } 168 169 /** 170 * Retrieve the last computed velocity for a given motion axis. You must first call 171 * {@link VelocityTracker#computeCurrentVelocity(int)} or 172 * {@link VelocityTracker#computeCurrentVelocity(int, float)} before calling this function. 173 * 174 * <p>In addition to {@link MotionEvent#AXIS_X} and {@link MotionEvent#AXIS_Y} which have been 175 * supported since the introduction of this class, the following axes can be candidates for this 176 * method: 177 * <ul> 178 * <li> {@link MotionEvent#AXIS_SCROLL}: supported via the platform starting 179 * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}. Supported via a fallback logic at all 180 * platform levels for the active pointer only. 181 * </ul> 182 * 183 * <p>Before accessing velocities of an axis using this method, check that your 184 * {@link VelocityTracker} instance supports the axis by using 185 * {@link #isAxisSupported(VelocityTracker, int)}. 186 * 187 * @param tracker The {@link VelocityTracker} from which to get axis velocity. 188 * @param axis Which axis' velocity to return. 189 * @param pointerId Which pointer's velocity to return. 190 * @return The previously computed velocity for {@code axis} for pointer ID of {@code id} if 191 * {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not 192 * supported for the axis. 193 * @see #isAxisSupported(VelocityTracker, int) 194 */ getAxisVelocity( @onNull VelocityTracker tracker, @VelocityTrackableMotionEventAxis int axis, int pointerId)195 public static float getAxisVelocity( 196 @NonNull VelocityTracker tracker, 197 @VelocityTrackableMotionEventAxis int axis, 198 int pointerId) { 199 if (Build.VERSION.SDK_INT >= 34) { 200 return Api34Impl.getAxisVelocity(tracker, axis, pointerId); 201 } 202 if (axis == MotionEvent.AXIS_X) { 203 return tracker.getXVelocity(pointerId); 204 } 205 if (axis == MotionEvent.AXIS_Y) { 206 return tracker.getYVelocity(pointerId); 207 } 208 return 0; 209 } 210 211 /** Reset the velocity tracker back to its initial state. */ clear(@onNull VelocityTracker tracker)212 public static void clear(@NonNull VelocityTracker tracker) { 213 tracker.clear(); 214 removeFallbackForTracker(tracker); 215 } 216 217 /** 218 * Return a {@link VelocityTracker} object back to be re-used by others. 219 * 220 * <p>Call this method for your {@link VelocityTracker} when you have finished tracking 221 * velocity for the use-case you created this tracker for and decided that you no longer need 222 * it. This allows it to be returned back to the pool of trackers to be re-used by others. 223 * 224 * <p>You must <b>not</b> touch the object after calling this function. That is, don't call any 225 * methods on it, or pass it as an input to any of this class' compat APIs, as the instance 226 * is no longer valid for velocity tracking. 227 * 228 * @see VelocityTracker#recycle() 229 */ recycle(@onNull VelocityTracker tracker)230 public static void recycle(@NonNull VelocityTracker tracker) { 231 tracker.recycle(); 232 removeFallbackForTracker(tracker); 233 } 234 235 /** 236 * Compute the current velocity based on the points that have been 237 * collected. Only call this when you actually want to retrieve velocity 238 * information, as it is relatively expensive. You can then retrieve 239 * the velocity with {@link #getAxisVelocity(VelocityTracker, int)} ()}. 240 * 241 * @param tracker The {@link VelocityTracker} for which to compute velocity. 242 * @param units The units you would like the velocity in. A value of 1 243 * provides units per millisecond, 1000 provides units per second, etc. 244 * Note that the units referred to here are the same units with which motion is reported. For 245 * axes X and Y, the units are pixels. 246 * @param maxVelocity The maximum velocity that can be computed by this method. 247 * This value must be declared in the same unit as the units parameter. This value 248 * must be positive. 249 */ computeCurrentVelocity( @onNull VelocityTracker tracker, int units, float maxVelocity)250 public static void computeCurrentVelocity( 251 @NonNull VelocityTracker tracker, int units, float maxVelocity) { 252 tracker.computeCurrentVelocity(units, maxVelocity); 253 VelocityTrackerFallback fallback = getFallbackTrackerOrNull(tracker); 254 if (fallback != null) { 255 fallback.computeCurrentVelocity(units, maxVelocity); 256 } 257 } 258 259 /** 260 * Equivalent to invoking {@link #computeCurrentVelocity(VelocityTracker, int, float)} with a 261 * maximum velocity of Float.MAX_VALUE. 262 */ computeCurrentVelocity(@onNull VelocityTracker tracker, int units)263 public static void computeCurrentVelocity(@NonNull VelocityTracker tracker, int units) { 264 VelocityTrackerCompat.computeCurrentVelocity(tracker, units, Float.MAX_VALUE); 265 } 266 267 /** 268 * Add a user's movement to the tracker. 269 * 270 * <p>For pointer events, you should call this for the initial 271 * {@link MotionEvent#ACTION_DOWN}, the following 272 * {@link MotionEvent#ACTION_MOVE} events that you receive, and the final 273 * {@link MotionEvent#ACTION_UP}. You can, however, call this 274 * for whichever events you desire. 275 * 276 * @param tracker The {@link VelocityTracker} to add the movement to. 277 * @param event The MotionEvent you received and would like to track. 278 */ addMovement(@onNull VelocityTracker tracker, @NonNull MotionEvent event)279 public static void addMovement(@NonNull VelocityTracker tracker, @NonNull MotionEvent event) { 280 tracker.addMovement(event); 281 if (Build.VERSION.SDK_INT >= 34) { 282 // For API levels 34 and above, we currently do not support any compat logic. 283 return; 284 } 285 286 if (event.getSource() == InputDevice.SOURCE_ROTARY_ENCODER) { 287 // We support compat logic for AXIS_SCROLL. 288 // Initialize the compat instance if needed. 289 if (!sFallbackTrackers.containsKey(tracker)) { 290 sFallbackTrackers.put(tracker, new VelocityTrackerFallback()); 291 } 292 sFallbackTrackers.get(tracker).addMovement(event); 293 } 294 } 295 removeFallbackForTracker(VelocityTracker tracker)296 private static void removeFallbackForTracker(VelocityTracker tracker) { 297 sFallbackTrackers.remove(tracker); 298 } 299 getFallbackTrackerOrNull( VelocityTracker tracker)300 private static @Nullable VelocityTrackerFallback getFallbackTrackerOrNull( 301 VelocityTracker tracker) { 302 return sFallbackTrackers.get(tracker); 303 } 304 305 @RequiresApi(34) 306 private static class Api34Impl { Api34Impl()307 private Api34Impl() { 308 // This class is not instantiable. 309 } 310 isAxisSupported(VelocityTracker velocityTracker, int axis)311 static boolean isAxisSupported(VelocityTracker velocityTracker, int axis) { 312 return velocityTracker.isAxisSupported(axis); 313 } 314 getAxisVelocity(VelocityTracker velocityTracker, int axis, int id)315 static float getAxisVelocity(VelocityTracker velocityTracker, int axis, int id) { 316 return velocityTracker.getAxisVelocity(axis, id); 317 } 318 getAxisVelocity(VelocityTracker velocityTracker, int axis)319 static float getAxisVelocity(VelocityTracker velocityTracker, int axis) { 320 return velocityTracker.getAxisVelocity(axis); 321 } 322 } 323 VelocityTrackerCompat()324 private VelocityTrackerCompat() {} 325 } 326