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