1 /*
2  * Copyright 2024 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 androidx.core.view;
18 
19 import android.os.Build;
20 import android.view.InputDevice;
21 import android.view.MotionEvent;
22 import android.view.ScrollFeedbackProvider;
23 import android.view.View;
24 import android.view.ViewConfiguration;
25 
26 import androidx.annotation.RequiresApi;
27 
28 import org.jspecify.annotations.NonNull;
29 
30 /** Compat to access {@link ScrollFeedbackProvider} across different build versions. */
31 public class ScrollFeedbackProviderCompat {
32 
33     private final ScrollFeedbackProviderImpl mImpl;
34 
ScrollFeedbackProviderCompat(@onNull View view)35     private ScrollFeedbackProviderCompat(@NonNull View view) {
36         if (Build.VERSION.SDK_INT >= 35) {
37             mImpl = new ScrollFeedbackProviderApi35Impl(view);
38         } else {
39             mImpl = new ScrollFeedbackProviderBaseImpl();
40         }
41     }
42 
43     /** Creates an instance of {@link ScrollFeedbackProviderCompat}. */
createProvider(@onNull View view)44     public static @NonNull ScrollFeedbackProviderCompat createProvider(@NonNull View view) {
45         return new ScrollFeedbackProviderCompat(view);
46     }
47 
48     /**
49      * Call this when the view has snapped to an item.
50      *
51      * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion triggering
52      *          the snap.
53      * @param source the input source of the motion causing the snap.
54      * @param axis the axis of {@code event} that caused the item to snap.
55      */
onSnapToItem(int inputDeviceId, int source, int axis)56     public void onSnapToItem(int inputDeviceId, int source, int axis) {
57         mImpl.onSnapToItem(inputDeviceId, source, axis);
58     }
59 
60     /**
61      * Call this when the view has reached the scroll limit.
62      *
63      * <p>Note that a feedback may not be provided on every call to this method. This interface, for
64      * instance, may provide feedback on every `N`th scroll limit event. For the interface to
65      * properly provide feedback when needed, call this method for each scroll limit event that you
66      * want to be accounted to scroll limit feedback.
67      *
68      * @param inputDeviceId the ID of the {@link InputDevice} that caused scrolling to hit limit.
69      * @param source the input source of the motion that caused scrolling to hit the limit.
70      * @param axis the axis of {@code event} that caused scrolling to hit the limit.
71      * @param isStart {@code true} if scrolling hit limit at the start of the scrolling list, and
72      *                {@code false} if the scrolling hit limit at the end of the scrolling list.
73      *                <i>start</i> and <i>end</i> in this context are not geometrical references.
74      *                Instead, they refer to the start and end of a scrolling experience. As such,
75      *                "start" for some views may be at the bottom of a scrolling list, while it may
76      *                be at the top of scrolling list for others.
77      */
onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart)78     public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) {
79         mImpl.onScrollLimit(inputDeviceId, source, axis, isStart);
80     }
81 
82     /**
83      * Call this when the view has scrolled.
84      *
85      * <p>Different axes have different ways to map their raw axis values to pixels for scrolling.
86      * When calling this method, use the scroll values in pixels by which the view was scrolled; do
87      * not use the raw axis values. That is, use whatever value is passed to one of View's scrolling
88      * methods (example: {@link View#scrollBy(int, int)}). For example, for vertical scrolling on
89      * {@link MotionEvent#AXIS_SCROLL}, convert the raw axis value to the equivalent pixels by using
90      * {@link ViewConfiguration#getScaledVerticalScrollFactor()}, and use that value for this method
91      * call.
92      *
93      * <p>Note that a feedback may not be provided on every call to this method. This interface, for
94      * instance, may provide feedback for every `x` pixels scrolled. For the interface to properly
95      * track scroll progress and provide feedback when needed, call this method for each scroll
96      * event that you want to be accounted to scroll feedback.
97      *
98      * @param inputDeviceId the ID of the {@link InputDevice} that caused scroll progress.
99      * @param source the input source of the motion that caused scroll progress.
100      * @param axis the axis of {@code event} that caused scroll progress.
101      * @param deltaInPixels the amount of scroll progress, in pixels.
102      */
onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels)103     public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) {
104         mImpl.onScrollProgress(inputDeviceId, source, axis, deltaInPixels);
105     }
106 
107     @RequiresApi(35)
108     private static class ScrollFeedbackProviderApi35Impl implements ScrollFeedbackProviderImpl {
109         private final ScrollFeedbackProvider mProvider;
110 
ScrollFeedbackProviderApi35Impl(View view)111         ScrollFeedbackProviderApi35Impl(View view) {
112             mProvider = ScrollFeedbackProvider.createProvider(view);
113         }
114 
115         @Override
onSnapToItem(int inputDeviceId, int source, int axis)116         public void onSnapToItem(int inputDeviceId, int source, int axis) {
117             mProvider.onSnapToItem(inputDeviceId, source, axis);
118         }
119 
120         @Override
onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart)121         public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) {
122             mProvider.onScrollLimit(inputDeviceId, source, axis, isStart);
123         }
124 
125         @Override
onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels)126         public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) {
127             mProvider.onScrollProgress(inputDeviceId, source, axis, deltaInPixels);
128         }
129     }
130 
131     private static class ScrollFeedbackProviderBaseImpl implements ScrollFeedbackProviderImpl {
132         @Override
onSnapToItem(int inputDeviceId, int source, int axis)133         public void onSnapToItem(int inputDeviceId, int source, int axis) {}
134 
135         @Override
onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart)136         public void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart) {}
137 
138         @Override
onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels)139         public void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels) {}
140     }
141 
142     /**
143      * An interface parallel to {@link ScrollFeedbackProvider}, to allow different compat
144      * implementations based on Build SDK version.
145      */
146     private interface ScrollFeedbackProviderImpl {
onSnapToItem(int inputDeviceId, int source, int axis)147         void onSnapToItem(int inputDeviceId, int source, int axis);
onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart)148         void onScrollLimit(int inputDeviceId, int source, int axis, boolean isStart);
onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels)149         void onScrollProgress(int inputDeviceId, int source, int axis, int deltaInPixels);
150     }
151 }
152