Differential motion fling for platform widgets
This change adds a DifferentialMotionFlingHelper class to control flinging from differential motion, and uses it in AbsListView and ScrollView. The helper class is currently hidden. Unless we have a strong incentive to open up the API for developers, we will keep the API hidden. Demo video: http://shortn/_0quHhjPtCV Interaction with stretch effect is also demonstrated in the demo. Bug: 293332089 Test: manual, DifferentialMotionFlingHelperTest Test: no CTS regression for ScrollViewTest, ListViewTest, GridViewTest Change-Id: I7acd1aa63da4c64d72cca3c8ac6e10dea1f5bd0b
This commit is contained in:
@@ -916,6 +916,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
|
||||
}
|
||||
}
|
||||
|
||||
private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper;
|
||||
|
||||
public AbsListView(Context context) {
|
||||
super(context);
|
||||
setupDeviceConfigProperties();
|
||||
@@ -4488,17 +4490,22 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
final float axisValue;
|
||||
final int axis;
|
||||
if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
|
||||
axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||
axis = MotionEvent.AXIS_VSCROLL;
|
||||
} else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
|
||||
axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
|
||||
axis = MotionEvent.AXIS_SCROLL;
|
||||
} else {
|
||||
axisValue = 0;
|
||||
axis = -1;
|
||||
}
|
||||
|
||||
final float axisValue = (axis == -1) ? 0 : event.getAxisValue(axis);
|
||||
final int delta = Math.round(axisValue * mVerticalScrollFactor);
|
||||
if (delta != 0) {
|
||||
// Tracks whether or not we should attempt fling for this event.
|
||||
// Fling should not be attempted if the view is already at the limit of scroll,
|
||||
// since it conflicts with EdgeEffect.
|
||||
boolean shouldAttemptFling = true;
|
||||
// If we're moving down, we want the top item. If we're moving up, bottom item.
|
||||
final int motionIndex = delta > 0 ? 0 : getChildCount() - 1;
|
||||
|
||||
@@ -4511,6 +4518,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
|
||||
final int overscrollMode = getOverScrollMode();
|
||||
|
||||
if (!trackMotionScroll(delta, delta)) {
|
||||
if (shouldAttemptFling) {
|
||||
initDifferentialFlingHelperIfNotExists();
|
||||
mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
|
||||
}
|
||||
return true;
|
||||
} else if (!event.isFromSource(InputDevice.SOURCE_MOUSE) && motionView != null
|
||||
&& (overscrollMode == OVER_SCROLL_ALWAYS
|
||||
@@ -4677,6 +4688,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
|
||||
}
|
||||
}
|
||||
|
||||
private void initDifferentialFlingHelperIfNotExists() {
|
||||
if (mDifferentialMotionFlingHelper == null) {
|
||||
mDifferentialMotionFlingHelper =
|
||||
new DifferentialMotionFlingHelper(
|
||||
mContext, new DifferentialFlingTarget());
|
||||
}
|
||||
}
|
||||
|
||||
private void recycleVelocityTracker() {
|
||||
if (mVelocityTracker != null) {
|
||||
mVelocityTracker.recycle();
|
||||
@@ -8197,4 +8216,26 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DifferentialFlingTarget
|
||||
implements DifferentialMotionFlingHelper.DifferentialMotionFlingTarget {
|
||||
@Override
|
||||
public boolean startDifferentialMotionFling(float velocity) {
|
||||
stopDifferentialMotionFling();
|
||||
fling((int) velocity);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopDifferentialMotionFling() {
|
||||
if (mFlingRunnable != null) {
|
||||
mFlingRunnable.endFling();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getScaledScrollFactor() {
|
||||
return -mVerticalScrollFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
249
core/java/android/widget/DifferentialMotionFlingHelper.java
Normal file
249
core/java/android/widget/DifferentialMotionFlingHelper.java
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public DifferentialMotionFlingHelper(
|
||||
Context context,
|
||||
DifferentialMotionFlingTarget target,
|
||||
FlingVelocityThresholdCalculator velocityThresholdCalculator,
|
||||
DifferentialVelocityProvider velocityProvider) {
|
||||
mContext = context;
|
||||
mTarget = target;
|
||||
mVelocityThresholdCalculator = velocityThresholdCalculator;
|
||||
mVelocityProvider = velocityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,8 @@ public class ScrollView extends FrameLayout {
|
||||
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||||
private StrictMode.Span mFlingStrictSpan = null;
|
||||
|
||||
private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper;
|
||||
|
||||
/**
|
||||
* Sentinel value for no current active pointer.
|
||||
* Used by {@link #mActivePointerId}.
|
||||
@@ -594,6 +596,14 @@ public class ScrollView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void initDifferentialFlingHelperIfNotExists() {
|
||||
if (mDifferentialMotionFlingHelper == null) {
|
||||
mDifferentialMotionFlingHelper =
|
||||
new DifferentialMotionFlingHelper(
|
||||
mContext, new DifferentialFlingTarget());
|
||||
}
|
||||
}
|
||||
|
||||
private void recycleVelocityTracker() {
|
||||
if (mVelocityTracker != null) {
|
||||
mVelocityTracker.recycle();
|
||||
@@ -942,17 +952,22 @@ public class ScrollView extends FrameLayout {
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
final float axisValue;
|
||||
final int axis;
|
||||
if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
|
||||
axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||
axis = MotionEvent.AXIS_VSCROLL;
|
||||
} else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
|
||||
axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
|
||||
axis = MotionEvent.AXIS_SCROLL;
|
||||
} else {
|
||||
axisValue = 0;
|
||||
axis = -1;
|
||||
}
|
||||
|
||||
final float axisValue = (axis == -1) ? 0 : event.getAxisValue(axis);
|
||||
final int delta = Math.round(axisValue * mVerticalScrollFactor);
|
||||
if (delta != 0) {
|
||||
// Tracks whether or not we should attempt fling for this event.
|
||||
// Fling should not be attempted if the view is already at the limit of scroll,
|
||||
// since it conflicts with EdgeEffect.
|
||||
boolean shouldAttemptFling = true;
|
||||
final int range = getScrollRange();
|
||||
int oldScrollY = mScrollY;
|
||||
int newScrollY = oldScrollY - delta;
|
||||
@@ -971,6 +986,7 @@ public class ScrollView extends FrameLayout {
|
||||
absorbed = true;
|
||||
}
|
||||
newScrollY = 0;
|
||||
shouldAttemptFling = false;
|
||||
} else if (newScrollY > range) {
|
||||
if (canOverscroll) {
|
||||
mEdgeGlowBottom.onPullDistance(
|
||||
@@ -980,9 +996,14 @@ public class ScrollView extends FrameLayout {
|
||||
absorbed = true;
|
||||
}
|
||||
newScrollY = range;
|
||||
shouldAttemptFling = false;
|
||||
}
|
||||
if (newScrollY != oldScrollY) {
|
||||
super.scrollTo(mScrollX, newScrollY);
|
||||
if (shouldAttemptFling) {
|
||||
initDifferentialFlingHelperIfNotExists();
|
||||
mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (absorbed) {
|
||||
@@ -2126,4 +2147,23 @@ public class ScrollView extends FrameLayout {
|
||||
};
|
||||
}
|
||||
|
||||
private class DifferentialFlingTarget
|
||||
implements DifferentialMotionFlingHelper.DifferentialMotionFlingTarget {
|
||||
@Override
|
||||
public boolean startDifferentialMotionFling(float velocity) {
|
||||
stopDifferentialMotionFling();
|
||||
fling((int) velocity);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopDifferentialMotionFling() {
|
||||
mScroller.abortAnimation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getScaledScrollFactor() {
|
||||
return -mVerticalScrollFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DifferentialMotionFlingHelperTest {
|
||||
private int mMinVelocity = 0;
|
||||
private int mMaxVelocity = Integer.MAX_VALUE;
|
||||
/** A fake velocity value that's going to be returned from the velocity provider. */
|
||||
private float mVelocity;
|
||||
private boolean mVelocityCalculated;
|
||||
|
||||
private final DifferentialMotionFlingHelper.DifferentialVelocityProvider mVelocityProvider =
|
||||
(vt, event, axis) -> {
|
||||
mVelocityCalculated = true;
|
||||
return mVelocity;
|
||||
};
|
||||
|
||||
private final DifferentialMotionFlingHelper.FlingVelocityThresholdCalculator
|
||||
mVelocityThresholdCalculator =
|
||||
(ctx, buffer, event, axis) -> {
|
||||
buffer[0] = mMinVelocity;
|
||||
buffer[1] = mMaxVelocity;
|
||||
};
|
||||
|
||||
private final TestDifferentialMotionFlingTarget mFlingTarget =
|
||||
new TestDifferentialMotionFlingTarget();
|
||||
|
||||
private DifferentialMotionFlingHelper mFlingHelper;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mFlingHelper = new DifferentialMotionFlingHelper(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
mFlingTarget,
|
||||
mVelocityThresholdCalculator,
|
||||
mVelocityProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deviceDoesNotSupportFling_noVelocityCalculated() {
|
||||
mMinVelocity = Integer.MAX_VALUE;
|
||||
mMaxVelocity = Integer.MIN_VALUE;
|
||||
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, 60);
|
||||
|
||||
assertFalse(mVelocityCalculated);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flingVelocityOppositeToPrevious_stopsOngoingFling() {
|
||||
deliverEventWithVelocity(createRotaryEncoderEvent(), MotionEvent.AXIS_SCROLL, 50);
|
||||
deliverEventWithVelocity(createRotaryEncoderEvent(), MotionEvent.AXIS_SCROLL, -10);
|
||||
|
||||
// One stop on the initial event, and second stop due to opposite velocities.
|
||||
assertEquals(2, mFlingTarget.mNumStops);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flingParamsChanged_stopsOngoingFling() {
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, 50);
|
||||
deliverEventWithVelocity(createRotaryEncoderEvent(), MotionEvent.AXIS_SCROLL, 10);
|
||||
|
||||
// One stop on the initial event, and second stop due to changed axis/source.
|
||||
assertEquals(2, mFlingTarget.mNumStops);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positiveFlingVelocityTooLow_doesNotGenerateFling() {
|
||||
mMinVelocity = 50;
|
||||
mMaxVelocity = 100;
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, 20);
|
||||
|
||||
assertEquals(0, mFlingTarget.mLastFlingVelocity, /* delta= */ 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeFlingVelocityTooLow_doesNotGenerateFling() {
|
||||
mMinVelocity = 50;
|
||||
mMaxVelocity = 100;
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, -20);
|
||||
|
||||
assertEquals(0, mFlingTarget.mLastFlingVelocity, /* delta= */ 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positiveFlingVelocityAboveMinimum_generateFlings() {
|
||||
mMinVelocity = 50;
|
||||
mMaxVelocity = 100;
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, 60);
|
||||
|
||||
assertEquals(60, mFlingTarget.mLastFlingVelocity, /* delta= */ 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeFlingVelocityAboveMinimum_generateFlings() {
|
||||
mMinVelocity = 50;
|
||||
mMaxVelocity = 100;
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, -60);
|
||||
|
||||
assertEquals(-60, mFlingTarget.mLastFlingVelocity, /* delta= */ 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positiveFlingVelocityAboveMaximum_velocityClamped() {
|
||||
mMinVelocity = 50;
|
||||
mMaxVelocity = 100;
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, 3000);
|
||||
|
||||
assertEquals(100, mFlingTarget.mLastFlingVelocity, /* delta= */ 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeFlingVelocityAboveMaximum_velocityClamped() {
|
||||
mMinVelocity = 50;
|
||||
mMaxVelocity = 100;
|
||||
deliverEventWithVelocity(createPointerEvent(), MotionEvent.AXIS_VSCROLL, -3000);
|
||||
|
||||
assertEquals(-100, mFlingTarget.mLastFlingVelocity, /* delta= */ 0);
|
||||
}
|
||||
|
||||
private MotionEvent createRotaryEncoderEvent() {
|
||||
return MotionEventUtils.createRotaryEvent(-2);
|
||||
}
|
||||
|
||||
private MotionEvent createPointerEvent() {
|
||||
return MotionEventUtils.createGenericPointerEvent(/* hScroll= */ 0, /* vScroll= */ -1);
|
||||
|
||||
}
|
||||
|
||||
private void deliverEventWithVelocity(MotionEvent ev, int axis, float velocity) {
|
||||
mVelocity = velocity;
|
||||
mFlingHelper.onMotionEvent(ev, axis);
|
||||
ev.recycle();
|
||||
}
|
||||
|
||||
private static class TestDifferentialMotionFlingTarget
|
||||
implements DifferentialMotionFlingHelper.DifferentialMotionFlingTarget {
|
||||
float mLastFlingVelocity = 0;
|
||||
int mNumStops = 0;
|
||||
|
||||
@Override
|
||||
public boolean startDifferentialMotionFling(float velocity) {
|
||||
mLastFlingVelocity = velocity;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopDifferentialMotionFling() {
|
||||
mNumStops++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getScaledScrollFactor() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 static android.view.InputDevice.SOURCE_CLASS_POINTER;
|
||||
import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
|
||||
import static android.view.MotionEvent.ACTION_SCROLL;
|
||||
import static android.view.MotionEvent.AXIS_HSCROLL;
|
||||
import static android.view.MotionEvent.AXIS_SCROLL;
|
||||
import static android.view.MotionEvent.AXIS_VSCROLL;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/** Test utilities for {@link MotionEvent}s. */
|
||||
public class MotionEventUtils {
|
||||
|
||||
/** Creates a test {@link MotionEvent} from a {@link SOURCE_ROTARY_ENCODER}. */
|
||||
public static MotionEvent createRotaryEvent(float scroll) {
|
||||
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||||
coords.setAxisValue(AXIS_SCROLL, scroll);
|
||||
|
||||
return createGenericMotionEvent(SOURCE_ROTARY_ENCODER, ACTION_SCROLL, coords);
|
||||
}
|
||||
|
||||
/** Creates a test {@link MotionEvent} from a {@link SOURCE_CLASS_POINTER}. */
|
||||
public static MotionEvent createGenericPointerEvent(float hScroll, float vScroll) {
|
||||
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||||
coords.setAxisValue(AXIS_HSCROLL, hScroll);
|
||||
coords.setAxisValue(AXIS_VSCROLL, vScroll);
|
||||
|
||||
return createGenericMotionEvent(SOURCE_CLASS_POINTER, ACTION_SCROLL, coords);
|
||||
}
|
||||
|
||||
private static MotionEvent createGenericMotionEvent(
|
||||
int source, int action, MotionEvent.PointerCoords coords) {
|
||||
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
|
||||
props.id = 0;
|
||||
|
||||
return MotionEvent.obtain(
|
||||
/* downTime= */ 0,
|
||||
/* eventTime= */ 100,
|
||||
action,
|
||||
/* pointerCount= */ 1,
|
||||
new MotionEvent.PointerProperties[] {props},
|
||||
new MotionEvent.PointerCoords[] {coords},
|
||||
/* metaState= */ 0,
|
||||
/* buttonState= */ 0,
|
||||
/* xPrecision= */ 0,
|
||||
/* yPrecision= */ 0,
|
||||
/* deviceId= */ 1,
|
||||
/* edgeFlags= */ 0,
|
||||
source,
|
||||
/* flags= */ 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user