• 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 static android.view.flags.Flags.dynamicViewRotaryHapticsConfiguration;
20 
21 import android.annotation.NonNull;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 
25 /**
26  * {@link ScrollFeedbackProvider} that performs haptic feedback when scrolling.
27  *
28  * <p>Each scrolling widget should have its own instance of this class to ensure that scroll state
29  * is isolated.
30  *
31  * <p>Check {@link ScrollFeedbackProvider} for details on the arguments that should be passed to the
32  * methods in this class. To check if your input device ID, source, and motion axis are valid for
33  * haptic feedback, you can use the
34  * {@link ViewConfiguration#isHapticScrollFeedbackEnabled(int, int, int)} API.
35  *
36  * @hide
37  */
38 public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider {
39     private static final String TAG = "HapticScrollFeedbackProvider";
40 
41     private static final int TICK_INTERVAL_NO_TICK = 0;
42     private static final boolean INITIAL_END_OF_LIST_HAPTICS_ENABLED = false;
43 
44     private final View mView;
45     private final ViewConfiguration mViewConfig;
46     /** Whether or not this provider is being used directly by the View class. */
47     private final boolean mIsFromView;
48 
49 
50     // Info about the cause of the latest scroll event.
51     /** The ID of the {link @InputDevice} that caused the latest scroll event. */
52     private int mDeviceId = -1;
53     /** The axis on which the latest scroll event happened. */
54     private int mAxis = -1;
55     /** The {@link InputDevice} source from which the latest scroll event happened. */
56     private int mSource = -1;
57 
58     /** The tick interval corresponding to the current InputDevice/source/axis. */
59     private int mTickIntervalPixels = TICK_INTERVAL_NO_TICK;
60     private int mTotalScrollPixels = 0;
61     private boolean mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
62     private boolean mHapticScrollFeedbackEnabled = false;
63 
HapticScrollFeedbackProvider(@onNull View view)64     public HapticScrollFeedbackProvider(@NonNull View view) {
65         this(view, ViewConfiguration.get(view.getContext()), /* isFromView= */ false);
66     }
67 
68     /** @hide */
69     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
HapticScrollFeedbackProvider( View view, ViewConfiguration viewConfig, boolean isFromView)70     public HapticScrollFeedbackProvider(
71             View view, ViewConfiguration viewConfig, boolean isFromView) {
72         mView = view;
73         mViewConfig = viewConfig;
74         mIsFromView = isFromView;
75         if (dynamicViewRotaryHapticsConfiguration() && !isFromView) {
76             // Disable the View class's rotary scroll feedback logic if this provider is not being
77             // directly used by the View class. This is to avoid double rotary scroll feedback:
78             // one from the View class, and one from this provider instance (i.e. mute the View
79             // class's rotary feedback and enable this provider).
80             view.disableRotaryScrollFeedback();
81         }
82     }
83 
84     @Override
onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels)85     public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) {
86         maybeUpdateCurrentConfig(inputDeviceId, source, axis);
87         if (!mHapticScrollFeedbackEnabled) {
88             return;
89         }
90 
91         // Unlock limit feedback regardless of scroll tick being enabled as long as there's a
92         // non-zero scroll progress.
93         if (deltaInPixels != 0) {
94             mCanPlayLimitFeedback = true;
95         }
96 
97         if (mTickIntervalPixels == TICK_INTERVAL_NO_TICK) {
98             // There's no valid tick interval. Exit early before doing any further computation.
99             return;
100         }
101 
102         mTotalScrollPixels += deltaInPixels;
103 
104         if (Math.abs(mTotalScrollPixels) >= mTickIntervalPixels) {
105             mTotalScrollPixels %= mTickIntervalPixels;
106             if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) {
107                 mView.performHapticFeedbackForInputDevice(
108                         HapticFeedbackConstants.SCROLL_TICK, inputDeviceId, source, /* flags= */ 0);
109             } else {
110                 mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK);
111             }
112         }
113     }
114 
115     @Override
onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart)116     public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) {
117         maybeUpdateCurrentConfig(inputDeviceId, source, axis);
118         if (!mHapticScrollFeedbackEnabled) {
119             return;
120         }
121 
122         if (!mCanPlayLimitFeedback) {
123             return;
124         }
125         if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) {
126             mView.performHapticFeedbackForInputDevice(
127                     HapticFeedbackConstants.SCROLL_LIMIT, inputDeviceId, source, /* flags= */ 0);
128         } else {
129             mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT);
130         }
131 
132         mCanPlayLimitFeedback = false;
133     }
134 
135     @Override
onSnapToItem(int inputDeviceId, int source, int axis)136     public void onSnapToItem(int inputDeviceId, int source, int axis) {
137         maybeUpdateCurrentConfig(inputDeviceId, source, axis);
138         if (!mHapticScrollFeedbackEnabled) {
139             return;
140         }
141         if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) {
142             mView.performHapticFeedbackForInputDevice(
143                     HapticFeedbackConstants.SCROLL_ITEM_FOCUS, inputDeviceId, source,
144                     /* flags= */ 0);
145         } else {
146             mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS);
147         }
148         mCanPlayLimitFeedback = true;
149     }
150 
maybeUpdateCurrentConfig(int deviceId, int source, int axis)151     private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) {
152         if (mAxis != axis || mSource != source || mDeviceId != deviceId) {
153             mSource = source;
154             mAxis = axis;
155             mDeviceId = deviceId;
156 
157             if (!dynamicViewRotaryHapticsConfiguration()
158                     && !mIsFromView
159                     && (source == InputDevice.SOURCE_ROTARY_ENCODER)
160                     && mViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) {
161                 mHapticScrollFeedbackEnabled = false;
162                 return;
163             }
164 
165             mHapticScrollFeedbackEnabled =
166                     mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source);
167             mCanPlayLimitFeedback = INITIAL_END_OF_LIST_HAPTICS_ENABLED;
168             mTotalScrollPixels = 0;
169             updateTickIntervals(deviceId, source, axis);
170         }
171     }
172 
updateTickIntervals(int deviceId, int source, int axis)173     private void updateTickIntervals(int deviceId, int source, int axis) {
174         mTickIntervalPixels = mHapticScrollFeedbackEnabled
175                 ? mViewConfig.getHapticScrollFeedbackTickInterval(deviceId, axis, source)
176                 : TICK_INTERVAL_NO_TICK;
177     }
178 }
179