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

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

Merge "Initial Implementation of HapticScrollHapticFeedbackProvider"

parents 9d8af4ec 6a60532a
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