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