260 lines
11 KiB
Java
260 lines
11 KiB
Java
![]() |
/*
|
||
|
* 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.
|
||
|
*
|
||
|
* <p><b>Differential motion</b> 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}.
|
||
|
*
|
||
|
* <p>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.
|
||
|
*
|
||
|
* <p>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.
|
||
|
*
|
||
|
* <p>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.
|
||
|
*
|
||
|
* <p>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);
|
||
|
}
|
||
|
}
|