Loading core/api/current.txt +18 −0 Original line number Diff line number Diff line Loading @@ -50040,6 +50040,13 @@ package android.view { field public static final int VIRTUAL_KEY_RELEASE = 8; // 0x8 } @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public class HapticScrollFeedbackProvider implements android.view.ScrollFeedbackProvider { ctor public HapticScrollFeedbackProvider(@NonNull android.view.View); method public void onScrollLimit(int, int, int, boolean); method public void onScrollProgress(int, int, int, int); method public void onSnapToItem(int, int, int); } public class InflateException extends java.lang.RuntimeException { ctor public InflateException(); ctor public InflateException(String, Throwable); Loading Loading @@ -51205,6 +51212,15 @@ package android.view { method @UiThread public void updatePositionInWindow(); } @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public interface ScrollFeedbackProvider { method public void onScrollLimit(int, int, int, boolean); method public default void onScrollLimit(@NonNull android.view.MotionEvent, int, boolean); method public void onScrollProgress(int, int, int, int); method public default void onScrollProgress(@NonNull android.view.MotionEvent, int, int); method public void onSnapToItem(int, int, int); method public default void onSnapToItem(@NonNull android.view.MotionEvent, int); } public class SearchEvent { ctor public SearchEvent(android.view.InputDevice); method public android.view.InputDevice getInputDevice(); Loading Loading @@ -52482,6 +52498,7 @@ package android.view { method @Deprecated public static int getEdgeSlop(); method @Deprecated public static int getFadingEdgeLength(); method @Deprecated public static long getGlobalActionKeyTimeout(); method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public int getHapticScrollFeedbackTickInterval(int, int, int); method public static int getJumpTapTimeout(); method public static int getKeyRepeatDelay(); method public static int getKeyRepeatTimeout(); Loading Loading @@ -52521,6 +52538,7 @@ package android.view { method @Deprecated public static int getWindowTouchSlop(); method public static long getZoomControlsTimeout(); method public boolean hasPermanentMenuKey(); method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public boolean isHapticScrollFeedbackEnabled(int, int, int); method public boolean shouldShowMenuShortcutsWhenKeyboardPresent(); } core/java/android/view/HapticScrollFeedbackProvider.java +39 −36 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package android.view; import static com.android.internal.R.dimen.config_rotaryEncoderAxisScrollTickInterval; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.view.flags.Flags; import com.android.internal.annotations.VisibleForTesting; Loading @@ -25,16 +27,15 @@ import com.android.internal.annotations.VisibleForTesting; * * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state * is isolated. * * @hide */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { private static final String TAG = "HapticScrollFeedbackProvider"; private static final int TICK_INTERVAL_NO_TICK = 0; private static final int TICK_INTERVAL_UNSET = Integer.MAX_VALUE; private final View mView; private final ViewConfiguration mViewConfig; // Info about the cause of the latest scroll event. Loading @@ -49,26 +50,35 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { * Cache for tick interval for scroll tick caused by a {@link InputDevice#SOURCE_ROTARY_ENCODER} * on {@link MotionEvent#AXIS_SCROLL}. Set to -1 if the value has not been fetched and cached. */ private int mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_UNSET; /** The tick interval corresponding to the current InputDevice/source/axis. */ private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK; private int mTotalScrollPixels = 0; private boolean mCanPlayLimitFeedback = true; private boolean mHapticScrollFeedbackEnabled = false; public HapticScrollFeedbackProvider(View view) { this(view, /* rotaryEncoderAxisScrollTickIntervalPixels= */ TICK_INTERVAL_UNSET); public HapticScrollFeedbackProvider(@NonNull View view) { this(view, ViewConfiguration.get(view.getContext())); } /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public HapticScrollFeedbackProvider(View view, int rotaryEncoderAxisScrollTickIntervalPixels) { public HapticScrollFeedbackProvider(View view, ViewConfiguration viewConfig) { mView = view; mRotaryEncoderAxisScrollTickIntervalPixels = rotaryEncoderAxisScrollTickIntervalPixels; mViewConfig = viewConfig; } @Override public void onScrollProgress(MotionEvent event, int axis, int deltaInPixels) { maybeUpdateCurrentConfig(event, axis); public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) { maybeUpdateCurrentConfig(inputDeviceId, source, axis); if (!mHapticScrollFeedbackEnabled) { return; } // Unlock limit feedback regardless of scroll tick being enabled as long as there's a // non-zero scroll progress. if (deltaInPixels != 0) { mCanPlayLimitFeedback = true; } if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) { // There's no valid tick interval. Exit early before doing any further computation. Loading @@ -82,13 +92,14 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); } mCanPlayLimitFeedback = true; } @Override public void onScrollLimit(MotionEvent event, int axis, boolean isStart) { maybeUpdateCurrentConfig(event, axis); public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) { maybeUpdateCurrentConfig(inputDeviceId, source, axis); if (!mHapticScrollFeedbackEnabled) { return; } if (!mCanPlayLimitFeedback) { return; Loading @@ -101,41 +112,33 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { } @Override public void onSnapToItem(MotionEvent event, int axis) { public void onSnapToItem(int inputDeviceId, int source, int axis) { maybeUpdateCurrentConfig(inputDeviceId, source, axis); if (!mHapticScrollFeedbackEnabled) { return; } // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); mCanPlayLimitFeedback = true; } private void maybeUpdateCurrentConfig(MotionEvent event, int axis) { int source = event.getSource(); int deviceId = event.getDeviceId(); private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) { if (mAxis != axis || mSource != source || mDeviceId != deviceId) { mSource = source; mAxis = axis; mDeviceId = deviceId; mHapticScrollFeedbackEnabled = mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source); mCanPlayLimitFeedback = true; mTotalScrollPixels = 0; calculateTickIntervals(source, axis); updateTickIntervals(deviceId, source, axis); } } private void calculateTickIntervals(int source, int axis) { mTickIntervalPixels = TICK_INTERVAL_NO_TICK; if (axis == MotionEvent.AXIS_SCROLL && source == InputDevice.SOURCE_ROTARY_ENCODER) { if (mRotaryEncoderAxisScrollTickIntervalPixels == TICK_INTERVAL_UNSET) { // Value has not been fetched yet. Fetch and cache it. mRotaryEncoderAxisScrollTickIntervalPixels = mView.getContext().getResources().getDimensionPixelSize( config_rotaryEncoderAxisScrollTickInterval); if (mRotaryEncoderAxisScrollTickIntervalPixels < 0) { mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_NO_TICK; } } mTickIntervalPixels = mRotaryEncoderAxisScrollTickIntervalPixels; } private void updateTickIntervals(int deviceId, int source, int axis) { mTickIntervalPixels = mHapticScrollFeedbackEnabled ? mViewConfig.getHapticScrollFeedbackTickInterval(deviceId, axis, source) : TICK_INTERVAL_NO_TICK; } } core/java/android/view/ScrollFeedbackProvider.java +88 −8 Original line number Diff line number Diff line Loading @@ -16,16 +16,45 @@ package android.view; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.view.flags.Flags; /** * Interface to represent an entity giving consistent feedback for different events surrounding view * scroll. * * @hide * <p>When you have access to the {@link MotionEvent}s that triggered the different scroll events, * use the {@link MotionEvent} based APIs in this class. If you do not have access to the motion * events, you can use the methods that accept the {@link InputDevice} ID (which can be obtained by * APIs like {@link MotionEvent#getDeviceId()} and {@link InputDevice#getId()}) and source (which * can be obtained by APIs like {@link MotionEvent#getSource()}) of the motion that caused the * scroll events. */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public interface ScrollFeedbackProvider { /** * The view has snapped to an item, with a motion from a given {@link MotionEvent} on a given * {@code axis}. * Call this when the view has snapped to an item, with a motion generated by an * {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and on * a given motion event {@code axis}. * * <p>This method has the same purpose as {@link #onSnapToItem(MotionEvent, int)}. When a scroll * snap happens, call either this method or {@link #onSnapToItem(MotionEvent, int)}, not both. * This method is useful when you have no direct access to the {@link MotionEvent} that * caused the snap event. * * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion triggering * the snap. * @param source the input source of the motion causing the snap. * @param axis the axis of {@code event} that caused the item to snap. * * @see #onSnapToItem(MotionEvent, int) */ void onSnapToItem(int inputDeviceId, int source, int axis); /** * Call this when the view has snapped to an item, with a motion from a given * {@link MotionEvent} on a given {@code axis}. * * <p>The interface is not aware of the internal scroll states of the view for which scroll * feedback is played. As such, the client should call Loading @@ -33,22 +62,69 @@ public interface ScrollFeedbackProvider { * * @param event the {@link MotionEvent} that caused the item to snap. * @param axis the axis of {@code event} that caused the item to snap. * * @see #onSnapToItem(int, int, int) */ default void onSnapToItem(@NonNull MotionEvent event, int axis) { onSnapToItem(event.getDeviceId(), event.getSource(), axis); } /** * Call this when the view has reached the scroll limit when scrolled by a motion generated by * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and * on a given motion event {@code axis}. * * <p>This method has the same purpose as {@link #onScrollLimit(MotionEvent, int, boolean)}. * When a scroll limit happens, call either this method or * {@link #onScrollLimit(MotionEvent, int, boolean)}, not both. This method is useful when you * have no direct access to the {@link MotionEvent} that caused the scroll limit. * * @param inputDeviceId the ID of the {@link InputDevice} that caused scrolling to hit limit. * @param source the input source of the motion that caused scrolling to hit the limit. * @param axis the axis of {@code event} that caused scrolling to hit the limit. * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and * {@code false} if the scrolling hit limit at the end of the scrolling list. * * @see #onScrollLimit(MotionEvent, int, boolean) */ void onSnapToItem(MotionEvent event, int axis); void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart); /** * The view has reached the scroll limit when scrolled by the motion from a given * Call this when the view has reached the scroll limit when scrolled by the motion from a given * {@link MotionEvent} on a given {@code axis}. * * @param event the {@link MotionEvent} that caused scrolling to hit the limit. * @param axis the axis of {@code event} that caused scrolling to hit the limit. * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and * {@code false} if the scrolling hit limit at the end of the scrolling list. * * @see #onScrollLimit(int, int, int, boolean) */ default void onScrollLimit(@NonNull MotionEvent event, int axis, boolean isStart) { onScrollLimit(event.getDeviceId(), event.getSource(), axis, isStart); } /** * Call this when the view has scrolled by {@code deltaInPixels} due to the motion generated by * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and * on a given motion event {@code axis}. * * <p>This method has the same purpose as {@link #onScrollProgress(MotionEvent, int, int)}. * When a scroll progress happens, call either this method or * {@link #onScrollProgress(MotionEvent, int, int)}, not both. This method is useful when you * have no direct access to the {@link MotionEvent} that caused the scroll progress. * * @param inputDeviceId the ID of the {@link InputDevice} that caused scroll progress. * @param source the input source of the motion that caused scroll progress. * @param axis the axis of {@code event} that caused scroll progress. * @param deltaInPixels the amount of scroll progress, in pixels. * * @see #onScrollProgress(MotionEvent, int, int) */ void onScrollLimit(MotionEvent event, int axis, boolean isStart); void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels); /** * The view has scrolled by {@code deltaInPixels} due to the motion from a given * Call this when the view has scrolled by {@code deltaInPixels} due to the motion from a given * {@link MotionEvent} on a given {@code axis}. * * <p>The interface is not aware of the internal scroll states of the view for which scroll Loading @@ -58,6 +134,10 @@ public interface ScrollFeedbackProvider { * @param event the {@link MotionEvent} that caused scroll progress. * @param axis the axis of {@code event} that caused scroll progress. * @param deltaInPixels the amount of scroll progress, in pixels. * * @see #onScrollProgress(int, int, int, int) */ void onScrollProgress(MotionEvent event, int axis, int deltaInPixels); default void onScrollProgress(@NonNull MotionEvent event, int axis, int deltaInPixels) { onScrollProgress(event.getDeviceId(), event.getSource(), axis, deltaInPixels); } } core/java/android/view/ViewConfiguration.java +110 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package android.view; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.TestApi; Loading @@ -37,6 +38,7 @@ import android.provider.Settings; import android.util.DisplayMetrics; import android.util.SparseArray; import android.util.TypedValue; import android.view.flags.Flags; /** * Contains methods to standard constants used in the UI for timeouts, sizes, and distances. Loading Loading @@ -240,6 +242,9 @@ public class ViewConfiguration { /** Value used as a maximum fling velocity, when fling is not supported. */ private static final int NO_FLING_MAX_VELOCITY = Integer.MIN_VALUE; /** @hide */ public static final int NO_HAPTIC_SCROLL_TICK_INTERVAL = Integer.MAX_VALUE; /** * Delay before dispatching a recurring accessibility event in milliseconds. * This delay guarantees that a recurring event will be send at most once Loading Loading @@ -343,6 +348,8 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mMinimumRotaryEncoderFlingVelocity; private final int mMaximumRotaryEncoderFlingVelocity; private final int mRotaryEncoderHapticScrollFeedbackTickIntervalPixels; private final boolean mRotaryEncoderHapticScrollFeedbackEnabled; private final int mScrollbarSize; private final int mTouchSlop; private final int mHandwritingSlop; Loading Loading @@ -390,6 +397,8 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mMinimumRotaryEncoderFlingVelocity = MINIMUM_FLING_VELOCITY; mMaximumRotaryEncoderFlingVelocity = MAXIMUM_FLING_VELOCITY; mRotaryEncoderHapticScrollFeedbackEnabled = false; mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = NO_HAPTIC_SCROLL_TICK_INTERVAL; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; mHandwritingSlop = HANDWRITING_SLOP; Loading Loading @@ -529,6 +538,20 @@ public class ViewConfiguration { mMaximumRotaryEncoderFlingVelocity = configMaxRotaryEncoderFlingVelocity; } int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels = res.getDimensionPixelSize( com.android.internal.R.dimen .config_rotaryEncoderAxisScrollTickInterval); mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0 ? configRotaryEncoderHapticScrollFeedbackTickIntervalPixels : NO_HAPTIC_SCROLL_TICK_INTERVAL; mRotaryEncoderHapticScrollFeedbackEnabled = res.getBoolean( com.android.internal.R.bool .config_viewRotaryEncoderHapticScrollFedbackEnabled); mGlobalActionsKeyTimeout = res.getInteger( com.android.internal.R.integer.config_globalActionsKeyTimeout); Loading Loading @@ -1193,6 +1216,93 @@ public class ViewConfiguration { return mMaximumFlingVelocity; } /** * Checks if any kind of scroll haptic feedback is enabled for a motion generated by a specific * input device configuration and motion axis. * * <h3>Obtaining the correct arguments for this method call</h3> * <p><b>inputDeviceId</b>: if calling this method in response to a {@link MotionEvent}, use * the device ID that is reported by the event, which can be obtained using * {@link MotionEvent#getDeviceId()}. Otherwise, use a valid ID that is obtained from * {@link InputDevice#getId()}, or from an {@link InputManager} instance * ({@link InputManager#getInputDeviceIds()} gives all the valid input device IDs). * * <p><b>axis</b>: a {@link MotionEvent} may report data for multiple axes, and each axis may * have multiple data points for different pointers. Use the axis whose movement produced the * scrolls that would generate the scroll haptics. You can use * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the * {@link InputDevice}, from which you can derive all the valid axes for the device. * * <p><b>source</b>: use {@link MotionEvent#getSource()} if calling this method in response to a * {@link MotionEvent}. Otherwise, use a valid source for the {@link InputDevice}. You can use * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the * {@link InputDevice}, from which you can derive all the valid sources for the device. * * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may * produce scroll haptics. * @param source the input source of the motion that may produce scroll haptics. * @param axis the axis of the motion that may produce scroll haptics. * @return {@code true} if motions generated by the provided input and motion configuration * should produce scroll haptics. {@code false} otherwise. */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public boolean isHapticScrollFeedbackEnabled(int inputDeviceId, int axis, int source) { if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return false; if (source == InputDevice.SOURCE_ROTARY_ENCODER) { return mRotaryEncoderHapticScrollFeedbackEnabled; } return false; } /** * Provides the minimum scroll interval (in pixels) between consecutive scroll tick haptics for * motions generated by a specific input device configuration and motion axis. * * <p><b>Scroll tick</b> here refers to an interval-based, consistent scroll feedback provided * to the user as the user scrolls through a scrollable view. * * <p>If you are supporting scroll tick haptics, use this interval as the minimum pixel scroll * distance between consecutive scroll ticks. That is, once your view has scrolled for at least * this interval, play a haptic, and wait again until the view has further scrolled with this * interval in the same direction before playing the next scroll haptic. * * <p>Some devices may support other types of scroll haptics but not interval based tick * haptics. In those cases, this method will return {@code Integer.MAX_VALUE}. The same value * will be returned if the device does not support scroll haptics at all (which can be checked * via {@link #isHapticScrollFeedbackEnabled(int, int, int)}). * * <p>See {@link #isHapticScrollFeedbackEnabled(int, int, int)} for more details about obtaining * the correct arguments for this method. * * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may * produce scroll haptics. * @param source the input source of the motion that may produce scroll haptics. * @param axis the axis of the motion that may produce scroll haptics. * @return the absolute value of the minimum scroll interval, in pixels, between consecutive * scroll feedback haptics for motions generated by the provided input and motion * configuration. If scroll haptics is disabled for the given configuration, or if the * device does not support scroll tick haptics for the given configuration, this method * returns {@code Integer.MAX_VALUE}. */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public int getHapticScrollFeedbackTickInterval(int inputDeviceId, int axis, int source) { if (!mRotaryEncoderHapticScrollFeedbackEnabled) { return NO_HAPTIC_SCROLL_TICK_INTERVAL; } if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) { return NO_HAPTIC_SCROLL_TICK_INTERVAL; } if (source == InputDevice.SOURCE_ROTARY_ENCODER) { return mRotaryEncoderHapticScrollFeedbackTickIntervalPixels; } return NO_HAPTIC_SCROLL_TICK_INTERVAL; } private static boolean isInputDeviceInfoValid(int id, int axis, int source) { InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id); return device != null && device.getMotionRange(axis, source) != null; Loading core/java/android/view/flags/scroll_feedback_flags.aconfig 0 → 100644 +8 −0 Original line number Diff line number Diff line package: "android.view.flags" flag { namespace: "toolkit" name: "scroll_feedback_api" description: "Enable the scroll feedback APIs" bug: "239594271" } No newline at end of file Loading
core/api/current.txt +18 −0 Original line number Diff line number Diff line Loading @@ -50040,6 +50040,13 @@ package android.view { field public static final int VIRTUAL_KEY_RELEASE = 8; // 0x8 } @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public class HapticScrollFeedbackProvider implements android.view.ScrollFeedbackProvider { ctor public HapticScrollFeedbackProvider(@NonNull android.view.View); method public void onScrollLimit(int, int, int, boolean); method public void onScrollProgress(int, int, int, int); method public void onSnapToItem(int, int, int); } public class InflateException extends java.lang.RuntimeException { ctor public InflateException(); ctor public InflateException(String, Throwable); Loading Loading @@ -51205,6 +51212,15 @@ package android.view { method @UiThread public void updatePositionInWindow(); } @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public interface ScrollFeedbackProvider { method public void onScrollLimit(int, int, int, boolean); method public default void onScrollLimit(@NonNull android.view.MotionEvent, int, boolean); method public void onScrollProgress(int, int, int, int); method public default void onScrollProgress(@NonNull android.view.MotionEvent, int, int); method public void onSnapToItem(int, int, int); method public default void onSnapToItem(@NonNull android.view.MotionEvent, int); } public class SearchEvent { ctor public SearchEvent(android.view.InputDevice); method public android.view.InputDevice getInputDevice(); Loading Loading @@ -52482,6 +52498,7 @@ package android.view { method @Deprecated public static int getEdgeSlop(); method @Deprecated public static int getFadingEdgeLength(); method @Deprecated public static long getGlobalActionKeyTimeout(); method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public int getHapticScrollFeedbackTickInterval(int, int, int); method public static int getJumpTapTimeout(); method public static int getKeyRepeatDelay(); method public static int getKeyRepeatTimeout(); Loading Loading @@ -52521,6 +52538,7 @@ package android.view { method @Deprecated public static int getWindowTouchSlop(); method public static long getZoomControlsTimeout(); method public boolean hasPermanentMenuKey(); method @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public boolean isHapticScrollFeedbackEnabled(int, int, int); method public boolean shouldShowMenuShortcutsWhenKeyboardPresent(); }
core/java/android/view/HapticScrollFeedbackProvider.java +39 −36 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package android.view; import static com.android.internal.R.dimen.config_rotaryEncoderAxisScrollTickInterval; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.view.flags.Flags; import com.android.internal.annotations.VisibleForTesting; Loading @@ -25,16 +27,15 @@ import com.android.internal.annotations.VisibleForTesting; * * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state * is isolated. * * @hide */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { private static final String TAG = "HapticScrollFeedbackProvider"; private static final int TICK_INTERVAL_NO_TICK = 0; private static final int TICK_INTERVAL_UNSET = Integer.MAX_VALUE; private final View mView; private final ViewConfiguration mViewConfig; // Info about the cause of the latest scroll event. Loading @@ -49,26 +50,35 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { * Cache for tick interval for scroll tick caused by a {@link InputDevice#SOURCE_ROTARY_ENCODER} * on {@link MotionEvent#AXIS_SCROLL}. Set to -1 if the value has not been fetched and cached. */ private int mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_UNSET; /** The tick interval corresponding to the current InputDevice/source/axis. */ private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK; private int mTotalScrollPixels = 0; private boolean mCanPlayLimitFeedback = true; private boolean mHapticScrollFeedbackEnabled = false; public HapticScrollFeedbackProvider(View view) { this(view, /* rotaryEncoderAxisScrollTickIntervalPixels= */ TICK_INTERVAL_UNSET); public HapticScrollFeedbackProvider(@NonNull View view) { this(view, ViewConfiguration.get(view.getContext())); } /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public HapticScrollFeedbackProvider(View view, int rotaryEncoderAxisScrollTickIntervalPixels) { public HapticScrollFeedbackProvider(View view, ViewConfiguration viewConfig) { mView = view; mRotaryEncoderAxisScrollTickIntervalPixels = rotaryEncoderAxisScrollTickIntervalPixels; mViewConfig = viewConfig; } @Override public void onScrollProgress(MotionEvent event, int axis, int deltaInPixels) { maybeUpdateCurrentConfig(event, axis); public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) { maybeUpdateCurrentConfig(inputDeviceId, source, axis); if (!mHapticScrollFeedbackEnabled) { return; } // Unlock limit feedback regardless of scroll tick being enabled as long as there's a // non-zero scroll progress. if (deltaInPixels != 0) { mCanPlayLimitFeedback = true; } if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) { // There's no valid tick interval. Exit early before doing any further computation. Loading @@ -82,13 +92,14 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); } mCanPlayLimitFeedback = true; } @Override public void onScrollLimit(MotionEvent event, int axis, boolean isStart) { maybeUpdateCurrentConfig(event, axis); public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) { maybeUpdateCurrentConfig(inputDeviceId, source, axis); if (!mHapticScrollFeedbackEnabled) { return; } if (!mCanPlayLimitFeedback) { return; Loading @@ -101,41 +112,33 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { } @Override public void onSnapToItem(MotionEvent event, int axis) { public void onSnapToItem(int inputDeviceId, int source, int axis) { maybeUpdateCurrentConfig(inputDeviceId, source, axis); if (!mHapticScrollFeedbackEnabled) { return; } // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); mCanPlayLimitFeedback = true; } private void maybeUpdateCurrentConfig(MotionEvent event, int axis) { int source = event.getSource(); int deviceId = event.getDeviceId(); private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) { if (mAxis != axis || mSource != source || mDeviceId != deviceId) { mSource = source; mAxis = axis; mDeviceId = deviceId; mHapticScrollFeedbackEnabled = mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source); mCanPlayLimitFeedback = true; mTotalScrollPixels = 0; calculateTickIntervals(source, axis); updateTickIntervals(deviceId, source, axis); } } private void calculateTickIntervals(int source, int axis) { mTickIntervalPixels = TICK_INTERVAL_NO_TICK; if (axis == MotionEvent.AXIS_SCROLL && source == InputDevice.SOURCE_ROTARY_ENCODER) { if (mRotaryEncoderAxisScrollTickIntervalPixels == TICK_INTERVAL_UNSET) { // Value has not been fetched yet. Fetch and cache it. mRotaryEncoderAxisScrollTickIntervalPixels = mView.getContext().getResources().getDimensionPixelSize( config_rotaryEncoderAxisScrollTickInterval); if (mRotaryEncoderAxisScrollTickIntervalPixels < 0) { mRotaryEncoderAxisScrollTickIntervalPixels = TICK_INTERVAL_NO_TICK; } } mTickIntervalPixels = mRotaryEncoderAxisScrollTickIntervalPixels; } private void updateTickIntervals(int deviceId, int source, int axis) { mTickIntervalPixels = mHapticScrollFeedbackEnabled ? mViewConfig.getHapticScrollFeedbackTickInterval(deviceId, axis, source) : TICK_INTERVAL_NO_TICK; } }
core/java/android/view/ScrollFeedbackProvider.java +88 −8 Original line number Diff line number Diff line Loading @@ -16,16 +16,45 @@ package android.view; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.view.flags.Flags; /** * Interface to represent an entity giving consistent feedback for different events surrounding view * scroll. * * @hide * <p>When you have access to the {@link MotionEvent}s that triggered the different scroll events, * use the {@link MotionEvent} based APIs in this class. If you do not have access to the motion * events, you can use the methods that accept the {@link InputDevice} ID (which can be obtained by * APIs like {@link MotionEvent#getDeviceId()} and {@link InputDevice#getId()}) and source (which * can be obtained by APIs like {@link MotionEvent#getSource()}) of the motion that caused the * scroll events. */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public interface ScrollFeedbackProvider { /** * The view has snapped to an item, with a motion from a given {@link MotionEvent} on a given * {@code axis}. * Call this when the view has snapped to an item, with a motion generated by an * {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and on * a given motion event {@code axis}. * * <p>This method has the same purpose as {@link #onSnapToItem(MotionEvent, int)}. When a scroll * snap happens, call either this method or {@link #onSnapToItem(MotionEvent, int)}, not both. * This method is useful when you have no direct access to the {@link MotionEvent} that * caused the snap event. * * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion triggering * the snap. * @param source the input source of the motion causing the snap. * @param axis the axis of {@code event} that caused the item to snap. * * @see #onSnapToItem(MotionEvent, int) */ void onSnapToItem(int inputDeviceId, int source, int axis); /** * Call this when the view has snapped to an item, with a motion from a given * {@link MotionEvent} on a given {@code axis}. * * <p>The interface is not aware of the internal scroll states of the view for which scroll * feedback is played. As such, the client should call Loading @@ -33,22 +62,69 @@ public interface ScrollFeedbackProvider { * * @param event the {@link MotionEvent} that caused the item to snap. * @param axis the axis of {@code event} that caused the item to snap. * * @see #onSnapToItem(int, int, int) */ default void onSnapToItem(@NonNull MotionEvent event, int axis) { onSnapToItem(event.getDeviceId(), event.getSource(), axis); } /** * Call this when the view has reached the scroll limit when scrolled by a motion generated by * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and * on a given motion event {@code axis}. * * <p>This method has the same purpose as {@link #onScrollLimit(MotionEvent, int, boolean)}. * When a scroll limit happens, call either this method or * {@link #onScrollLimit(MotionEvent, int, boolean)}, not both. This method is useful when you * have no direct access to the {@link MotionEvent} that caused the scroll limit. * * @param inputDeviceId the ID of the {@link InputDevice} that caused scrolling to hit limit. * @param source the input source of the motion that caused scrolling to hit the limit. * @param axis the axis of {@code event} that caused scrolling to hit the limit. * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and * {@code false} if the scrolling hit limit at the end of the scrolling list. * * @see #onScrollLimit(MotionEvent, int, boolean) */ void onSnapToItem(MotionEvent event, int axis); void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart); /** * The view has reached the scroll limit when scrolled by the motion from a given * Call this when the view has reached the scroll limit when scrolled by the motion from a given * {@link MotionEvent} on a given {@code axis}. * * @param event the {@link MotionEvent} that caused scrolling to hit the limit. * @param axis the axis of {@code event} that caused scrolling to hit the limit. * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and * {@code false} if the scrolling hit limit at the end of the scrolling list. * * @see #onScrollLimit(int, int, int, boolean) */ default void onScrollLimit(@NonNull MotionEvent event, int axis, boolean isStart) { onScrollLimit(event.getDeviceId(), event.getSource(), axis, isStart); } /** * Call this when the view has scrolled by {@code deltaInPixels} due to the motion generated by * an {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and * on a given motion event {@code axis}. * * <p>This method has the same purpose as {@link #onScrollProgress(MotionEvent, int, int)}. * When a scroll progress happens, call either this method or * {@link #onScrollProgress(MotionEvent, int, int)}, not both. This method is useful when you * have no direct access to the {@link MotionEvent} that caused the scroll progress. * * @param inputDeviceId the ID of the {@link InputDevice} that caused scroll progress. * @param source the input source of the motion that caused scroll progress. * @param axis the axis of {@code event} that caused scroll progress. * @param deltaInPixels the amount of scroll progress, in pixels. * * @see #onScrollProgress(MotionEvent, int, int) */ void onScrollLimit(MotionEvent event, int axis, boolean isStart); void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels); /** * The view has scrolled by {@code deltaInPixels} due to the motion from a given * Call this when the view has scrolled by {@code deltaInPixels} due to the motion from a given * {@link MotionEvent} on a given {@code axis}. * * <p>The interface is not aware of the internal scroll states of the view for which scroll Loading @@ -58,6 +134,10 @@ public interface ScrollFeedbackProvider { * @param event the {@link MotionEvent} that caused scroll progress. * @param axis the axis of {@code event} that caused scroll progress. * @param deltaInPixels the amount of scroll progress, in pixels. * * @see #onScrollProgress(int, int, int, int) */ void onScrollProgress(MotionEvent event, int axis, int deltaInPixels); default void onScrollProgress(@NonNull MotionEvent event, int axis, int deltaInPixels) { onScrollProgress(event.getDeviceId(), event.getSource(), axis, deltaInPixels); } }
core/java/android/view/ViewConfiguration.java +110 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package android.view; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.TestApi; Loading @@ -37,6 +38,7 @@ import android.provider.Settings; import android.util.DisplayMetrics; import android.util.SparseArray; import android.util.TypedValue; import android.view.flags.Flags; /** * Contains methods to standard constants used in the UI for timeouts, sizes, and distances. Loading Loading @@ -240,6 +242,9 @@ public class ViewConfiguration { /** Value used as a maximum fling velocity, when fling is not supported. */ private static final int NO_FLING_MAX_VELOCITY = Integer.MIN_VALUE; /** @hide */ public static final int NO_HAPTIC_SCROLL_TICK_INTERVAL = Integer.MAX_VALUE; /** * Delay before dispatching a recurring accessibility event in milliseconds. * This delay guarantees that a recurring event will be send at most once Loading Loading @@ -343,6 +348,8 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mMinimumRotaryEncoderFlingVelocity; private final int mMaximumRotaryEncoderFlingVelocity; private final int mRotaryEncoderHapticScrollFeedbackTickIntervalPixels; private final boolean mRotaryEncoderHapticScrollFeedbackEnabled; private final int mScrollbarSize; private final int mTouchSlop; private final int mHandwritingSlop; Loading Loading @@ -390,6 +397,8 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mMinimumRotaryEncoderFlingVelocity = MINIMUM_FLING_VELOCITY; mMaximumRotaryEncoderFlingVelocity = MAXIMUM_FLING_VELOCITY; mRotaryEncoderHapticScrollFeedbackEnabled = false; mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = NO_HAPTIC_SCROLL_TICK_INTERVAL; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; mHandwritingSlop = HANDWRITING_SLOP; Loading Loading @@ -529,6 +538,20 @@ public class ViewConfiguration { mMaximumRotaryEncoderFlingVelocity = configMaxRotaryEncoderFlingVelocity; } int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels = res.getDimensionPixelSize( com.android.internal.R.dimen .config_rotaryEncoderAxisScrollTickInterval); mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0 ? configRotaryEncoderHapticScrollFeedbackTickIntervalPixels : NO_HAPTIC_SCROLL_TICK_INTERVAL; mRotaryEncoderHapticScrollFeedbackEnabled = res.getBoolean( com.android.internal.R.bool .config_viewRotaryEncoderHapticScrollFedbackEnabled); mGlobalActionsKeyTimeout = res.getInteger( com.android.internal.R.integer.config_globalActionsKeyTimeout); Loading Loading @@ -1193,6 +1216,93 @@ public class ViewConfiguration { return mMaximumFlingVelocity; } /** * Checks if any kind of scroll haptic feedback is enabled for a motion generated by a specific * input device configuration and motion axis. * * <h3>Obtaining the correct arguments for this method call</h3> * <p><b>inputDeviceId</b>: if calling this method in response to a {@link MotionEvent}, use * the device ID that is reported by the event, which can be obtained using * {@link MotionEvent#getDeviceId()}. Otherwise, use a valid ID that is obtained from * {@link InputDevice#getId()}, or from an {@link InputManager} instance * ({@link InputManager#getInputDeviceIds()} gives all the valid input device IDs). * * <p><b>axis</b>: a {@link MotionEvent} may report data for multiple axes, and each axis may * have multiple data points for different pointers. Use the axis whose movement produced the * scrolls that would generate the scroll haptics. You can use * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the * {@link InputDevice}, from which you can derive all the valid axes for the device. * * <p><b>source</b>: use {@link MotionEvent#getSource()} if calling this method in response to a * {@link MotionEvent}. Otherwise, use a valid source for the {@link InputDevice}. You can use * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the * {@link InputDevice}, from which you can derive all the valid sources for the device. * * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may * produce scroll haptics. * @param source the input source of the motion that may produce scroll haptics. * @param axis the axis of the motion that may produce scroll haptics. * @return {@code true} if motions generated by the provided input and motion configuration * should produce scroll haptics. {@code false} otherwise. */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public boolean isHapticScrollFeedbackEnabled(int inputDeviceId, int axis, int source) { if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return false; if (source == InputDevice.SOURCE_ROTARY_ENCODER) { return mRotaryEncoderHapticScrollFeedbackEnabled; } return false; } /** * Provides the minimum scroll interval (in pixels) between consecutive scroll tick haptics for * motions generated by a specific input device configuration and motion axis. * * <p><b>Scroll tick</b> here refers to an interval-based, consistent scroll feedback provided * to the user as the user scrolls through a scrollable view. * * <p>If you are supporting scroll tick haptics, use this interval as the minimum pixel scroll * distance between consecutive scroll ticks. That is, once your view has scrolled for at least * this interval, play a haptic, and wait again until the view has further scrolled with this * interval in the same direction before playing the next scroll haptic. * * <p>Some devices may support other types of scroll haptics but not interval based tick * haptics. In those cases, this method will return {@code Integer.MAX_VALUE}. The same value * will be returned if the device does not support scroll haptics at all (which can be checked * via {@link #isHapticScrollFeedbackEnabled(int, int, int)}). * * <p>See {@link #isHapticScrollFeedbackEnabled(int, int, int)} for more details about obtaining * the correct arguments for this method. * * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion that may * produce scroll haptics. * @param source the input source of the motion that may produce scroll haptics. * @param axis the axis of the motion that may produce scroll haptics. * @return the absolute value of the minimum scroll interval, in pixels, between consecutive * scroll feedback haptics for motions generated by the provided input and motion * configuration. If scroll haptics is disabled for the given configuration, or if the * device does not support scroll tick haptics for the given configuration, this method * returns {@code Integer.MAX_VALUE}. */ @FlaggedApi(Flags.FLAG_SCROLL_FEEDBACK_API) public int getHapticScrollFeedbackTickInterval(int inputDeviceId, int axis, int source) { if (!mRotaryEncoderHapticScrollFeedbackEnabled) { return NO_HAPTIC_SCROLL_TICK_INTERVAL; } if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) { return NO_HAPTIC_SCROLL_TICK_INTERVAL; } if (source == InputDevice.SOURCE_ROTARY_ENCODER) { return mRotaryEncoderHapticScrollFeedbackTickIntervalPixels; } return NO_HAPTIC_SCROLL_TICK_INTERVAL; } private static boolean isInputDeviceInfoValid(int id, int axis, int source) { InputDevice device = InputManagerGlobal.getInstance().getInputDevice(id); return device != null && device.getMotionRange(axis, source) != null; Loading
core/java/android/view/flags/scroll_feedback_flags.aconfig 0 → 100644 +8 −0 Original line number Diff line number Diff line package: "android.view.flags" flag { namespace: "toolkit" name: "scroll_feedback_api" description: "Enable the scroll feedback APIs" bug: "239594271" } No newline at end of file