• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import android.annotation.NonNull;
20 
21 import com.android.internal.annotations.VisibleForTesting;
22 
23 /**
24  * {@link ScrollFeedbackProvider} that performs haptic feedback when scrolling.
25  *
26  * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state
27  * is isolated.
28  *
29  * <p>Check {@link ScrollFeedbackProvider} for details on the arguments that should be passed to the
30  * methods in this class. To check if your input device ID, source, and motion axis are valid for
31  * haptic feedback, you can use the
32  * {@link ViewConfiguration#isHapticScrollFeedbackEnabled(int, int, int)} API.
33  *
34  * @hide
35  */
36 public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider {
37     private static final String TAG = "HapticScrollFeedbackProvider";
38 
39     private static final int TICK_INTERVAL_NO_TICK = 0;
40     private static final boolean INITIAL_END_OF_LIST_HAPTICS_ENABLED = false;
41 
42     private final View mView;
43     private final ViewConfiguration mViewConfig;
44     /**
45      * Flag to disable the logic in this class if the View-based scroll haptics implementation is
46      * enabled. If {@code false}, this class will continue to run despite the View's scroll
47      * haptics implementation being enabled. This value should be set to {@code true} when this
48      * class is directly used by the View class.
49      */
50     private final boolean mDisabledIfViewPlaysScrollHaptics;
51 
52 
53     // Info about the cause of the latest scroll event.
54     /** The ID of the {link @InputDevice} that caused the latest scroll event. */
55     private int mDeviceId = -1;
56     /** The axis on which the latest scroll event happened. */
57     private int mAxis = -1;
58     /** The {@link InputDevice} source from which the latest scroll event happened. */
59     private int mSource = -1;
60 
61     /** The tick interval corresponding to the current InputDevice/source/axis. */
62     private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK;
63     private int mTotalScrollPixels = 0;
64     private boolean mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
65     private boolean mHapticScrollFeedbackEnabled = false;
66 
HapticScrollFeedbackProvider(@onNull View view)67     public HapticScrollFeedbackProvider(@NonNull View view) {
68         this(view, ViewConfiguration.get(view.getContext()),
69                 /* disabledIfViewPlaysScrollHaptics= */ true);
70     }
71 
72     /** @hide */
73     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
HapticScrollFeedbackProvider( View view, ViewConfiguration viewConfig, boolean disabledIfViewPlaysScrollHaptics)74     public HapticScrollFeedbackProvider(
75             View view, ViewConfiguration viewConfig, boolean disabledIfViewPlaysScrollHaptics) {
76         mView = view;
77         mViewConfig = viewConfig;
78         mDisabledIfViewPlaysScrollHaptics = disabledIfViewPlaysScrollHaptics;
79     }
80 
81     @Override
onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels)82     public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) {
83         maybeUpdateCurrentConfig(inputDeviceId, source, axis);
84         if (!mHapticScrollFeedbackEnabled) {
85             return;
86         }
87 
88         // Unlock limit feedback regardless of scroll tick being enabled as long as there's a
89         // non-zero scroll progress.
90         if (deltaInPixels != 0) {
91             mCanPlayLimitFeedback = true;
92         }
93 
94         if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) {
95             // There's no valid tick interval. Exit early before doing any further computation.
96             return;
97         }
98 
99         mTotalScrollPixels += deltaInPixels;
100 
101         if (Math.abs(mTotalScrollPixels) >= mTickIntervalPixels) {
102             mTotalScrollPixels %= mTickIntervalPixels;
103             // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here.
104             mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK);
105         }
106     }
107 
108     @Override
onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart)109     public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) {
110         maybeUpdateCurrentConfig(inputDeviceId, source, axis);
111         if (!mHapticScrollFeedbackEnabled) {
112             return;
113         }
114 
115         if (!mCanPlayLimitFeedback) {
116             return;
117         }
118 
119         // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here.
120         mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT);
121 
122         mCanPlayLimitFeedback = false;
123     }
124 
125     @Override
onSnapToItem(int inputDeviceId, int source, int axis)126     public void onSnapToItem(int inputDeviceId, int source, int axis) {
127         maybeUpdateCurrentConfig(inputDeviceId, source, axis);
128         if (!mHapticScrollFeedbackEnabled) {
129             return;
130         }
131         // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here.
132         mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS);
133         mCanPlayLimitFeedback = true;
134     }
135 
maybeUpdateCurrentConfig(int deviceId, int source, int axis)136     private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) {
137         if (mAxis != axis || mSource != source || mDeviceId != deviceId) {
138             if (mDisabledIfViewPlaysScrollHaptics
139                     && (source == InputDevice.SOURCE_ROTARY_ENCODER)
140                     && mViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) {
141                 mHapticScrollFeedbackEnabled = false;
142                 return;
143             }
144             mSource = source;
145             mAxis = axis;
146             mDeviceId = deviceId;
147 
148             mHapticScrollFeedbackEnabled =
149                     mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source);
150             mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
151             mTotalScrollPixels = 0;
152             updateTickIntervals(deviceId, source, axis);
153         }
154     }
155 
updateTickIntervals(int deviceId, int source, int axis)156     private void updateTickIntervals(int deviceId, int source, int axis) {
157         mTickIntervalPixels = mHapticScrollFeedbackEnabled
158                 ? mViewConfig.getHapticScrollFeedbackTickInterval(deviceId, axis, source)
159                 : TICK_INTERVAL_NO_TICK;
160     }
161 }
162