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