/* * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import android.annotation.Nullable; import android.content.Context; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.widget.flags.FeatureFlags; import android.widget.flags.FeatureFlagsImpl; import com.android.internal.annotations.VisibleForTesting; /** * Helper for controlling differential motion flings. * *

Differential motion here refers to motions that report change in position instead of * absolution position. For instance, differential data points of 2, -1, 5 represent: there was * a movement by "2" units, then by "-1" units, then by "5" units. Examples of motions reported * differentially include motions from {@link MotionEvent#AXIS_SCROLL}. * *

The client should call {@link #onMotionEvent} when a differential motion event happens on * the target View (that is, the View on which we want to fling), and this class processes the event * to orchestrate fling. * *

Note that this helper class currently works to control fling only in one direction at a time. * As such, it works independently of horizontal/vertical orientations. It requests its client to * start/stop fling, and it's up to the client to choose the fling direction based on its specific * internal configurations and/or preferences. * * @hide */ public class DifferentialMotionFlingHelper { private final Context mContext; private final DifferentialMotionFlingTarget mTarget; private final FlingVelocityThresholdCalculator mVelocityThresholdCalculator; private final DifferentialVelocityProvider mVelocityProvider; private final FeatureFlags mWidgetFeatureFlags; @Nullable private VelocityTracker mVelocityTracker; private float mLastFlingVelocity; private int mLastProcessedAxis = -1; private int mLastProcessedSource = -1; private int mLastProcessedDeviceId = -1; // Initialize min and max to +infinity and 0, to effectively disable fling at start. private final int[] mFlingVelocityThresholds = new int[] {Integer.MAX_VALUE, 0}; /** Interface to calculate the fling velocity thresholds. Helps fake during testing. */ @VisibleForTesting public interface FlingVelocityThresholdCalculator { /** * Calculates the fling velocity thresholds (in pixels/second) and puts them in a provided * store. * * @param context the context associated with the View that may be flung. * @param store an at-least size-2 int array. The method will overwrite positions 0 and 1 * with the min and max fling velocities, respectively. * @param event the event that may trigger fling. * @param axis the axis being processed for the event. */ void calculateFlingVelocityThresholds( Context context, int[] store, MotionEvent event, int axis); } /** * Interface to provide velocity. Helps fake during testing. * *

The client should call {@link #getCurrentVelocity(VelocityTracker, MotionEvent, int)} each * time it wants to consider a {@link MotionEvent} towards the latest velocity, and the * interface handles providing velocity that accounts for the latest and all past events. */ @VisibleForTesting public interface DifferentialVelocityProvider { /** * Returns the latest velocity. * * @param vt the {@link VelocityTracker} to be used to compute velocity. * @param event the latest event to be considered in the velocity computations. * @param axis the axis being processed for the event. * @return the calculated, latest velocity. */ float getCurrentVelocity(VelocityTracker vt, MotionEvent event, int axis); } /** * Represents an entity that may be flung by a differential motion or an entity that initiates * fling on a target View. */ public interface DifferentialMotionFlingTarget { /** * Start flinging on the target View by a given velocity. * * @param velocity the fling velocity, in pixels/second. * @return {@code true} if fling was successfully initiated, {@code false} otherwise. */ boolean startDifferentialMotionFling(float velocity); /** Stop any ongoing fling on the target View that is caused by a differential motion. */ void stopDifferentialMotionFling(); /** * Returns the scaled scroll factor to be used for differential motions. This is the * value that the raw {@link MotionEvent} values should be multiplied with to get pixels. * *

This usually is one of the values provided by {@link ViewConfiguration}. It is * up to the client to choose and provide any value as per its internal configuration. * * @see ViewConfiguration#getScaledHorizontalScrollFactor() * @see ViewConfiguration#getScaledVerticalScrollFactor() */ float getScaledScrollFactor(); } /** Constructs an instance for a given {@link DifferentialMotionFlingTarget}. */ public DifferentialMotionFlingHelper( Context context, DifferentialMotionFlingTarget target) { this(context, target, DifferentialMotionFlingHelper::calculateFlingVelocityThresholds, DifferentialMotionFlingHelper::getCurrentVelocity, /* widgetFeatureFlags= */ new FeatureFlagsImpl()); } @VisibleForTesting public DifferentialMotionFlingHelper( Context context, DifferentialMotionFlingTarget target, FlingVelocityThresholdCalculator velocityThresholdCalculator, DifferentialVelocityProvider velocityProvider, FeatureFlags widgetFeatureFlags) { mContext = context; mTarget = target; mVelocityThresholdCalculator = velocityThresholdCalculator; mVelocityProvider = velocityProvider; mWidgetFeatureFlags = widgetFeatureFlags; } /** * Called to report when a differential motion happens on the View that's the target for fling. * * @param event the {@link MotionEvent} being reported. * @param axis the axis being processed by the target View. */ public void onMotionEvent(MotionEvent event, int axis) { if (!mWidgetFeatureFlags.enablePlatformWidgetDifferentialMotionFling()) { return; } boolean flingParamsChanged = calculateFlingVelocityThresholds(event, axis); if (mFlingVelocityThresholds[0] == Integer.MAX_VALUE) { // Integer.MAX_VALUE means that the device does not support fling for the current // configuration. Do not proceed any further. recycleVelocityTracker(); return; } float scaledVelocity = getCurrentVelocity(event, axis) * mTarget.getScaledScrollFactor(); float velocityDirection = Math.signum(scaledVelocity); // Stop ongoing fling if there has been state changes affecting fling, or if the current // velocity (if non-zero) is opposite of the velocity that last caused fling. if (flingParamsChanged || (velocityDirection != Math.signum(mLastFlingVelocity) && velocityDirection != 0)) { mTarget.stopDifferentialMotionFling(); } if (Math.abs(scaledVelocity) < mFlingVelocityThresholds[0]) { return; } // Clamp the scaled velocity between [-max, max]. // e.g. if max=100, and vel=200 // vel = max(-100, min(200, 100)) = max(-100, 100) = 100 // e.g. if max=100, and vel=-200 // vel = max(-100, min(-200, 100)) = max(-100, -200) = -100 scaledVelocity = Math.max( -mFlingVelocityThresholds[1], Math.min(scaledVelocity, mFlingVelocityThresholds[1])); boolean flung = mTarget.startDifferentialMotionFling(scaledVelocity); mLastFlingVelocity = flung ? scaledVelocity : 0; } /** * Calculates fling velocity thresholds based on the provided event and axis, and returns {@code * true} if there has been a change of any params that may affect fling velocity thresholds. */ private boolean calculateFlingVelocityThresholds(MotionEvent event, int axis) { int source = event.getSource(); int deviceId = event.getDeviceId(); if (mLastProcessedSource != source || mLastProcessedDeviceId != deviceId || mLastProcessedAxis != axis) { mVelocityThresholdCalculator.calculateFlingVelocityThresholds( mContext, mFlingVelocityThresholds, event, axis); // Save data about this processing so that we don't have to re-process fling thresholds // for similar parameters. mLastProcessedSource = source; mLastProcessedDeviceId = deviceId; mLastProcessedAxis = axis; return true; } return false; } private static void calculateFlingVelocityThresholds( Context context, int[] buffer, MotionEvent event, int axis) { int source = event.getSource(); int deviceId = event.getDeviceId(); ViewConfiguration vc = ViewConfiguration.get(context); buffer[0] = vc.getScaledMinimumFlingVelocity(deviceId, axis, source); buffer[1] = vc.getScaledMaximumFlingVelocity(deviceId, axis, source); } private float getCurrentVelocity(MotionEvent event, int axis) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } return mVelocityProvider.getCurrentVelocity(mVelocityTracker, event, axis); } private void recycleVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } private static float getCurrentVelocity(VelocityTracker vt, MotionEvent event, int axis) { vt.addMovement(event); vt.computeCurrentVelocity(1000); return vt.getAxisVelocity(axis); } }