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 android.widget;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.view.MotionEvent;
22 import android.view.VelocityTracker;
23 import android.view.ViewConfiguration;
24 import android.widget.flags.FeatureFlags;
25 import android.widget.flags.FeatureFlagsImpl;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 /**
30  * Helper for controlling differential motion flings.
31  *
32  * <p><b>Differential motion</b> here refers to motions that report change in position instead of
33  * absolution position. For instance, differential data points of 2, -1, 5 represent: there was
34  * a movement by "2" units, then by "-1" units, then by "5" units. Examples of motions reported
35  * differentially include motions from {@link MotionEvent#AXIS_SCROLL}.
36  *
37  * <p>The client should call {@link #onMotionEvent} when a differential motion event happens on
38  * the target View (that is, the View on which we want to fling), and this class processes the event
39  * to orchestrate fling.
40  *
41  * <p>Note that this helper class currently works to control fling only in one direction at a time.
42  * As such, it works independently of horizontal/vertical orientations. It requests its client to
43  * start/stop fling, and it's up to the client to choose the fling direction based on its specific
44  * internal configurations and/or preferences.
45  *
46  * @hide
47  */
48 public class DifferentialMotionFlingHelper {
49     private final Context mContext;
50     private final DifferentialMotionFlingTarget mTarget;
51 
52     private final FlingVelocityThresholdCalculator mVelocityThresholdCalculator;
53     private final DifferentialVelocityProvider mVelocityProvider;
54 
55     private final FeatureFlags mWidgetFeatureFlags;
56 
57     @Nullable private VelocityTracker mVelocityTracker;
58 
59     private float mLastFlingVelocity;
60 
61     private int mLastProcessedAxis = -1;
62     private int mLastProcessedSource = -1;
63     private int mLastProcessedDeviceId = -1;
64 
65     // Initialize min and max to +infinity and 0, to effectively disable fling at start.
66     private final int[] mFlingVelocityThresholds = new int[] {Integer.MAX_VALUE, 0};
67 
68     /** Interface to calculate the fling velocity thresholds. Helps fake during testing. */
69     @VisibleForTesting
70     public interface FlingVelocityThresholdCalculator {
71         /**
72          * Calculates the fling velocity thresholds (in pixels/second) and puts them in a provided
73          * store.
74          *
75          * @param context the context associated with the View that may be flung.
76          * @param store an at-least size-2 int array. The method will overwrite positions 0 and 1
77          *             with the min and max fling velocities, respectively.
78          * @param event the event that may trigger fling.
79          * @param axis the axis being processed for the event.
80          */
calculateFlingVelocityThresholds( Context context, int[] store, MotionEvent event, int axis)81         void calculateFlingVelocityThresholds(
82                 Context context, int[] store, MotionEvent event, int axis);
83     }
84 
85     /**
86      * Interface to provide velocity. Helps fake during testing.
87      *
88      * <p>The client should call {@link #getCurrentVelocity(VelocityTracker, MotionEvent, int)} each
89      * time it wants to consider a {@link MotionEvent} towards the latest velocity, and the
90      * interface handles providing velocity that accounts for the latest and all past events.
91      */
92     @VisibleForTesting
93     public interface DifferentialVelocityProvider {
94         /**
95          * Returns the latest velocity.
96          *
97          * @param vt the {@link VelocityTracker} to be used to compute velocity.
98          * @param event the latest event to be considered in the velocity computations.
99          * @param axis the axis being processed for the event.
100          * @return the calculated, latest velocity.
101          */
getCurrentVelocity(VelocityTracker vt, MotionEvent event, int axis)102         float getCurrentVelocity(VelocityTracker vt, MotionEvent event, int axis);
103     }
104 
105     /**
106      * Represents an entity that may be flung by a differential motion or an entity that initiates
107      * fling on a target View.
108      */
109     public interface DifferentialMotionFlingTarget {
110         /**
111          * Start flinging on the target View by a given velocity.
112          *
113          * @param velocity the fling velocity, in pixels/second.
114          * @return {@code true} if fling was successfully initiated, {@code false} otherwise.
115          */
startDifferentialMotionFling(float velocity)116         boolean startDifferentialMotionFling(float velocity);
117 
118         /** Stop any ongoing fling on the target View that is caused by a differential motion. */
stopDifferentialMotionFling()119         void stopDifferentialMotionFling();
120 
121         /**
122          * Returns the scaled scroll factor to be used for differential motions. This is the
123          * value that the raw {@link MotionEvent} values should be multiplied with to get pixels.
124          *
125          * <p>This usually is one of the values provided by {@link ViewConfiguration}. It is
126          * up to the client to choose and provide any value as per its internal configuration.
127          *
128          * @see ViewConfiguration#getScaledHorizontalScrollFactor()
129          * @see ViewConfiguration#getScaledVerticalScrollFactor()
130          */
getScaledScrollFactor()131         float getScaledScrollFactor();
132     }
133 
134     /** Constructs an instance for a given {@link DifferentialMotionFlingTarget}. */
DifferentialMotionFlingHelper( Context context, DifferentialMotionFlingTarget target)135     public DifferentialMotionFlingHelper(
136             Context context,
137             DifferentialMotionFlingTarget target) {
138         this(context,
139                 target,
140                 DifferentialMotionFlingHelper::calculateFlingVelocityThresholds,
141                 DifferentialMotionFlingHelper::getCurrentVelocity,
142                 /* widgetFeatureFlags= */ new FeatureFlagsImpl());
143     }
144 
145     @VisibleForTesting
DifferentialMotionFlingHelper( Context context, DifferentialMotionFlingTarget target, FlingVelocityThresholdCalculator velocityThresholdCalculator, DifferentialVelocityProvider velocityProvider, FeatureFlags widgetFeatureFlags)146     public DifferentialMotionFlingHelper(
147             Context context,
148             DifferentialMotionFlingTarget target,
149             FlingVelocityThresholdCalculator velocityThresholdCalculator,
150             DifferentialVelocityProvider velocityProvider,
151             FeatureFlags widgetFeatureFlags) {
152         mContext = context;
153         mTarget = target;
154         mVelocityThresholdCalculator = velocityThresholdCalculator;
155         mVelocityProvider = velocityProvider;
156         mWidgetFeatureFlags = widgetFeatureFlags;
157     }
158 
159     /**
160      * Called to report when a differential motion happens on the View that's the target for fling.
161      *
162      * @param event the {@link MotionEvent} being reported.
163      * @param axis the axis being processed by the target View.
164      */
onMotionEvent(MotionEvent event, int axis)165     public void onMotionEvent(MotionEvent event, int axis) {
166         if (!mWidgetFeatureFlags.enablePlatformWidgetDifferentialMotionFling()) {
167             return;
168         }
169         boolean flingParamsChanged = calculateFlingVelocityThresholds(event, axis);
170         if (mFlingVelocityThresholds[0] == Integer.MAX_VALUE) {
171             // Integer.MAX_VALUE means that the device does not support fling for the current
172             // configuration. Do not proceed any further.
173             recycleVelocityTracker();
174             return;
175         }
176 
177         float scaledVelocity =
178                 getCurrentVelocity(event, axis) * mTarget.getScaledScrollFactor();
179 
180         float velocityDirection = Math.signum(scaledVelocity);
181         // Stop ongoing fling if there has been state changes affecting fling, or if the current
182         // velocity (if non-zero) is opposite of the velocity that last caused fling.
183         if (flingParamsChanged
184                 || (velocityDirection != Math.signum(mLastFlingVelocity)
185                     && velocityDirection != 0)) {
186             mTarget.stopDifferentialMotionFling();
187         }
188 
189         if (Math.abs(scaledVelocity) < mFlingVelocityThresholds[0]) {
190             return;
191         }
192 
193         // Clamp the scaled velocity between [-max, max].
194         // e.g. if max=100, and vel=200
195         // vel = max(-100, min(200, 100)) = max(-100, 100) = 100
196         // e.g. if max=100, and vel=-200
197         // vel = max(-100, min(-200, 100)) = max(-100, -200) = -100
198         scaledVelocity =
199                 Math.max(
200                         -mFlingVelocityThresholds[1],
201                         Math.min(scaledVelocity, mFlingVelocityThresholds[1]));
202 
203         boolean flung = mTarget.startDifferentialMotionFling(scaledVelocity);
204         mLastFlingVelocity = flung ? scaledVelocity : 0;
205     }
206 
207     /**
208      * Calculates fling velocity thresholds based on the provided event and axis, and returns {@code
209      * true} if there has been a change of any params that may affect fling velocity thresholds.
210      */
calculateFlingVelocityThresholds(MotionEvent event, int axis)211     private boolean calculateFlingVelocityThresholds(MotionEvent event, int axis) {
212         int source = event.getSource();
213         int deviceId = event.getDeviceId();
214         if (mLastProcessedSource != source
215                 || mLastProcessedDeviceId != deviceId
216                 || mLastProcessedAxis != axis) {
217             mVelocityThresholdCalculator.calculateFlingVelocityThresholds(
218                     mContext, mFlingVelocityThresholds, event, axis);
219             // Save data about this processing so that we don't have to re-process fling thresholds
220             // for similar parameters.
221             mLastProcessedSource = source;
222             mLastProcessedDeviceId = deviceId;
223             mLastProcessedAxis = axis;
224             return true;
225         }
226         return false;
227     }
228 
calculateFlingVelocityThresholds( Context context, int[] buffer, MotionEvent event, int axis)229     private static void calculateFlingVelocityThresholds(
230             Context context, int[] buffer, MotionEvent event, int axis) {
231         int source = event.getSource();
232         int deviceId = event.getDeviceId();
233 
234         ViewConfiguration vc = ViewConfiguration.get(context);
235         buffer[0] = vc.getScaledMinimumFlingVelocity(deviceId, axis, source);
236         buffer[1] = vc.getScaledMaximumFlingVelocity(deviceId, axis, source);
237     }
238 
getCurrentVelocity(MotionEvent event, int axis)239     private float getCurrentVelocity(MotionEvent event, int axis) {
240         if (mVelocityTracker == null) {
241             mVelocityTracker = VelocityTracker.obtain();
242         }
243 
244         return mVelocityProvider.getCurrentVelocity(mVelocityTracker, event, axis);
245     }
246 
recycleVelocityTracker()247     private void recycleVelocityTracker() {
248         if (mVelocityTracker != null) {
249             mVelocityTracker.recycle();
250             mVelocityTracker = null;
251         }
252     }
253 
getCurrentVelocity(VelocityTracker vt, MotionEvent event, int axis)254     private static float getCurrentVelocity(VelocityTracker vt, MotionEvent event, int axis) {
255         vt.addMovement(event);
256         vt.computeCurrentVelocity(1000);
257         return vt.getAxisVelocity(axis);
258     }
259 }
260