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

Commit 6a60532a authored by Yeabkal Wubshit's avatar Yeabkal Wubshit
Browse files

Initial Implementation of HapticScrollHapticFeedbackProvider

Define ScrollFeedbackProvider interface, and a haptics-based
implementation for it.

Bug: 239594271
Test: atest FrameworksCoreTests:HapticScrollFeedbackProviderTest

Change-Id: Ia40a9ff7d3928940a94678ec03e89e05a0dbebbe
parent 9877bc08
Loading
Loading
Loading
Loading
+6 −8
Original line number Diff line number Diff line
@@ -129,27 +129,25 @@ public class HapticFeedbackConstants {
    public static final int REJECT = 17;

    /**
     * A haptic effect to provide texture while a rotary input device is being scrolled.
     * A haptic effect to provide texture while scrolling.
     *
     * @hide
     */
    public static final int ROTARY_SCROLL_TICK = 18;
    public static final int SCROLL_TICK = 18;

    /**
     * A haptic effect to signal that a list element has been focused while scrolling using a rotary
     * input device.
     * A haptic effect to signal that a list element has been focused while scrolling.
     *
     * @hide
     */
    public static final int ROTARY_SCROLL_ITEM_FOCUS = 19;
    public static final int SCROLL_ITEM_FOCUS = 19;

    /**
     * A haptic effect to signal reaching the scrolling limits of a list while scrolling using a
     * rotary input device.
     * A haptic effect to signal reaching the scrolling limits of a list while scrolling.
     *
     * @hide
     */
    public static final int ROTARY_SCROLL_LIMIT = 20;
    public static final int SCROLL_LIMIT = 20;

    /**
     * The user has toggled a switch or button into the on position.
+141 −0
Original line number Diff line number Diff line
/**
 * Copyright (C) 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.view;

import static com.android.internal.R.dimen.config_rotaryEncoderAxisScrollTickInterval;

import com.android.internal.annotations.VisibleForTesting;

/**
 * {@link ScrollFeedbackProvider} that performs haptic feedback when scrolling.
 *
 * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state
 * is isolated.
 *
 * @hide
 */
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;


    // Info about the cause of the latest scroll event.
    /** The ID of the {link @InputDevice} that caused the latest scroll event. */
    private int mDeviceId = -1;
    /** The axis on which the latest scroll event happened. */
    private int mAxis = -1;
    /** The {@link InputDevice} source from which the latest scroll event happened. */
    private int mSource = -1;

    /**
     * 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;

    public HapticScrollFeedbackProvider(View view) {
        this(view, /* rotaryEncoderAxisScrollTickIntervalPixels= */ TICK_INTERVAL_UNSET);
    }

    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public HapticScrollFeedbackProvider(View view, int rotaryEncoderAxisScrollTickIntervalPixels) {
        mView = view;
        mRotaryEncoderAxisScrollTickIntervalPixels = rotaryEncoderAxisScrollTickIntervalPixels;
    }

    @Override
    public void onScrollProgress(MotionEvent event, int axis, int deltaInPixels) {
        maybeUpdateCurrentConfig(event, axis);

        if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) {
            // There's no valid tick interval. Exit early before doing any further computation.
            return;
        }

        mTotalScrollPixels += deltaInPixels;

        if (Math.abs(mTotalScrollPixels) >= mTickIntervalPixels) {
            mTotalScrollPixels %= mTickIntervalPixels;
            // 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);

        if (!mCanPlayLimitFeedback) {
            return;
        }

        // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here.
        mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT);

        mCanPlayLimitFeedback = false;
    }

    @Override
    public void onSnapToItem(MotionEvent event, int axis) {
        // 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();

        if (mAxis != axis || mSource != source || mDeviceId != deviceId) {
            mSource = source;
            mAxis = axis;
            mDeviceId = deviceId;

            mCanPlayLimitFeedback = true;
            mTotalScrollPixels = 0;
            calculateTickIntervals(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;
        }
    }
}
+63 −0
Original line number Diff line number Diff line
/**
 * Copyright (C) 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.view;

/**
 * Interface to represent an entity giving consistent feedback for different events surrounding view
 * scroll.
 *
 * @hide
 */
public interface ScrollFeedbackProvider {
    /**
     * 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
     * {@link #onScrollLimit(MotionEvent, int, int)} when scrolling has reached limit.
     *
     * @param event the {@link MotionEvent} that caused the item to snap.
     * @param axis the axis of {@code event} that caused the item to snap.
     */
    void onSnapToItem(MotionEvent event, int axis);

    /**
     * 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.
     */
    void onScrollLimit(MotionEvent event, int axis, boolean isStart);

    /**
     * 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
     * feedback is played. As such, the client should call
     * {@link #onScrollLimit(MotionEvent, int, int)} when scrolling has reached limit.
     *
     * @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.
     */
    void onScrollProgress(MotionEvent event, int axis, int deltaInPixels);
}
+5 −0
Original line number Diff line number Diff line
@@ -2766,6 +2766,11 @@
         measured in dips per second. Setting this to -1dp disables rotary encoder fling.  -->
    <dimen name="config_viewMaxRotaryEncoderFlingVelocity">-1dp</dimen>

    <!-- Tick intervals in dp for rotary encoder scrolls on {@link MotionEvent#AXIS_SCROLL}
         generated by {@link InputDevice#SOURCE_ROTARY_ENCODER} devices. A valid tick interval value
         is a positive value. Setting this to 0dp disables scroll tick. -->
    <dimen name="config_rotaryEncoderAxisScrollTickInterval">21dp</dimen>

    <!-- Amount of time in ms the user needs to press the relevant key to bring up the
         global actions dialog -->
    <integer name="config_globalActionsKeyTimeout">500</integer>
+1 −0
Original line number Diff line number Diff line
@@ -2032,6 +2032,7 @@
  <java-symbol type="integer" name="config_notificationsBatteryMediumARGB" />
  <java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" />
  <java-symbol type="integer" name="config_notificationServiceArchiveSize" />
  <java-symbol type="dimen" name="config_rotaryEncoderAxisScrollTickInterval" />
  <java-symbol type="integer" name="config_previousVibrationsDumpLimit" />
  <java-symbol type="integer" name="config_defaultVibrationAmplitude" />
  <java-symbol type="dimen" name="config_hapticChannelMaxVibrationAmplitude" />
Loading