Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4391aaa1 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit Committed by Android (Google) Code Review
Browse files

Merge "Make ScrollFeedbackProvider APIs Public" into main

parents b2b28ed1 0aaed5a6
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -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);
@@ -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();
@@ -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();
@@ -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();
  }
+39 −36
Original line number Diff line number Diff line
@@ -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;

@@ -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.
@@ -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.
@@ -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;
@@ -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;
    }
}
+88 −8
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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);
    }
}
+110 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.view;

import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.TestApi;
@@ -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.
@@ -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
@@ -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;
@@ -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;
@@ -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);

@@ -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;
+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