BaseLinearLayoutManagerTest.java revision 4143554adb9b31b700b6876a251a64419e6111e2
1/* 2 * Copyright (C) 2015 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 */ 16package android.support.v7.widget; 17 18import android.content.Context; 19import android.graphics.Rect; 20import android.util.Log; 21import android.view.View; 22import android.view.ViewGroup; 23 24import java.lang.reflect.Field; 25import java.util.ArrayList; 26import java.util.LinkedHashMap; 27import java.util.List; 28import java.util.Map; 29import java.util.concurrent.CountDownLatch; 30import java.util.concurrent.TimeUnit; 31 32import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; 33import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 34import static android.view.ViewGroup.LayoutParams.FILL_PARENT; 35import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 36import static org.junit.Assert.*; 37 38public class BaseLinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 39 40 protected static final boolean DEBUG = false; 41 protected static final String TAG = "LinearLayoutManagerTest"; 42 43 protected static List<Config> createBaseVariations() { 44 List<Config> variations = new ArrayList<>(); 45 for (int orientation : new int[]{VERTICAL, HORIZONTAL}) { 46 for (boolean reverseLayout : new boolean[]{false, true}) { 47 for (boolean stackFromBottom : new boolean[]{false, true}) { 48 for (boolean wrap : new boolean[]{false, true}) { 49 variations.add( 50 new Config(orientation, reverseLayout, stackFromBottom).wrap(wrap)); 51 } 52 53 } 54 } 55 } 56 return variations; 57 } 58 59 WrappedLinearLayoutManager mLayoutManager; 60 TestAdapter mTestAdapter; 61 62 protected static List<Config> addConfigVariation(List<Config> base, String fieldName, 63 Object... variations) 64 throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException { 65 List<Config> newConfigs = new ArrayList<Config>(); 66 Field field = Config.class.getDeclaredField(fieldName); 67 for (Config config : base) { 68 for (Object variation : variations) { 69 Config newConfig = (Config) config.clone(); 70 field.set(newConfig, variation); 71 newConfigs.add(newConfig); 72 } 73 } 74 return newConfigs; 75 } 76 77 void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable { 78 mRecyclerView = inflateWrappedRV(); 79 80 mRecyclerView.setHasFixedSize(true); 81 mTestAdapter = config.mTestAdapter == null ? new TestAdapter(config.mItemCount) 82 : config.mTestAdapter; 83 mRecyclerView.setAdapter(mTestAdapter); 84 mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation, 85 config.mReverseLayout); 86 mLayoutManager.setStackFromEnd(config.mStackFromEnd); 87 mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach); 88 mRecyclerView.setLayoutManager(mLayoutManager); 89 if (config.mWrap) { 90 mRecyclerView.setLayoutParams( 91 new ViewGroup.LayoutParams( 92 config.mOrientation == HORIZONTAL ? WRAP_CONTENT : FILL_PARENT, 93 config.mOrientation == VERTICAL ? WRAP_CONTENT : FILL_PARENT 94 ) 95 ); 96 } 97 if (waitForFirstLayout) { 98 waitForFirstLayout(); 99 } 100 } 101 102 public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset) 103 throws Throwable { 104 setupByConfig(new Config(VERTICAL, false, false), true); 105 106 mLayoutManager.mOnLayoutListener = new OnLayoutListener() { 107 @Override 108 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 109 if (state.isPreLayout()) { 110 assertEquals("pending scroll position should still be pending", 111 scrollPosition, mLayoutManager.mPendingScrollPosition); 112 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 113 assertEquals("pending scroll position offset should still be pending", 114 scrollOffset, mLayoutManager.mPendingScrollPositionOffset); 115 } 116 } else { 117 RecyclerView.ViewHolder vh = 118 mRecyclerView.findViewHolderForLayoutPosition(scrollPosition); 119 assertNotNull("scroll to position should work", vh); 120 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 121 assertEquals("scroll offset should be applied properly", 122 mLayoutManager.getPaddingTop() + scrollOffset + 123 ((RecyclerView.LayoutParams) vh.itemView 124 .getLayoutParams()).topMargin, 125 mLayoutManager.getDecoratedTop(vh.itemView)); 126 } 127 } 128 } 129 }; 130 mLayoutManager.expectLayouts(2); 131 runTestOnUiThread(new Runnable() { 132 @Override 133 public void run() { 134 try { 135 mTestAdapter.addAndNotify(0, 1); 136 if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) { 137 mLayoutManager.scrollToPosition(scrollPosition); 138 } else { 139 mLayoutManager.scrollToPositionWithOffset(scrollPosition, 140 scrollOffset); 141 } 142 143 } catch (Throwable throwable) { 144 throwable.printStackTrace(); 145 } 146 147 } 148 }); 149 mLayoutManager.waitForLayout(2); 150 checkForMainThreadException(); 151 } 152 153 protected void waitForFirstLayout() throws Throwable { 154 mLayoutManager.expectLayouts(1); 155 setRecyclerView(mRecyclerView); 156 mLayoutManager.waitForLayout(2); 157 } 158 159 void scrollToPositionWithOffset(final int position, final int offset) throws Throwable { 160 runTestOnUiThread(new Runnable() { 161 @Override 162 public void run() { 163 mLayoutManager.scrollToPositionWithOffset(position, offset); 164 } 165 }); 166 } 167 168 public void assertRectSetsNotEqual(String message, Map<Item, Rect> before, 169 Map<Item, Rect> after, boolean strictItemEquality) { 170 Throwable throwable = null; 171 try { 172 assertRectSetsEqual("NOT " + message, before, after, strictItemEquality); 173 } catch (Throwable t) { 174 throwable = t; 175 } 176 assertNotNull(message + "\ntwo layout should be different", throwable); 177 } 178 179 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) { 180 assertRectSetsEqual(message, before, after, true); 181 } 182 183 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, 184 boolean strictItemEquality) { 185 StringBuilder sb = new StringBuilder(); 186 sb.append("checking rectangle equality.\n"); 187 sb.append("before:\n"); 188 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 189 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 190 } 191 sb.append("after:\n"); 192 for (Map.Entry<Item, Rect> entry : after.entrySet()) { 193 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 194 } 195 message = message + "\n" + sb.toString(); 196 assertEquals(message + ":\nitem counts should be equal", before.size() 197 , after.size()); 198 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 199 final Item beforeItem = entry.getKey(); 200 Rect afterRect = null; 201 if (strictItemEquality) { 202 afterRect = after.get(beforeItem); 203 assertNotNull(message + ":\nSame item should be visible after simple re-layout", 204 afterRect); 205 } else { 206 for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) { 207 final Item afterItem = afterEntry.getKey(); 208 if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) { 209 afterRect = afterEntry.getValue(); 210 break; 211 } 212 } 213 assertNotNull(message + ":\nItem with same adapter index should be visible " + 214 "after simple re-layout", 215 afterRect); 216 } 217 assertEquals(message + ":\nItem should be laid out at the same coordinates", 218 entry.getValue(), afterRect); 219 } 220 } 221 222 static class VisibleChildren { 223 224 int firstVisiblePosition = RecyclerView.NO_POSITION; 225 226 int firstFullyVisiblePosition = RecyclerView.NO_POSITION; 227 228 int lastVisiblePosition = RecyclerView.NO_POSITION; 229 230 int lastFullyVisiblePosition = RecyclerView.NO_POSITION; 231 232 @Override 233 public String toString() { 234 return "VisibleChildren{" + 235 "firstVisiblePosition=" + firstVisiblePosition + 236 ", firstFullyVisiblePosition=" + firstFullyVisiblePosition + 237 ", lastVisiblePosition=" + lastVisiblePosition + 238 ", lastFullyVisiblePosition=" + lastFullyVisiblePosition + 239 '}'; 240 } 241 } 242 243 static class OnLayoutListener { 244 245 void before(RecyclerView.Recycler recycler, RecyclerView.State state) { 246 } 247 248 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 249 } 250 } 251 252 static class Config implements Cloneable { 253 254 static final int DEFAULT_ITEM_COUNT = 100; 255 256 boolean mStackFromEnd; 257 258 int mOrientation = VERTICAL; 259 260 boolean mReverseLayout = false; 261 262 boolean mRecycleChildrenOnDetach = false; 263 264 int mItemCount = DEFAULT_ITEM_COUNT; 265 266 boolean mWrap = false; 267 268 TestAdapter mTestAdapter; 269 270 Config(int orientation, boolean reverseLayout, boolean stackFromEnd) { 271 mOrientation = orientation; 272 mReverseLayout = reverseLayout; 273 mStackFromEnd = stackFromEnd; 274 } 275 276 public Config() { 277 278 } 279 280 Config adapter(TestAdapter adapter) { 281 mTestAdapter = adapter; 282 return this; 283 } 284 285 Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) { 286 mRecycleChildrenOnDetach = recycleChildrenOnDetach; 287 return this; 288 } 289 290 Config orientation(int orientation) { 291 mOrientation = orientation; 292 return this; 293 } 294 295 Config stackFromBottom(boolean stackFromBottom) { 296 mStackFromEnd = stackFromBottom; 297 return this; 298 } 299 300 Config reverseLayout(boolean reverseLayout) { 301 mReverseLayout = reverseLayout; 302 return this; 303 } 304 305 public Config itemCount(int itemCount) { 306 mItemCount = itemCount; 307 return this; 308 } 309 310 // required by convention 311 @Override 312 public Object clone() throws CloneNotSupportedException { 313 return super.clone(); 314 } 315 316 @Override 317 public String toString() { 318 return "Config{" + 319 "mStackFromEnd=" + mStackFromEnd + 320 ", mOrientation=" + mOrientation + 321 ", mReverseLayout=" + mReverseLayout + 322 ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach + 323 ", mItemCount=" + mItemCount + 324 ", wrap=" + mWrap + 325 '}'; 326 } 327 328 public Config wrap(boolean wrap) { 329 mWrap = wrap; 330 return this; 331 } 332 } 333 334 class WrappedLinearLayoutManager extends LinearLayoutManager { 335 336 CountDownLatch layoutLatch; 337 338 OrientationHelper mSecondaryOrientation; 339 340 OnLayoutListener mOnLayoutListener; 341 342 public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 343 super(context, orientation, reverseLayout); 344 } 345 346 public void expectLayouts(int count) { 347 layoutLatch = new CountDownLatch(count); 348 } 349 350 public void waitForLayout(long timeout) throws InterruptedException { 351 waitForLayout(timeout, TimeUnit.SECONDS); 352 } 353 354 @Override 355 public void setOrientation(int orientation) { 356 super.setOrientation(orientation); 357 mSecondaryOrientation = null; 358 } 359 360 @Override 361 public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) { 362 if (DEBUG) { 363 Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child)); 364 } 365 super.removeAndRecycleView(child, recycler); 366 } 367 368 @Override 369 public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) { 370 if (DEBUG) { 371 Log.d(TAG, 372 "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index))); 373 } 374 super.removeAndRecycleViewAt(index, recycler); 375 } 376 377 @Override 378 void ensureLayoutState() { 379 super.ensureLayoutState(); 380 if (mSecondaryOrientation == null) { 381 mSecondaryOrientation = OrientationHelper.createOrientationHelper(this, 382 1 - getOrientation()); 383 } 384 } 385 386 private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException { 387 layoutLatch.await(timeout * (DEBUG ? 100 : 1), timeUnit); 388 assertEquals("all expected layouts should be executed at the expected time", 389 0, layoutLatch.getCount()); 390 getInstrumentation().waitForIdleSync(); 391 } 392 393 @Override 394 LayoutState createLayoutState() { 395 return new LayoutState() { 396 @Override 397 View next(RecyclerView.Recycler recycler) { 398 final boolean hadMore = hasMore(mRecyclerView.mState); 399 final int position = mCurrentPosition; 400 View next = super.next(recycler); 401 assertEquals("if has more, should return a view", hadMore, next != null); 402 assertEquals("position of the returned view must match current position", 403 position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition()); 404 return next; 405 } 406 }; 407 } 408 409 public String getBoundsLog() { 410 StringBuilder sb = new StringBuilder(); 411 sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding()) 412 .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding()); 413 sb.append("\nchildren bounds\n"); 414 final int childCount = getChildCount(); 415 for (int i = 0; i < childCount; i++) { 416 View child = getChildAt(i); 417 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child)) 418 .append("[").append("start:").append( 419 mOrientationHelper.getDecoratedStart(child)).append(", end:") 420 .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n"); 421 } 422 return sb.toString(); 423 } 424 425 public void waitForAnimationsToEnd(int timeoutInSeconds) throws InterruptedException { 426 RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); 427 if (itemAnimator == null) { 428 return; 429 } 430 final CountDownLatch latch = new CountDownLatch(1); 431 final boolean running = itemAnimator.isRunning( 432 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 433 @Override 434 public void onAnimationsFinished() { 435 latch.countDown(); 436 } 437 } 438 ); 439 if (running) { 440 latch.await(timeoutInSeconds, TimeUnit.SECONDS); 441 } 442 } 443 444 public VisibleChildren traverseAndFindVisibleChildren() { 445 int childCount = getChildCount(); 446 final VisibleChildren visibleChildren = new VisibleChildren(); 447 final int start = mOrientationHelper.getStartAfterPadding(); 448 final int end = mOrientationHelper.getEndAfterPadding(); 449 for (int i = 0; i < childCount; i++) { 450 View child = getChildAt(i); 451 final int childStart = mOrientationHelper.getDecoratedStart(child); 452 final int childEnd = mOrientationHelper.getDecoratedEnd(child); 453 final boolean fullyVisible = childStart >= start && childEnd <= end; 454 final boolean hidden = childEnd <= start || childStart >= end; 455 if (hidden) { 456 continue; 457 } 458 final int position = getPosition(child); 459 if (fullyVisible) { 460 if (position < visibleChildren.firstFullyVisiblePosition || 461 visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) { 462 visibleChildren.firstFullyVisiblePosition = position; 463 } 464 465 if (position > visibleChildren.lastFullyVisiblePosition) { 466 visibleChildren.lastFullyVisiblePosition = position; 467 } 468 } 469 470 if (position < visibleChildren.firstVisiblePosition || 471 visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) { 472 visibleChildren.firstVisiblePosition = position; 473 } 474 475 if (position > visibleChildren.lastVisiblePosition) { 476 visibleChildren.lastVisiblePosition = position; 477 } 478 479 } 480 return visibleChildren; 481 } 482 483 Rect getViewBounds(View view) { 484 if (getOrientation() == HORIZONTAL) { 485 return new Rect( 486 mOrientationHelper.getDecoratedStart(view), 487 mSecondaryOrientation.getDecoratedStart(view), 488 mOrientationHelper.getDecoratedEnd(view), 489 mSecondaryOrientation.getDecoratedEnd(view)); 490 } else { 491 return new Rect( 492 mSecondaryOrientation.getDecoratedStart(view), 493 mOrientationHelper.getDecoratedStart(view), 494 mSecondaryOrientation.getDecoratedEnd(view), 495 mOrientationHelper.getDecoratedEnd(view)); 496 } 497 498 } 499 500 Map<Item, Rect> collectChildCoordinates() throws Throwable { 501 final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>(); 502 runTestOnUiThread(new Runnable() { 503 @Override 504 public void run() { 505 final int childCount = getChildCount(); 506 Rect layoutBounds = new Rect(0, 0, 507 mLayoutManager.getWidth(), mLayoutManager.getHeight()); 508 for (int i = 0; i < childCount; i++) { 509 View child = getChildAt(i); 510 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child 511 .getLayoutParams(); 512 TestViewHolder vh = (TestViewHolder) lp.mViewHolder; 513 Rect childBounds = getViewBounds(child); 514 if (new Rect(childBounds).intersect(layoutBounds)) { 515 items.put(vh.mBoundItem, childBounds); 516 } 517 } 518 } 519 }); 520 return items; 521 } 522 523 @Override 524 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 525 try { 526 if (mOnLayoutListener != null) { 527 mOnLayoutListener.before(recycler, state); 528 } 529 super.onLayoutChildren(recycler, state); 530 if (mOnLayoutListener != null) { 531 mOnLayoutListener.after(recycler, state); 532 } 533 } catch (Throwable t) { 534 postExceptionToInstrumentation(t); 535 } 536 layoutLatch.countDown(); 537 } 538 539 540 } 541} 542