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