RecyclerViewAnimationsTest.java revision c35968d173f900d8024bdf38174e2225c9a7f311
1/* 2 * Copyright (C) 2014 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 android.support.v7.widget; 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.Map; 29import java.util.Set; 30import java.util.concurrent.CountDownLatch; 31import java.util.concurrent.TimeUnit; 32import java.util.concurrent.atomic.AtomicInteger; 33 34public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest { 35 36 private static final boolean DEBUG = false; 37 38 private static final String TAG = "RecyclerViewAnimationsTest"; 39 40 Throwable mainThreadException; 41 42 AnimationLayoutManager mLayoutManager; 43 44 TestAdapter mTestAdapter; 45 46 public RecyclerViewAnimationsTest() { 47 super(DEBUG); 48 } 49 50 @Override 51 protected void setUp() throws Exception { 52 super.setUp(); 53 } 54 55 void checkForMainThreadException() throws Throwable { 56 if (mainThreadException != null) { 57 throw mainThreadException; 58 } 59 } 60 61 RecyclerView setupBasic(int itemCount) throws Throwable { 62 return setupBasic(itemCount, 0, itemCount); 63 } 64 65 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount) 66 throws Throwable { 67 return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null); 68 } 69 70 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount, 71 TestAdapter testAdapter) 72 throws Throwable { 73 final TestRecyclerView recyclerView = new TestRecyclerView(getActivity()); 74 recyclerView.setHasFixedSize(true); 75 if (testAdapter == null) { 76 mTestAdapter = new TestAdapter(itemCount); 77 } else { 78 mTestAdapter = testAdapter; 79 } 80 recyclerView.setAdapter(mTestAdapter); 81 mLayoutManager = new AnimationLayoutManager(); 82 recyclerView.setLayoutManager(mLayoutManager); 83 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex; 84 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount; 85 86 mLayoutManager.expectLayouts(1); 87 recyclerView.expectDraw(1); 88 setRecyclerView(recyclerView); 89 mLayoutManager.waitForLayout(2); 90 recyclerView.waitForDraw(1); 91 mLayoutManager.mOnLayoutCallbacks.reset(); 92 return recyclerView; 93 } 94 95 96 public void getItemForDeletedViewTest() throws Throwable { 97 testGetItemForDeletedView(false); 98 testGetItemForDeletedView(true); 99 } 100 101 public void testGetItemForDeletedView(boolean stableIds) throws Throwable { 102 final Set<Integer> itemViewTypeQueries = new HashSet<Integer>(); 103 final Set<Integer> itemIdQueries = new HashSet<Integer>(); 104 TestAdapter adapter = new TestAdapter(10) { 105 @Override 106 public int getItemViewType(int position) { 107 itemViewTypeQueries.add(position); 108 return super.getItemViewType(position); 109 } 110 111 @Override 112 public long getItemId(int position) { 113 itemIdQueries.add(position); 114 return mItems.get(position).mId; 115 } 116 }; 117 adapter.setHasStableIds(stableIds); 118 setupBasic(10, 0, 10, adapter); 119 assertEquals("getItemViewType for all items should be called", 10, 120 itemViewTypeQueries.size()); 121 if (adapter.hasStableIds()) { 122 assertEquals("getItemId should be called when adapter has stable ids", 10, 123 itemIdQueries.size()); 124 } else { 125 assertEquals("getItemId should not be called when adapter does not have stable ids", 0, 126 itemIdQueries.size()); 127 } 128 itemViewTypeQueries.clear(); 129 itemIdQueries.clear(); 130 mLayoutManager.expectLayouts(2); 131 // delete last two 132 final int deleteStart = 8; 133 final int deleteCount = adapter.getItemCount() - deleteStart; 134 adapter.deleteAndNotify(deleteStart, deleteCount); 135 mLayoutManager.waitForLayout(2); 136 for (int i = 0; i < deleteStart; i++) { 137 assertTrue("getItemViewType for existing item " + i + " should be called", 138 itemViewTypeQueries.contains(i)); 139 if (adapter.hasStableIds()) { 140 assertTrue("getItemId for existing item " + i 141 + " should be called when adapter has stable ids", 142 itemIdQueries.contains(i)); 143 } 144 } 145 for (int i = deleteStart; i < deleteStart + deleteCount; i++) { 146 assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called", 147 itemViewTypeQueries.contains(i)); 148 if (adapter.hasStableIds()) { 149 assertTrue("getItemId for deleted item " + i + " SHOULD NOT be called", 150 itemIdQueries.contains(i)); 151 } 152 } 153 } 154 155 public void testAdapterChangeDuringScrolling() throws Throwable { 156 setupBasic(10); 157 final AtomicInteger onLayoutItemCount = new AtomicInteger(0); 158 final AtomicInteger onScrollItemCount = new AtomicInteger(0); 159 160 mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() { 161 @Override 162 void onLayoutChildren(RecyclerView.Recycler recycler, 163 AnimationLayoutManager lm, RecyclerView.State state) { 164 onLayoutItemCount.set(state.getItemCount()); 165 super.onLayoutChildren(recycler, lm, state); 166 } 167 168 @Override 169 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 170 onScrollItemCount.set(state.getItemCount()); 171 super.onScroll(dx, recycler, state); 172 } 173 }); 174 runTestOnUiThread(new Runnable() { 175 @Override 176 public void run() { 177 mTestAdapter.mItems.remove(5); 178 mTestAdapter.notifyItemRangeRemoved(5, 1); 179 mRecyclerView.scrollBy(0, 100); 180 assertTrue("scrolling while there are pending adapter updates should " 181 + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0); 182 assertEquals("scroll by should be called w/ updated adapter count", 183 mTestAdapter.mItems.size(), onScrollItemCount.get()); 184 185 } 186 }); 187 } 188 189 public void testAddInvisibleAndVisible() throws Throwable { 190 setupBasic(10, 1, 7); 191 mLayoutManager.expectLayouts(2); 192 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12); 193 mTestAdapter.addAndNotify(0, 1);// add a new item 0 // invisible 194 mTestAdapter.addAndNotify(7, 1);// add a new item after 5th (old 5, new 6) 195 mLayoutManager.waitForLayout(2); 196 } 197 198 public void testAddInvisible() throws Throwable { 199 setupBasic(10, 1, 7); 200 mLayoutManager.expectLayouts(1); 201 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12); 202 mTestAdapter.addAndNotify(0, 1);// add a new item 0 203 mTestAdapter.addAndNotify(8, 1);// add a new item after 6th (old 6, new 7) 204 mLayoutManager.waitForLayout(2); 205 } 206 207 public void testBasicAdd() throws Throwable { 208 setupBasic(10); 209 mLayoutManager.expectLayouts(2); 210 setExpectedItemCounts(10, 13); 211 mTestAdapter.addAndNotify(2, 3); 212 mLayoutManager.waitForLayout(2); 213 } 214 215 public TestRecyclerView getTestRecyclerView() { 216 return (TestRecyclerView) mRecyclerView; 217 } 218 219 public void testRemoveScrapInvalidate() throws Throwable { 220 setupBasic(10); 221 TestRecyclerView testRecyclerView = getTestRecyclerView(); 222 mLayoutManager.expectLayouts(1); 223 testRecyclerView.expectDraw(1); 224 runTestOnUiThread(new Runnable() { 225 @Override 226 public void run() { 227 mTestAdapter.mItems.clear(); 228 mTestAdapter.notifyDataSetChanged(); 229 } 230 }); 231 mLayoutManager.waitForLayout(2); 232 testRecyclerView.waitForDraw(2); 233 } 234 235 public void testDeleteVisibleAndInvisible() throws Throwable { 236 setupBasic(11, 3, 5); //layout items 3 4 5 6 7 237 mLayoutManager.expectLayouts(2); 238 setLayoutRange(3, 6); //layout previously invisible child 10 from end of the list 239 setExpectedItemCounts(9, 8); 240 mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9 241 mLayoutManager.waitForLayout(2); 242 } 243 244 private void setLayoutRange(int start, int count) { 245 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start; 246 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count; 247 } 248 249 private void setExpectedItemCounts(int preLayout, int postLayout) { 250 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout); 251 } 252 253 public void testDeleteInvisible() throws Throwable { 254 setupBasic(10, 1, 7); 255 mLayoutManager.expectLayouts(1); 256 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8); 257 mTestAdapter.deleteAndNotify(0, 1);// delete item id 0 258 mTestAdapter.deleteAndNotify(7, 1);// delete item id 8 259 mLayoutManager.waitForLayout(2); 260 } 261 262 public void testBasicDelete() throws Throwable { 263 setupBasic(10); 264 final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() { 265 @Override 266 public void postDispatchLayout() { 267 // verify this only in first layout 268 assertEquals("deleted views should still be children of RV", 269 mLayoutManager.getChildCount() + mDeletedViewCount 270 , mRecyclerView.getChildCount()); 271 } 272 273 @Override 274 void afterPreLayout(RecyclerView.Recycler recycler, 275 AnimationLayoutManager layoutManager, 276 RecyclerView.State state) { 277 super.afterPreLayout(recycler, layoutManager, state); 278 mLayoutItemCount = 3; 279 mLayoutMin = 0; 280 } 281 }; 282 callbacks.mLayoutItemCount = 10; 283 callbacks.setExpectedItemCounts(10, 3); 284 mLayoutManager.setOnLayoutCallbacks(callbacks); 285 286 mLayoutManager.expectLayouts(2); 287 mTestAdapter.deleteAndNotify(0, 7); 288 mLayoutManager.waitForLayout(2); 289 callbacks.reset();// when animations end another layout will happen 290 } 291 292 293 class AnimationLayoutManager extends TestLayoutManager { 294 295 OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() { 296 }; 297 298 @Override 299 public boolean supportsPredictiveItemAnimations() { 300 return true; 301 } 302 303 @Override 304 public void expectLayouts(int count) { 305 super.expectLayouts(count); 306 mOnLayoutCallbacks.mLayoutCount = 0; 307 } 308 309 public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) { 310 mOnLayoutCallbacks = onLayoutCallbacks; 311 } 312 313 @Override 314 public final void onLayoutChildren(RecyclerView.Recycler recycler, 315 RecyclerView.State state) { 316 try { 317 mOnLayoutCallbacks.onLayoutChildren(recycler, this, state); 318 } finally { 319 layoutLatch.countDown(); 320 } 321 } 322 323 @Override 324 public boolean canScrollVertically() { 325 return true; 326 } 327 328 @Override 329 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 330 RecyclerView.State state) { 331 mOnLayoutCallbacks.onScroll(dy, recycler, state); 332 return super.scrollVerticallyBy(dy, recycler, state); 333 } 334 335 public void onPostDispatchLayout() { 336 mOnLayoutCallbacks.postDispatchLayout(); 337 } 338 339 @Override 340 public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable { 341 super.waitForLayout(timeout, timeUnit); 342 checkForMainThreadException(); 343 } 344 } 345 346 abstract class OnLayoutCallbacks { 347 348 int mLayoutMin = Integer.MIN_VALUE; 349 350 int mLayoutItemCount = Integer.MAX_VALUE; 351 352 int expectedPreLayoutItemCount = -1; 353 354 int expectedPostLayoutItemCount = -1; 355 356 private int mLayoutCount; 357 358 int mDeletedViewCount; 359 360 void setExpectedItemCounts(int preLayout, int postLayout) { 361 expectedPreLayoutItemCount = preLayout; 362 expectedPostLayoutItemCount = postLayout; 363 } 364 365 void reset() { 366 mLayoutCount = 0; 367 mLayoutMin = Integer.MIN_VALUE; 368 mLayoutItemCount = Integer.MAX_VALUE; 369 expectedPreLayoutItemCount = -1; 370 expectedPostLayoutItemCount = -1; 371 } 372 373 void beforePreLayout(RecyclerView.Recycler recycler, 374 AnimationLayoutManager lm, RecyclerView.State state) { 375 mDeletedViewCount = 0; 376 for (int i = 0; i < lm.getChildCount(); i++) { 377 View v = lm.getChildAt(i); 378 if (lm.getLp(v).isItemRemoved()) { 379 mDeletedViewCount++; 380 } 381 } 382 } 383 384 void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 385 RecyclerView.State state) { 386 if (DEBUG) { 387 Log.d(TAG, "item count " + state.getItemCount()); 388 } 389 lm.detachAndScrapAttachedViews(recycler); 390 final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin; 391 final int count = mLayoutItemCount 392 == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount; 393 lm.layoutRange(recycler, start, start + count); 394 assertEquals("correct # of children should be laid out", 395 count - (inPreLayout() ? mDeletedViewCount : 0), lm.getChildCount()); 396 if (!inPreLayout()) { // may not be the correct check 397 lm.assertVisibleItemPositions(); 398 } 399 } 400 401 void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 402 RecyclerView.State state) { 403 404 if (mLayoutCount == 0) { 405 if (expectedPreLayoutItemCount != -1) { 406 assertEquals("on pre layout, state should return abstracted adapter size", 407 expectedPreLayoutItemCount, state.getItemCount()); 408 } 409 beforePreLayout(recycler, lm, state); 410 } else if (mLayoutCount == 1) { 411 if (expectedPostLayoutItemCount != -1) { 412 assertEquals("on post layout, state should return real adapter size", 413 expectedPostLayoutItemCount, state.getItemCount()); 414 } 415 beforePostLayout(recycler, lm, state); 416 } 417 doLayout(recycler, lm, state); 418 if (mLayoutCount == 0) { 419 afterPreLayout(recycler, lm, state); 420 } else if (mLayoutCount == 1) { 421 afterPostLayout(recycler, lm, state); 422 } 423 mLayoutCount++; 424 } 425 426 void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 427 RecyclerView.State state) { 428 } 429 430 void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 431 RecyclerView.State state) { 432 } 433 434 void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 435 RecyclerView.State state) { 436 } 437 438 void postDispatchLayout() { 439 } 440 441 boolean inPreLayout() { 442 return mLayoutCount == 0; 443 } 444 445 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 446 447 } 448 } 449 450 class TestRecyclerView extends RecyclerView { 451 452 CountDownLatch drawLatch; 453 454 public TestRecyclerView(Context context) { 455 super(context); 456 } 457 458 public TestRecyclerView(Context context, AttributeSet attrs) { 459 super(context, attrs); 460 } 461 462 public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) { 463 super(context, attrs, defStyle); 464 } 465 466 public void expectDraw(int count) { 467 drawLatch = new CountDownLatch(count); 468 } 469 470 public void waitForDraw(long timeout) throws Throwable { 471 drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS); 472 assertEquals("all expected draws should happen at the expected time frame", 473 0, drawLatch.getCount()); 474 } 475 476 @Override 477 protected void dispatchDraw(Canvas canvas) { 478 super.dispatchDraw(canvas); 479 if (drawLatch != null) { 480 drawLatch.countDown(); 481 } 482 } 483 484 @Override 485 void dispatchLayout() { 486 try { 487 super.dispatchLayout(); 488 if (getLayoutManager() instanceof AnimationLayoutManager) { 489 ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout(); 490 } 491 } catch (Throwable t) { 492 postExceptionToInstrumentation(t); 493 } 494 495 } 496 497 private void postExceptionToInstrumentation(Throwable t) { 498 if (DEBUG) { 499 Log.e(TAG, "captured exception on main thread", t); 500 } 501 mainThreadException = t; 502 if (mLayoutManager instanceof TestLayoutManager) { 503 TestLayoutManager lm = mLayoutManager; 504 // finish all layouts so that we get the correct exception 505 while (lm.layoutLatch.getCount() > 0) { 506 lm.layoutLatch.countDown(); 507 } 508 } 509 } 510 } 511} 512