BaseRecyclerViewAnimationsTest.java revision 121ba9616e5bed44d2490f1744f7b6a9d3e79866
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 18 19import android.content.Context; 20import android.graphics.Canvas; 21import android.util.AttributeSet; 22import android.util.Log; 23import android.view.View; 24 25import java.util.ArrayList; 26import java.util.HashSet; 27import java.util.List; 28import java.util.Set; 29import java.util.concurrent.CountDownLatch; 30import java.util.concurrent.TimeUnit; 31 32/** 33 * Base class for animation related tests. 34 */ 35public class BaseRecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest { 36 37 protected static final boolean DEBUG = false; 38 39 protected static final String TAG = "RecyclerViewAnimationsTest"; 40 41 AnimationLayoutManager mLayoutManager; 42 43 TestAdapter mTestAdapter; 44 45 public BaseRecyclerViewAnimationsTest() { 46 super(DEBUG); 47 } 48 49 @Override 50 protected void setUp() throws Exception { 51 super.setUp(); 52 } 53 54 RecyclerView setupBasic(int itemCount) throws Throwable { 55 return setupBasic(itemCount, 0, itemCount); 56 } 57 58 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount) 59 throws Throwable { 60 return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null); 61 } 62 63 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount, 64 TestAdapter testAdapter) 65 throws Throwable { 66 final TestRecyclerView recyclerView = new TestRecyclerView(getActivity()); 67 recyclerView.setHasFixedSize(true); 68 if (testAdapter == null) { 69 mTestAdapter = new TestAdapter(itemCount); 70 } else { 71 mTestAdapter = testAdapter; 72 } 73 recyclerView.setAdapter(mTestAdapter); 74 recyclerView.setItemAnimator(createItemAnimator()); 75 mLayoutManager = new AnimationLayoutManager(); 76 recyclerView.setLayoutManager(mLayoutManager); 77 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex; 78 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount; 79 80 mLayoutManager.expectLayouts(1); 81 recyclerView.expectDraw(1); 82 setRecyclerView(recyclerView); 83 mLayoutManager.waitForLayout(2); 84 recyclerView.waitForDraw(1); 85 mLayoutManager.mOnLayoutCallbacks.reset(); 86 getInstrumentation().waitForIdleSync(); 87 assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount()); 88 assertEquals("all expected children should be laid out", firstLayoutItemCount, 89 mLayoutManager.getChildCount()); 90 return recyclerView; 91 } 92 93 protected RecyclerView.ItemAnimator createItemAnimator() { 94 return new DefaultItemAnimator(); 95 } 96 97 public TestRecyclerView getTestRecyclerView() { 98 return (TestRecyclerView) mRecyclerView; 99 } 100 101 class AnimationLayoutManager extends TestLayoutManager { 102 103 protected int mTotalLayoutCount = 0; 104 private String log; 105 106 OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() { 107 }; 108 109 110 111 @Override 112 public boolean supportsPredictiveItemAnimations() { 113 return true; 114 } 115 116 public String getLog() { 117 return log; 118 } 119 120 private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) { 121 StringBuilder builder = new StringBuilder(); 122 builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done); 123 builder.append("\nViewHolders:\n"); 124 for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) { 125 builder.append(vh).append("\n"); 126 } 127 builder.append("scrap:\n"); 128 for (RecyclerView.ViewHolder vh : recycler.getScrapList()) { 129 builder.append(vh).append("\n"); 130 } 131 132 if (state.isPreLayout() && !done) { 133 log = "\n" + builder.toString(); 134 } else { 135 log += "\n" + builder.toString(); 136 } 137 return log; 138 } 139 140 @Override 141 public void expectLayouts(int count) { 142 super.expectLayouts(count); 143 mOnLayoutCallbacks.mLayoutCount = 0; 144 } 145 146 public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) { 147 mOnLayoutCallbacks = onLayoutCallbacks; 148 } 149 150 @Override 151 public final void onLayoutChildren(RecyclerView.Recycler recycler, 152 RecyclerView.State state) { 153 try { 154 mTotalLayoutCount++; 155 prepareLog(recycler, state, false); 156 if (state.isPreLayout()) { 157 validateOldPositions(recycler, state); 158 } else { 159 validateClearedOldPositions(recycler, state); 160 } 161 mOnLayoutCallbacks.onLayoutChildren(recycler, this, state); 162 prepareLog(recycler, state, true); 163 } finally { 164 layoutLatch.countDown(); 165 } 166 } 167 168 private void validateClearedOldPositions(RecyclerView.Recycler recycler, 169 RecyclerView.State state) { 170 if (getTestRecyclerView() == null) { 171 return; 172 } 173 for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) { 174 assertEquals("there should NOT be an old position in post layout", 175 RecyclerView.NO_POSITION, viewHolder.mOldPosition); 176 assertEquals("there should NOT be a pre layout position in post layout", 177 RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition); 178 } 179 } 180 181 private void validateOldPositions(RecyclerView.Recycler recycler, 182 RecyclerView.State state) { 183 if (getTestRecyclerView() == null) { 184 return; 185 } 186 for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) { 187 if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) { 188 assertTrue("there should be an old position in pre-layout", 189 viewHolder.mOldPosition != RecyclerView.NO_POSITION); 190 } 191 } 192 } 193 194 public int getTotalLayoutCount() { 195 return mTotalLayoutCount; 196 } 197 198 @Override 199 public boolean canScrollVertically() { 200 return true; 201 } 202 203 @Override 204 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 205 RecyclerView.State state) { 206 mOnLayoutCallbacks.onScroll(dy, recycler, state); 207 return super.scrollVerticallyBy(dy, recycler, state); 208 } 209 210 public void onPostDispatchLayout() { 211 mOnLayoutCallbacks.postDispatchLayout(); 212 } 213 214 @Override 215 public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable { 216 super.waitForLayout(timeout, timeUnit); 217 checkForMainThreadException(); 218 } 219 } 220 221 abstract class OnLayoutCallbacks { 222 223 int mLayoutMin = Integer.MIN_VALUE; 224 225 int mLayoutItemCount = Integer.MAX_VALUE; 226 227 int expectedPreLayoutItemCount = -1; 228 229 int expectedPostLayoutItemCount = -1; 230 231 int mDeletedViewCount; 232 233 int mLayoutCount = 0; 234 235 void setExpectedItemCounts(int preLayout, int postLayout) { 236 expectedPreLayoutItemCount = preLayout; 237 expectedPostLayoutItemCount = postLayout; 238 } 239 240 void reset() { 241 mLayoutMin = Integer.MIN_VALUE; 242 mLayoutItemCount = Integer.MAX_VALUE; 243 expectedPreLayoutItemCount = -1; 244 expectedPostLayoutItemCount = -1; 245 mLayoutCount = 0; 246 } 247 248 void beforePreLayout(RecyclerView.Recycler recycler, 249 AnimationLayoutManager lm, RecyclerView.State state) { 250 mDeletedViewCount = 0; 251 for (int i = 0; i < lm.getChildCount(); i++) { 252 View v = lm.getChildAt(i); 253 if (lm.getLp(v).isItemRemoved()) { 254 mDeletedViewCount++; 255 } 256 } 257 } 258 259 void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 260 RecyclerView.State state) { 261 if (DEBUG) { 262 Log.d(TAG, "item count " + state.getItemCount()); 263 } 264 lm.detachAndScrapAttachedViews(recycler); 265 final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin; 266 final int count = mLayoutItemCount 267 == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount; 268 lm.layoutRange(recycler, start, start + count); 269 assertEquals("correct # of children should be laid out", 270 count, lm.getChildCount()); 271 lm.assertVisibleItemPositions(); 272 } 273 274 private void assertNoPreLayoutPosition(RecyclerView.Recycler recycler) { 275 for (RecyclerView.ViewHolder vh : recycler.mAttachedScrap) { 276 assertPreLayoutPosition(vh); 277 } 278 } 279 280 private void assertNoPreLayoutPosition(RecyclerView.LayoutManager lm) { 281 for (int i = 0; i < lm.getChildCount(); i ++) { 282 final RecyclerView.ViewHolder vh = mRecyclerView 283 .getChildViewHolder(lm.getChildAt(i)); 284 assertPreLayoutPosition(vh); 285 } 286 } 287 288 private void assertPreLayoutPosition(RecyclerView.ViewHolder vh) { 289 assertEquals("in post layout, there should not be a view holder w/ a pre " 290 + "layout position", RecyclerView.NO_POSITION, vh.mPreLayoutPosition); 291 assertEquals("in post layout, there should not be a view holder w/ an old " 292 + "layout position", RecyclerView.NO_POSITION, vh.mOldPosition); 293 } 294 295 void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 296 RecyclerView.State state) { 297 if (state.isPreLayout()) { 298 if (expectedPreLayoutItemCount != -1) { 299 assertEquals("on pre layout, state should return abstracted adapter size", 300 expectedPreLayoutItemCount, state.getItemCount()); 301 } 302 beforePreLayout(recycler, lm, state); 303 } else { 304 if (expectedPostLayoutItemCount != -1) { 305 assertEquals("on post layout, state should return real adapter size", 306 expectedPostLayoutItemCount, state.getItemCount()); 307 } 308 beforePostLayout(recycler, lm, state); 309 } 310 if (!state.isPreLayout()) { 311 assertNoPreLayoutPosition(recycler); 312 } 313 doLayout(recycler, lm, state); 314 if (state.isPreLayout()) { 315 afterPreLayout(recycler, lm, state); 316 } else { 317 afterPostLayout(recycler, lm, state); 318 assertNoPreLayoutPosition(lm); 319 } 320 mLayoutCount++; 321 } 322 323 void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 324 RecyclerView.State state) { 325 } 326 327 void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 328 RecyclerView.State state) { 329 } 330 331 void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 332 RecyclerView.State state) { 333 } 334 335 void postDispatchLayout() { 336 } 337 338 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 339 340 } 341 } 342 343 class TestRecyclerView extends RecyclerView { 344 345 CountDownLatch drawLatch; 346 347 public TestRecyclerView(Context context) { 348 super(context); 349 } 350 351 public TestRecyclerView(Context context, AttributeSet attrs) { 352 super(context, attrs); 353 } 354 355 public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) { 356 super(context, attrs, defStyle); 357 } 358 359 @Override 360 void initAdapterManager() { 361 super.initAdapterManager(); 362 mAdapterHelper.mOnItemProcessedCallback = new Runnable() { 363 @Override 364 public void run() { 365 validatePostUpdateOp(); 366 } 367 }; 368 } 369 370 @Override 371 boolean isAccessibilityEnabled() { 372 return true; 373 } 374 375 public void expectDraw(int count) { 376 drawLatch = new CountDownLatch(count); 377 } 378 379 public void waitForDraw(long timeout) throws Throwable { 380 drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS); 381 assertEquals("all expected draws should happen at the expected time frame", 382 0, drawLatch.getCount()); 383 } 384 385 List<ViewHolder> collectViewHolders() { 386 List<ViewHolder> holders = new ArrayList<ViewHolder>(); 387 final int childCount = getChildCount(); 388 for (int i = 0; i < childCount; i++) { 389 ViewHolder holder = getChildViewHolderInt(getChildAt(i)); 390 if (holder != null) { 391 holders.add(holder); 392 } 393 } 394 return holders; 395 } 396 397 398 private void validateViewHolderPositions() { 399 final Set<Integer> existingOffsets = new HashSet<Integer>(); 400 int childCount = getChildCount(); 401 StringBuilder log = new StringBuilder(); 402 for (int i = 0; i < childCount; i++) { 403 ViewHolder vh = getChildViewHolderInt(getChildAt(i)); 404 TestViewHolder tvh = (TestViewHolder) vh; 405 log.append(tvh.mBoundItem).append(vh) 406 .append(" hidden:") 407 .append(mChildHelper.mHiddenViews.contains(vh.itemView)) 408 .append("\n"); 409 } 410 for (int i = 0; i < childCount; i++) { 411 ViewHolder vh = getChildViewHolderInt(getChildAt(i)); 412 if (vh.isInvalid()) { 413 continue; 414 } 415 if (vh.getLayoutPosition() < 0) { 416 LayoutManager lm = getLayoutManager(); 417 for (int j = 0; j < lm.getChildCount(); j ++) { 418 assertNotSame("removed view holder should not be in LM's child list", 419 vh.itemView, lm.getChildAt(j)); 420 } 421 } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) { 422 if (!existingOffsets.add(vh.getLayoutPosition())) { 423 throw new IllegalStateException("view holder position conflict for " 424 + "existing views " + vh + "\n" + log); 425 } 426 } 427 } 428 } 429 430 void validatePostUpdateOp() { 431 try { 432 validateViewHolderPositions(); 433 if (super.mState.isPreLayout()) { 434 validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager()); 435 } 436 validateAdapterPosition((AnimationLayoutManager) getLayoutManager()); 437 } catch (Throwable t) { 438 postExceptionToInstrumentation(t); 439 } 440 } 441 442 443 444 private void validateAdapterPosition(AnimationLayoutManager lm) { 445 for (ViewHolder vh : collectViewHolders()) { 446 if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) { 447 assertEquals("adapter position calculations should match view holder " 448 + "pre layout:" + mState.isPreLayout() 449 + " positions\n" + vh + "\n" + lm.getLog(), 450 mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition); 451 } 452 } 453 } 454 455 // ensures pre layout positions are continuous block. This is not necessarily a case 456 // but valid in test RV 457 private void validatePreLayoutSequence(AnimationLayoutManager lm) { 458 Set<Integer> preLayoutPositions = new HashSet<Integer>(); 459 for (ViewHolder vh : collectViewHolders()) { 460 assertTrue("pre layout positions should be distinct " + lm.getLog(), 461 preLayoutPositions.add(vh.mPreLayoutPosition)); 462 } 463 int minPos = Integer.MAX_VALUE; 464 for (Integer pos : preLayoutPositions) { 465 if (pos < minPos) { 466 minPos = pos; 467 } 468 } 469 for (int i = 1; i < preLayoutPositions.size(); i++) { 470 assertNotNull("next position should exist " + lm.getLog(), 471 preLayoutPositions.contains(minPos + i)); 472 } 473 } 474 475 @Override 476 protected void dispatchDraw(Canvas canvas) { 477 super.dispatchDraw(canvas); 478 if (drawLatch != null) { 479 drawLatch.countDown(); 480 } 481 } 482 483 @Override 484 void dispatchLayout() { 485 try { 486 super.dispatchLayout(); 487 if (getLayoutManager() instanceof AnimationLayoutManager) { 488 ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout(); 489 } 490 } catch (Throwable t) { 491 postExceptionToInstrumentation(t); 492 } 493 494 } 495 496 497 } 498 499 abstract class AdapterOps { 500 501 final public void run(TestAdapter adapter) throws Throwable { 502 onRun(adapter); 503 } 504 505 abstract void onRun(TestAdapter testAdapter) throws Throwable; 506 } 507 508 static class CollectPositionResult { 509 510 // true if found in scrap 511 public RecyclerView.ViewHolder scrapResult; 512 513 public RecyclerView.ViewHolder adapterResult; 514 515 static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) { 516 CollectPositionResult cpr = new CollectPositionResult(); 517 cpr.scrapResult = viewHolder; 518 return cpr; 519 } 520 521 static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) { 522 CollectPositionResult cpr = new CollectPositionResult(); 523 cpr.adapterResult = viewHolder; 524 return cpr; 525 } 526 527 @Override 528 public String toString() { 529 return "CollectPositionResult{" + 530 "scrapResult=" + scrapResult + 531 ", adapterResult=" + adapterResult + 532 '}'; 533 } 534 } 535 536 static class PositionConstraint { 537 538 public static enum Type { 539 scrap, 540 adapter, 541 adapterScrap /*first pass adapter, second pass scrap*/ 542 } 543 544 Type mType; 545 546 int mOldPos; // if VH 547 548 int mPreLayoutPos; 549 550 int mPostLayoutPos; 551 552 int mValidateCount = 0; 553 554 public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) { 555 PositionConstraint constraint = new PositionConstraint(); 556 constraint.mType = Type.scrap; 557 constraint.mOldPos = oldPos; 558 constraint.mPreLayoutPos = preLayoutPos; 559 constraint.mPostLayoutPos = postLayoutPos; 560 return constraint; 561 } 562 563 public static PositionConstraint adapterScrap(int preLayoutPos, int position) { 564 PositionConstraint constraint = new PositionConstraint(); 565 constraint.mType = Type.adapterScrap; 566 constraint.mOldPos = RecyclerView.NO_POSITION; 567 constraint.mPreLayoutPos = preLayoutPos; 568 constraint.mPostLayoutPos = position;// adapter pos does not change 569 return constraint; 570 } 571 572 public static PositionConstraint adapter(int position) { 573 PositionConstraint constraint = new PositionConstraint(); 574 constraint.mType = Type.adapter; 575 constraint.mPreLayoutPos = RecyclerView.NO_POSITION; 576 constraint.mOldPos = RecyclerView.NO_POSITION; 577 constraint.mPostLayoutPos = position;// adapter pos does not change 578 return constraint; 579 } 580 581 public void assertValidate() { 582 int expectedValidate = 0; 583 if (mPreLayoutPos >= 0) { 584 expectedValidate ++; 585 } 586 if (mPostLayoutPos >= 0) { 587 expectedValidate ++; 588 } 589 assertEquals("should run all validates", expectedValidate, mValidateCount); 590 } 591 592 @Override 593 public String toString() { 594 return "Cons{" + 595 "t=" + mType.name() + 596 ", old=" + mOldPos + 597 ", pre=" + mPreLayoutPos + 598 ", post=" + mPostLayoutPos + 599 '}'; 600 } 601 602 public void validate(RecyclerView.State state, CollectPositionResult result, String log) { 603 mValidateCount ++; 604 assertNotNull(this + ": result should not be null\n" + log, result); 605 RecyclerView.ViewHolder viewHolder; 606 if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) { 607 assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult); 608 viewHolder = result.scrapResult; 609 } else { 610 assertNotNull(this + ": result should come from adapter\n" + log, 611 result.adapterResult); 612 assertEquals(this + ": old position should be none when it came from adapter\n" + log, 613 RecyclerView.NO_POSITION, result.adapterResult.getOldPosition()); 614 viewHolder = result.adapterResult; 615 } 616 if (state.isPreLayout()) { 617 assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos, 618 viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition : 619 viewHolder.mPreLayoutPosition); 620 assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos, 621 viewHolder.getLayoutPosition()); 622 if (mType == Type.scrap) { 623 assertEquals(this + ": old position should match\n" + log, mOldPos, 624 result.scrapResult.getOldPosition()); 625 } 626 } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult 627 .isRemoved()) { 628 assertEquals(this + ": post-layout position should match\n" + log + "\n\n" 629 + viewHolder, mPostLayoutPos, viewHolder.getLayoutPosition()); 630 } 631 } 632 } 633} 634