1 /*
2  * Copyright 2018 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.recyclerview.widget;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotEquals;
21 import static org.junit.Assert.assertNull;
22 
23 import android.content.Context;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.view.ViewConfiguration;
27 import android.view.ViewGroup;
28 import android.view.animation.Interpolator;
29 import android.widget.OverScroller;
30 import android.widget.TextView;
31 
32 import androidx.annotation.Px;
33 import androidx.core.view.DifferentialMotionFlingController;
34 import androidx.core.view.DifferentialMotionFlingTarget;
35 import androidx.core.view.InputDeviceCompat;
36 import androidx.core.view.ViewConfigurationCompat;
37 import androidx.test.core.app.ApplicationProvider;
38 import androidx.test.ext.junit.runners.AndroidJUnit4;
39 import androidx.test.filters.SmallTest;
40 
41 import org.jspecify.annotations.NonNull;
42 import org.jspecify.annotations.Nullable;
43 import org.junit.Before;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 
47 @SmallTest
48 @RunWith(AndroidJUnit4.class)
49 public class RecyclerViewOnGenericMotionEventTest {
50 
51     TestRecyclerView mRecyclerView;
52     TestDifferentialMotionFlingController mFlingController;
53 
54     @Before
setUp()55     public void setUp() throws Exception {
56         mRecyclerView = new TestRecyclerView(getContext());
57         mFlingController = createDummyFlingController();
58         mRecyclerView.mDifferentialMotionFlingController = mFlingController;
59     }
60 
getContext()61     private Context getContext() {
62         return ApplicationProvider.getApplicationContext();
63     }
64 
layout()65     private void layout() {
66         mRecyclerView.layout(0, 0, 320, 320);
67     }
68 
69     @Test
rotaryEncoderVerticalScroll_nonLowResDevice()70     public void rotaryEncoderVerticalScroll_nonLowResDevice() {
71         mRecyclerView.mLowResRotaryEncoderFeature = false;
72         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
73         mRecyclerView.setLayoutManager(layoutManager);
74         layout();
75 
76         TouchUtils.scrollView(
77                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
78 
79         assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()),
80                 /* assertSmoothScroll= */ false);
81         assertEquals(MotionEvent.AXIS_SCROLL, mFlingController.mLastAxis);
82         assertEquals(mRecyclerView.mLastGenericMotionEvent, mFlingController.mLastMotionEvent);
83     }
84 
85     @Test
rotaryEncoderHorizontalScroll_nonLowResDevice()86     public void rotaryEncoderHorizontalScroll_nonLowResDevice() {
87         mRecyclerView.mLowResRotaryEncoderFeature = false;
88         // The encoder is one-dimensional, and can only scroll horizontally if vertical scrolling
89         // is not enabled.
90         MockLayoutManager layoutManager = new MockLayoutManager(true, false);
91         mRecyclerView.setLayoutManager(layoutManager);
92         layout();
93 
94         TouchUtils.scrollView(
95                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
96 
97         assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0,
98                 /* assertSmoothScroll= */ false);
99         assertEquals(MotionEvent.AXIS_SCROLL, mFlingController.mLastAxis);
100         assertEquals(mRecyclerView.mLastGenericMotionEvent, mFlingController.mLastMotionEvent);
101     }
102 
103     @Test
rotaryEncoderVerticalScroll_lowResDevice()104     public void rotaryEncoderVerticalScroll_lowResDevice() {
105         mRecyclerView.mLowResRotaryEncoderFeature = true;
106         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
107         mRecyclerView.setLayoutManager(layoutManager);
108         layout();
109         TouchUtils.scrollView(
110                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
111         assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()),
112                 /* assertSmoothScroll= */ true);
113         assertNull(mFlingController.mLastMotionEvent);
114     }
115 
116     @Test
rotaryEncoderVerticalScroll_lowResDevice_backToBackScrollEvents()117     public void rotaryEncoderVerticalScroll_lowResDevice_backToBackScrollEvents() {
118         mRecyclerView.mLowResRotaryEncoderFeature = true;
119         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
120         mRecyclerView.setLayoutManager(layoutManager);
121         layout();
122 
123         TouchUtils.scrollView(
124                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
125         OverScroller overScroller = mRecyclerView.mViewFlinger.mOverScroller;
126         int remainingScroll = overScroller.getFinalY() - overScroller.getCurrY();
127         TouchUtils.scrollView(
128                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
129 
130         // The expected total scroll will be the amount corresponding to each of the two scroll
131         // events, plus the amount of scroll remaining from the first scroll by the time the
132         // second scroll was initiated.
133         assertTotalScroll(0, (int) (-4f * getScaledVerticalScrollFactor()) + remainingScroll,
134                 /* assertSmoothScroll= */ true);
135     }
136 
137     @Test
rotaryEncoderHorizontalScroll_lowResDevice()138     public void rotaryEncoderHorizontalScroll_lowResDevice() {
139         mRecyclerView.mLowResRotaryEncoderFeature = true;
140         // The encoder is one-dimensional, and can only scroll horizontally if vertical scrolling
141         // is not enabled.
142         MockLayoutManager layoutManager = new MockLayoutManager(true, false);
143         mRecyclerView.setLayoutManager(layoutManager);
144         layout();
145         TouchUtils.scrollView(
146                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
147         assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0,
148                 /* assertSmoothScroll= */ true);
149         assertNull(mFlingController.mLastMotionEvent);
150     }
151 
152     @Test
rotaryEncoderHorizontalScroll_lowResDevice_backToBackScrollEvents()153     public void rotaryEncoderHorizontalScroll_lowResDevice_backToBackScrollEvents() {
154         mRecyclerView.mLowResRotaryEncoderFeature = true;
155         MockLayoutManager layoutManager = new MockLayoutManager(true, false);
156         mRecyclerView.setLayoutManager(layoutManager);
157         layout();
158 
159         TouchUtils.scrollView(
160                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
161         OverScroller overScroller = mRecyclerView.mViewFlinger.mOverScroller;
162         int remainingScroll = overScroller.getFinalX() - overScroller.getCurrX();
163         TouchUtils.scrollView(
164                 MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
165 
166         // The expected total scroll will be the amount corresponding to each of the two scroll
167         // events, plus the amount of scroll remaining from the first scroll by the time the
168         // second scroll was initiated.
169         assertTotalScroll((int) (4f * getScaledVerticalScrollFactor()) + remainingScroll, 0,
170                 /* assertSmoothScroll= */ true);
171     }
172 
173     @Test
pointerVerticalScroll()174     public void pointerVerticalScroll() {
175         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
176         mRecyclerView.setLayoutManager(layoutManager);
177         layout();
178         TouchUtils.scrollView(
179                 MotionEvent.AXIS_VSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
180         assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()));
181     }
182 
183     @Test
pointerHorizontalScroll()184     public void pointerHorizontalScroll() {
185         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
186         mRecyclerView.setLayoutManager(layoutManager);
187         layout();
188         TouchUtils.scrollView(
189                 MotionEvent.AXIS_HSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
190         assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0);
191     }
192 
193     @Test
nonZeroScaledVerticalScrollFactor()194     public void nonZeroScaledVerticalScrollFactor() {
195         assertNotEquals(0, getScaledVerticalScrollFactor());
196     }
197 
198     @Test
nonZeroScaledHorizontalScrollFactor()199     public void nonZeroScaledHorizontalScrollFactor() {
200         assertNotEquals(0, getScaledHorizontalScrollFactor());
201     }
202 
assertTotalScroll(int x, int y)203     private void assertTotalScroll(int x, int y) {
204         assertTotalScroll(x, y, /* smoothScroll= */ false);
205     }
206 
assertTotalScroll(int x, int y, boolean assertSmoothScroll)207     private void assertTotalScroll(int x, int y, boolean assertSmoothScroll) {
208         if (assertSmoothScroll) {
209             assertEquals("x total smooth scroll", x, mRecyclerView.mTotalSmoothX);
210             assertEquals("y total smooth scroll", y, mRecyclerView.mTotalSmoothY);
211         } else {
212             assertEquals("x total scroll", x, mRecyclerView.mTotalX);
213             assertEquals("y total scroll", y, mRecyclerView.mTotalY);
214         }
215     }
216 
obtainScrollMotionEvent(int axis, int axisValue, int inputDevice)217     private static MotionEvent obtainScrollMotionEvent(int axis, int axisValue, int inputDevice) {
218         MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
219         MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
220         coords.setAxisValue(axis, axisValue);
221         MotionEvent.PointerCoords[] pointerCoords = {coords};
222         float xPrecision = 1;
223         float yPrecision = 1;
224         int deviceId = 0;
225         int edgeFlags = 0;
226         int flags = 0;
227         return MotionEvent.obtain(0, System.currentTimeMillis(), MotionEvent.ACTION_SCROLL,
228                 1, pointerProperties, pointerCoords, 0, 0, xPrecision, yPrecision, deviceId,
229                 edgeFlags, inputDevice, flags);
230     }
231 
getScaledVerticalScrollFactor()232     private float getScaledVerticalScrollFactor() {
233         return ViewConfigurationCompat.getScaledVerticalScrollFactor(
234                 ViewConfiguration.get(getContext()), getContext());
235     }
236 
getScaledHorizontalScrollFactor()237     private float getScaledHorizontalScrollFactor() {
238         return ViewConfigurationCompat.getScaledHorizontalScrollFactor(
239                 ViewConfiguration.get(getContext()), getContext());
240     }
241 
242     static class MockLayoutManager extends RecyclerView.LayoutManager {
243 
244         private final boolean mCanScrollHorizontally;
245 
246         private final boolean mCanScrollVertically;
247 
MockLayoutManager(boolean canScrollHorizontally, boolean canScrollVertically)248         MockLayoutManager(boolean canScrollHorizontally, boolean canScrollVertically) {
249             mCanScrollHorizontally = canScrollHorizontally;
250             mCanScrollVertically = canScrollVertically;
251         }
252 
253         @Override
generateDefaultLayoutParams()254         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
255             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
256                     ViewGroup.LayoutParams.WRAP_CONTENT);
257         }
258 
259         @Override
scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)260         public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
261                 RecyclerView.State state) {
262             return dx;
263         }
264 
265         @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)266         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
267                 RecyclerView.State state) {
268             return dy;
269         }
270 
271         @Override
canScrollHorizontally()272         public boolean canScrollHorizontally() {
273             return mCanScrollHorizontally;
274         }
275 
276         @Override
canScrollVertically()277         public boolean canScrollVertically() {
278             return mCanScrollVertically;
279         }
280     }
281 
282     static class MockAdapter extends RecyclerView.Adapter {
283 
284         private int mCount = 0;
285 
MockAdapter(int count)286         MockAdapter(int count) {
287             this.mCount = count;
288         }
289 
290         @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)291         public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
292             return new MockViewHolder(new TextView(parent.getContext()));
293         }
294 
295         @Override
onBindViewHolder(RecyclerView.@onNull ViewHolder holder, int position)296         public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) {
297 
298         }
299 
300         @Override
getItemCount()301         public int getItemCount() {
302             return mCount;
303         }
304     }
305 
306     static class MockViewHolder extends RecyclerView.ViewHolder {
MockViewHolder(View itemView)307         MockViewHolder(View itemView) {
308             super(itemView);
309         }
310     }
311 
312     private static class TestRecyclerView extends RecyclerView {
313         int mTotalX = 0;
314         int mTotalY = 0;
315 
316         int mTotalSmoothX = 0;
317         int mTotalSmoothY = 0;
318 
319         MotionEvent mLastGenericMotionEvent;
320 
TestRecyclerView(Context context)321         TestRecyclerView(Context context) {
322             super(context);
323         }
324 
325         @Override
onGenericMotionEvent(MotionEvent ev)326         public boolean onGenericMotionEvent(MotionEvent ev) {
327             mLastGenericMotionEvent = ev;
328             return super.onGenericMotionEvent(ev);
329         }
330 
scrollByInternal( int x, int y, int horizontalAxis, int verticalAxis, @Nullable MotionEvent ev, int type)331         boolean scrollByInternal(
332                 int x,
333                 int y,
334                 int horizontalAxis,
335                 int verticalAxis,
336                 @Nullable MotionEvent ev,
337                 int type) {
338             mTotalX += x;
339             mTotalY += y;
340             return super.scrollByInternal(x, y, horizontalAxis, verticalAxis, ev, type);
341         }
342 
smoothScrollBy(@x int dx, @Px int dy, @Nullable Interpolator interpolator, int duration, boolean withNestedScrolling)343         void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator,
344                 int duration, boolean withNestedScrolling) {
345             mTotalSmoothX += dx;
346             mTotalSmoothY += dy;
347             super.smoothScrollBy(dx, dy, interpolator, duration, withNestedScrolling);
348         }
349     }
350 
createDummyFlingController()351     private TestDifferentialMotionFlingController createDummyFlingController() {
352         return new TestDifferentialMotionFlingController(
353                 mRecyclerView.getContext(),
354                 new DifferentialMotionFlingTarget() {
355                     @Override
356                     public boolean startDifferentialMotionFling(float velocity) {
357                         return false;
358                     }
359 
360                     @Override
361                     public void stopDifferentialMotionFling() {}
362 
363                     @Override
364                     public float getScaledScrollFactor() {
365                         return 0;
366                     }
367                 });
368     }
369 
370     private static class TestDifferentialMotionFlingController extends
371             DifferentialMotionFlingController {
372         MotionEvent mLastMotionEvent;
373         int mLastAxis;
374 
375         TestDifferentialMotionFlingController(Context context,
376                 DifferentialMotionFlingTarget target) {
377             super(context, target);
378         }
379 
380         @Override
381         public void onMotionEvent(MotionEvent event, int axis) {
382             mLastMotionEvent = event;
383             mLastAxis = axis;
384         }
385     }
386 
387 }
388