diff options
5 files changed, 593 insertions, 8 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 6ad1960cbda9..03364b66b42e 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -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; + } + } } diff --git a/core/java/android/widget/DifferentialMotionFlingHelper.java b/core/java/android/widget/DifferentialMotionFlingHelper.java new file mode 100644 index 000000000000..95d24ec31209 --- /dev/null +++ b/core/java/android/widget/DifferentialMotionFlingHelper.java @@ -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); + } +} diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index cb5dbe6c5618..eeb6b43a89f2 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -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; + } + } } diff --git a/core/tests/coretests/src/android/widget/DifferentialMotionFlingHelperTest.java b/core/tests/coretests/src/android/widget/DifferentialMotionFlingHelperTest.java new file mode 100644 index 000000000000..51c8bc06e878 --- /dev/null +++ b/core/tests/coretests/src/android/widget/DifferentialMotionFlingHelperTest.java @@ -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; + } + } +} diff --git a/core/tests/coretests/src/android/widget/MotionEventUtils.java b/core/tests/coretests/src/android/widget/MotionEventUtils.java new file mode 100644 index 000000000000..275efa3efd19 --- /dev/null +++ b/core/tests/coretests/src/android/widget/MotionEventUtils.java @@ -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); + } +} |