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 17package androidx.recyclerview.widget; 18 19import static androidx.recyclerview.widget.RecyclerView.VERTICAL; 20 21import static org.junit.Assert.assertFalse; 22import static org.junit.Assert.assertNotNull; 23import static org.junit.Assert.assertTrue; 24 25import android.app.Activity; 26import android.content.res.Resources; 27import android.graphics.Color; 28import android.graphics.drawable.StateListDrawable; 29import android.support.test.InstrumentationRegistry; 30import android.support.test.annotation.UiThreadTest; 31import android.support.test.filters.LargeTest; 32import android.support.test.runner.AndroidJUnit4; 33import android.view.LayoutInflater; 34import android.view.MotionEvent; 35import android.view.View; 36import android.view.ViewGroup; 37import android.view.ViewGroup.LayoutParams; 38import android.widget.TextView; 39 40import androidx.annotation.NonNull; 41import androidx.recyclerview.R; 42 43import org.junit.Test; 44import org.junit.runner.RunWith; 45 46import java.util.concurrent.CountDownLatch; 47import java.util.concurrent.TimeUnit; 48 49@LargeTest 50@RunWith(AndroidJUnit4.class) 51public class RecyclerViewFastScrollerTest extends BaseRecyclerViewInstrumentationTest { 52 private static final int FLAG_HORIZONTAL = 1; 53 private static final int FLAG_VERTICAL = 1 << 1; 54 private int mScrolledByY = -1000; 55 private int mScrolledByX = -1000; 56 private FastScroller mScroller; 57 private boolean mHide; 58 59 private void setContentView(final int layoutId) throws Throwable { 60 final Activity activity = mActivityRule.getActivity(); 61 mActivityRule.runOnUiThread(new Runnable() { 62 @Override 63 public void run() { 64 activity.setContentView(layoutId); 65 } 66 }); 67 } 68 69 @Test 70 public void xml_fastScrollEnabled_startsInvisibleAndAtTop() throws Throwable { 71 arrangeWithXml(); 72 73 assertTrue("Expected centerY to start == 0", mScroller.mVerticalThumbCenterY == 0); 74 assertFalse("Expected thumb to start invisible", mScroller.isVisible()); 75 } 76 77 @Test 78 public void scrollBy_displaysAndMovesFastScrollerThumb() throws Throwable { 79 arrangeWithXml(); 80 81 mActivityRule.runOnUiThread(new Runnable() { 82 @Override 83 public void run() { 84 mRecyclerView.scrollBy(0, 400); 85 } 86 }); 87 88 assertTrue("Expected centerY to be > 0" + mScroller.mVerticalThumbCenterY, 89 mScroller.mVerticalThumbCenterY > 0); 90 assertTrue("Expected thumb to be visible", mScroller.isVisible()); 91 } 92 93 @Test 94 public void ui_dragsThumb_scrollsRecyclerView() throws Throwable { 95 arrangeWithXml(); 96 97 // RecyclerView#scrollBy(int, int) used to cause the scroller thumb to show up. 98 mActivityRule.runOnUiThread(new Runnable() { 99 @Override 100 public void run() { 101 mRecyclerView.scrollBy(0, 1); 102 mRecyclerView.scrollBy(0, -1); 103 } 104 }); 105 int[] absoluteCoords = new int[2]; 106 mRecyclerView.getLocationOnScreen(absoluteCoords); 107 TouchUtils.drag(InstrumentationRegistry.getInstrumentation(), mRecyclerView.getWidth() - 10, 108 mRecyclerView.getWidth() - 10, mScroller.mVerticalThumbCenterY + absoluteCoords[1], 109 mRecyclerView.getHeight() + absoluteCoords[1], 100); 110 111 assertTrue("Expected dragging thumb to move recyclerView", 112 mRecyclerView.computeVerticalScrollOffset() > 0); 113 } 114 115 @Test 116 public void properCleanUp() throws Throwable { 117 mRecyclerView = new RecyclerView(getActivity()); 118 final Activity activity = mActivityRule.getActivity(); 119 final CountDownLatch latch = new CountDownLatch(1); 120 mActivityRule.runOnUiThread(new Runnable() { 121 @Override 122 public void run() { 123 activity.setContentView( 124 androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv); 125 mRecyclerView = (RecyclerView) activity.findViewById( 126 androidx.recyclerview.test.R.id.recycler_view); 127 LinearLayoutManager layout = new LinearLayoutManager(activity.getBaseContext()); 128 layout.setOrientation(VERTICAL); 129 mRecyclerView.setLayoutManager(layout); 130 mRecyclerView.setAdapter(new TestAdapter(50)); 131 Resources res = getActivity().getResources(); 132 mScroller = new FastScroller(mRecyclerView, (StateListDrawable) res.getDrawable( 133 androidx.recyclerview.test.R.drawable.fast_scroll_thumb_drawable), 134 res.getDrawable( 135 androidx.recyclerview.test.R.drawable 136 .fast_scroll_track_drawable), 137 (StateListDrawable) res.getDrawable( 138 androidx.recyclerview.test.R.drawable 139 .fast_scroll_thumb_drawable), 140 res.getDrawable( 141 androidx.recyclerview.test.R.drawable 142 .fast_scroll_track_drawable), 143 res.getDimensionPixelSize(R.dimen.fastscroll_default_thickness), 144 res.getDimensionPixelSize(R.dimen.fastscroll_minimum_range), 145 res.getDimensionPixelOffset(R.dimen.fastscroll_margin)) { 146 @Override 147 public void show() { 148 // Overriden to avoid animation calls in instrumentation thread 149 } 150 151 @Override 152 public void hide(int duration) { 153 latch.countDown(); 154 mHide = true; 155 } 156 }; 157 158 } 159 }); 160 waitForIdleScroll(mRecyclerView); 161 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 162 mActivityRule.runOnUiThread(new Runnable() { 163 @Override 164 public void run() { 165 mRecyclerView.scrollBy(0, 400); 166 mScroller.attachToRecyclerView(new RecyclerView(getActivity())); 167 } 168 }); 169 assertFalse(latch.await(2, TimeUnit.SECONDS)); 170 assertFalse(mHide); 171 } 172 173 @Test 174 public void inflationTest() throws Throwable { 175 setContentView(androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv); 176 getInstrumentation().waitForIdleSync(); 177 RecyclerView view = (RecyclerView) getActivity().findViewById( 178 androidx.recyclerview.test.R.id.recycler_view); 179 assertTrue(view.getItemDecorationCount() == 1); 180 assertTrue(view.getItemDecorationAt(0) instanceof FastScroller); 181 FastScroller scroller = (FastScroller) view.getItemDecorationAt(0); 182 assertNotNull(scroller.getHorizontalThumbDrawable()); 183 assertNotNull(scroller.getHorizontalTrackDrawable()); 184 assertNotNull(scroller.getVerticalThumbDrawable()); 185 assertNotNull(scroller.getVerticalTrackDrawable()); 186 } 187 188 @Test 189 public void removeFastScrollerSuccessful() throws Throwable { 190 setContentView(androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv); 191 getInstrumentation().waitForIdleSync(); 192 final RecyclerView view = (RecyclerView) getActivity().findViewById( 193 androidx.recyclerview.test.R.id.recycler_view); 194 assertTrue(view.getItemDecorationCount() == 1); 195 mActivityRule.runOnUiThread(new Runnable() { 196 @Override 197 public void run() { 198 view.removeItemDecorationAt(0); 199 assertTrue(view.getItemDecorationCount() == 0); 200 } 201 }); 202 } 203 204 @UiThreadTest 205 @Test 206 public void initWithBadDrawables() throws Throwable { 207 arrangeWithCode(); 208 209 Throwable exception = null; 210 try { 211 mRecyclerView.initFastScroller(null, null, null, null); 212 } catch (Throwable t) { 213 exception = t; 214 } 215 assertTrue(exception instanceof IllegalArgumentException); 216 } 217 218 @Test 219 public void verticalScrollUpdatesFastScrollThumb() throws Throwable { 220 scrollUpdatesFastScrollThumb(FLAG_VERTICAL); 221 } 222 223 @Test 224 public void horizontalScrollUpdatesFastScrollThumb() throws Throwable { 225 scrollUpdatesFastScrollThumb(FLAG_HORIZONTAL); 226 } 227 228 private void scrollUpdatesFastScrollThumb(int direction) throws Throwable { 229 arrangeWithCode(); 230 mScroller.updateScrollPosition(direction == FLAG_VERTICAL ? 0 : 250, 231 direction == FLAG_VERTICAL ? 250 : 0); 232 if (direction == FLAG_VERTICAL) { 233 assertTrue("Expected 250 for centerY, got " + mScroller.mVerticalThumbCenterY, 234 mScroller.mVerticalThumbCenterY == 250); 235 assertTrue("Expected 250 for thumb height, got " + mScroller.mVerticalThumbHeight, 236 mScroller.mVerticalThumbHeight == 250); 237 } else if (direction == FLAG_HORIZONTAL) { 238 assertTrue("Expected 250 for centerX, got " + mScroller.mHorizontalThumbCenterX, 239 mScroller.mHorizontalThumbCenterX == 250); 240 assertTrue("Expected 250 for thumb width, got " + mScroller.mHorizontalThumbWidth, 241 mScroller.mHorizontalThumbWidth == 250); 242 } 243 assertTrue(mScroller.isVisible()); 244 245 mScroller.updateScrollPosition(direction == FLAG_VERTICAL ? 0 : 42, 246 direction == FLAG_VERTICAL ? 42 : 0); 247 if (direction == FLAG_VERTICAL) { 248 assertTrue("Expected 146 for centerY, got " + mScroller.mVerticalThumbCenterY, 249 mScroller.mVerticalThumbCenterY == 146); 250 assertTrue("Expected 250 for thumb height, got " + mScroller.mVerticalThumbHeight, 251 mScroller.mVerticalThumbHeight == 250); 252 } else if (direction == FLAG_HORIZONTAL) { 253 assertTrue("Expected 146 for centerX, got " + mScroller.mHorizontalThumbCenterX, 254 mScroller.mHorizontalThumbCenterX == 146); 255 assertTrue("Expected 250 for thumb width, got " + mScroller.mHorizontalThumbWidth, 256 mScroller.mHorizontalThumbWidth == 250); 257 } 258 assertTrue(mScroller.isVisible()); 259 } 260 261 @Test 262 public void draggingDoesNotTriggerFastScrollIfNotInThumb() throws Throwable { 263 arrangeWithCode(); 264 mScroller.updateScrollPosition(0, 250); 265 final MotionEvent downEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_DOWN, 250, 250, 266 0); 267 assertFalse(mScroller.onInterceptTouchEvent(mRecyclerView, downEvent)); 268 final MotionEvent moveEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_MOVE, 250, 275, 269 0); 270 assertFalse(mScroller.onInterceptTouchEvent(mRecyclerView, moveEvent)); 271 } 272 273 @Test 274 public void verticalDraggingFastScrollThumbDoesActualScrolling() throws Throwable { 275 draggingFastScrollThumbDoesActualScrolling(FLAG_VERTICAL); 276 } 277 278 @Test 279 public void horizontalDraggingFastScrollThumbDoesActualScrolling() throws Throwable { 280 draggingFastScrollThumbDoesActualScrolling(FLAG_HORIZONTAL); 281 } 282 283 private void draggingFastScrollThumbDoesActualScrolling(int direction) throws Throwable { 284 arrangeWithCode(); 285 mScroller.updateScrollPosition(direction == FLAG_VERTICAL ? 0 : 250, 286 direction == FLAG_VERTICAL ? 250 : 0); 287 final MotionEvent downEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_DOWN, 288 direction == FLAG_VERTICAL ? 500 : 250, direction == FLAG_VERTICAL ? 250 : 500, 0); 289 assertTrue(mScroller.onInterceptTouchEvent(mRecyclerView, downEvent)); 290 assertTrue(mScroller.isDragging()); 291 final MotionEvent moveEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_MOVE, 292 direction == FLAG_VERTICAL ? 500 : 221, direction == FLAG_VERTICAL ? 221 : 500, 0); 293 mScroller.onTouchEvent(mRecyclerView, moveEvent); 294 if (direction == FLAG_VERTICAL) { 295 assertTrue("Expected to get -29, but got " + mScrolledByY, mScrolledByY == -29); 296 } else { 297 assertTrue("Expected to get -29, but got " + mScrolledByX, mScrolledByX == -29); 298 } 299 } 300 301 private void arrangeWithXml() throws Throwable { 302 303 final TestActivity activity = mActivityRule.getActivity(); 304 final TestedFrameLayout testedFrameLayout = activity.getContainer(); 305 306 RecyclerView recyclerView = (RecyclerView) LayoutInflater.from(activity).inflate( 307 androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv, 308 testedFrameLayout, 309 false); 310 311 LinearLayoutManager layout = new LinearLayoutManager(activity.getBaseContext()); 312 layout.setOrientation(VERTICAL); 313 recyclerView.setLayoutManager(layout); 314 315 recyclerView.setAdapter(new TestAdapter(50)); 316 317 mScroller = (FastScroller) recyclerView.getItemDecorationAt(0); 318 319 testedFrameLayout.expectLayouts(1); 320 testedFrameLayout.expectDraws(1); 321 setRecyclerView(recyclerView); 322 testedFrameLayout.waitForLayout(2); 323 testedFrameLayout.waitForDraw(2); 324 } 325 326 private void arrangeWithCode() throws Exception { 327 final int width = 500; 328 final int height = 500; 329 330 mRecyclerView = new RecyclerView(getActivity()) { 331 @Override 332 public int computeVerticalScrollRange() { 333 return 1000; 334 } 335 336 @Override 337 public int computeVerticalScrollExtent() { 338 return 500; 339 } 340 341 @Override 342 public int computeVerticalScrollOffset() { 343 return 250; 344 } 345 346 @Override 347 public int computeHorizontalScrollRange() { 348 return 1000; 349 } 350 351 @Override 352 public int computeHorizontalScrollExtent() { 353 return 500; 354 } 355 356 @Override 357 public int computeHorizontalScrollOffset() { 358 return 250; 359 } 360 361 @Override 362 public void scrollBy(int x, int y) { 363 mScrolledByY = y; 364 mScrolledByX = x; 365 } 366 }; 367 mRecyclerView.setAdapter(new TestAdapter(50)); 368 mRecyclerView.measure( 369 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), 370 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); 371 mRecyclerView.layout(0, 0, width, height); 372 373 Resources res = getActivity().getResources(); 374 mScroller = new FastScroller(mRecyclerView, (StateListDrawable) res.getDrawable( 375 androidx.recyclerview.test.R.drawable.fast_scroll_thumb_drawable), 376 res.getDrawable( 377 androidx.recyclerview.test.R.drawable.fast_scroll_track_drawable), 378 (StateListDrawable) res.getDrawable( 379 androidx.recyclerview.test.R.drawable.fast_scroll_thumb_drawable), 380 res.getDrawable( 381 androidx.recyclerview.test.R.drawable.fast_scroll_track_drawable), 382 res.getDimensionPixelSize(R.dimen.fastscroll_default_thickness), 383 res.getDimensionPixelSize(R.dimen.fastscroll_minimum_range), 384 res.getDimensionPixelOffset(R.dimen.fastscroll_margin)) { 385 @Override 386 public void show() { 387 // Overriden to avoid animation calls in instrumentation thread 388 } 389 390 @Override 391 public void hide(int duration) { 392 mHide = true; 393 } 394 }; 395 mRecyclerView.mEnableFastScroller = true; 396 397 // Draw it once so height/width gets updated 398 mScroller.onDrawOver(null, mRecyclerView, null); 399 } 400 401 private static class TestAdapter extends RecyclerView.Adapter { 402 private int mItemCount; 403 404 public static class ViewHolder extends RecyclerView.ViewHolder { 405 public TextView mTextView; 406 407 ViewHolder(TextView v) { 408 super(v); 409 mTextView = v; 410 } 411 412 @Override 413 public String toString() { 414 return super.toString() + " '" + mTextView.getText(); 415 } 416 } 417 418 TestAdapter(int itemCount) { 419 mItemCount = itemCount; 420 } 421 422 @Override 423 public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 424 int viewType) { 425 final ViewHolder h = new ViewHolder(new TextView(parent.getContext())); 426 h.mTextView.setMinimumHeight(128); 427 h.mTextView.setPadding(20, 0, 20, 0); 428 h.mTextView.setFocusable(true); 429 h.mTextView.setBackgroundColor(Color.BLUE); 430 RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams( 431 LayoutParams.MATCH_PARENT, 432 ViewGroup.LayoutParams.WRAP_CONTENT); 433 lp.leftMargin = 10; 434 lp.rightMargin = 5; 435 lp.topMargin = 20; 436 lp.bottomMargin = 15; 437 h.mTextView.setLayoutParams(lp); 438 return h; 439 } 440 441 @Override 442 public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 443 holder.itemView.setTag("pos " + position); 444 } 445 446 @Override 447 public int getItemCount() { 448 return mItemCount; 449 } 450 } 451} 452