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 
17 package android.support.v7.widget;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.Rect;
22 import android.support.v4.view.ViewCompat;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.view.View;
26 import android.view.ViewGroup;
27 
28 import java.util.ArrayList;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Set;
34 import java.util.concurrent.CountDownLatch;
35 import java.util.concurrent.TimeUnit;
36 import java.util.concurrent.atomic.AtomicInteger;
37 
38 public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
39 
40     private static final boolean DEBUG = false;
41 
42     private static final String TAG = "RecyclerViewAnimationsTest";
43 
44     AnimationLayoutManager mLayoutManager;
45 
46     TestAdapter mTestAdapter;
47 
RecyclerViewAnimationsTest()48     public RecyclerViewAnimationsTest() {
49         super(DEBUG);
50     }
51 
52     @Override
setUp()53     protected void setUp() throws Exception {
54         super.setUp();
55     }
56 
setupBasic(int itemCount)57     RecyclerView setupBasic(int itemCount) throws Throwable {
58         return setupBasic(itemCount, 0, itemCount);
59     }
60 
setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)61     RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
62             throws Throwable {
63         return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null);
64     }
65 
setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount, TestAdapter testAdapter)66     RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount,
67             TestAdapter testAdapter)
68             throws Throwable {
69         final TestRecyclerView recyclerView = new TestRecyclerView(getActivity());
70         recyclerView.setHasFixedSize(true);
71         if (testAdapter == null) {
72             mTestAdapter = new TestAdapter(itemCount);
73         } else {
74             mTestAdapter = testAdapter;
75         }
76         recyclerView.setAdapter(mTestAdapter);
77         mLayoutManager = new AnimationLayoutManager();
78         recyclerView.setLayoutManager(mLayoutManager);
79         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex;
80         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount;
81 
82         mLayoutManager.expectLayouts(1);
83         recyclerView.expectDraw(1);
84         setRecyclerView(recyclerView);
85         mLayoutManager.waitForLayout(2);
86         recyclerView.waitForDraw(1);
87         mLayoutManager.mOnLayoutCallbacks.reset();
88         getInstrumentation().waitForIdleSync();
89         assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount());
90         assertEquals("all expected children should be laid out", firstLayoutItemCount,
91                 mLayoutManager.getChildCount());
92         return recyclerView;
93     }
94 
testDetachBeforeAnimations()95     public void testDetachBeforeAnimations() throws Throwable {
96         setupBasic(10, 0, 5);
97         final RecyclerView rv = mRecyclerView;
98         waitForAnimations(2);
99         final DefaultItemAnimator animator = new DefaultItemAnimator() {
100             @Override
101             public void runPendingAnimations() {
102                 super.runPendingAnimations();
103             }
104         };
105         rv.setItemAnimator(animator);
106         mLayoutManager.expectLayouts(2);
107         mTestAdapter.deleteAndNotify(3, 4);
108         mLayoutManager.waitForLayout(2);
109         removeRecyclerView();
110         assertNull("test sanity check RV should be removed", rv.getParent());
111         assertEquals("no views should be hidden", 0, rv.mChildHelper.mHiddenViews.size());
112         assertFalse("there should not be any animations running", animator.isRunning());
113     }
114 
testMoveDeleted()115     public void testMoveDeleted() throws Throwable {
116         setupBasic(4, 0, 3);
117         waitForAnimations(2);
118         final View[] targetChild = new View[1];
119         final LoggingItemAnimator animator = new LoggingItemAnimator();
120         runTestOnUiThread(new Runnable() {
121             @Override
122             public void run() {
123                 mRecyclerView.setItemAnimator(animator);
124                 targetChild[0] = mRecyclerView.getChildAt(1);
125             }
126         });
127 
128         assertNotNull("test sanity", targetChild);
129         mLayoutManager.expectLayouts(1);
130         runTestOnUiThread(new Runnable() {
131             @Override
132             public void run() {
133                 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
134                     @Override
135                     public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
136                                                RecyclerView.State state) {
137                         if (view == targetChild[0]) {
138                             outRect.set(10, 20, 30, 40);
139                         } else {
140                             outRect.set(0, 0, 0, 0);
141                         }
142                     }
143                 });
144             }
145         });
146         mLayoutManager.waitForLayout(1);
147 
148         // now delete that item.
149         mLayoutManager.expectLayouts(2);
150         RecyclerView.ViewHolder targetVH = mRecyclerView.getChildViewHolder(targetChild[0]);
151         targetChild[0] = null;
152         mTestAdapter.deleteAndNotify(1, 1);
153         mLayoutManager.waitForLayout(2);
154         assertFalse("if deleted view moves, it should not be in move animations",
155                 animator.mMoveVHs.contains(targetVH));
156         assertEquals("only 1 item is deleted", 1, animator.mRemoveVHs.size());
157         assertTrue("the target view is removed", animator.mRemoveVHs.contains(targetVH
158         ));
159     }
160 
runTestImportantForAccessibilityWhileDeteling( final int boundImportantForAccessibility, final int expectedImportantForAccessibility)161     private void runTestImportantForAccessibilityWhileDeteling(
162             final int boundImportantForAccessibility,
163             final int expectedImportantForAccessibility) throws Throwable {
164         // Adapter binding the item to the initial accessibility option.
165         // RecyclerView is expected to change it to 'expectedImportantForAccessibility'.
166         TestAdapter adapter = new TestAdapter(1) {
167             @Override
168             public void onBindViewHolder(TestViewHolder holder, int position) {
169                 super.onBindViewHolder(holder, position);
170                 ViewCompat.setImportantForAccessibility(
171                         holder.itemView, boundImportantForAccessibility);
172             }
173         };
174 
175         // Set up with 1 item.
176         setupBasic(1, 0, 1, adapter);
177         waitForAnimations(2);
178         final View[] targetChild = new View[1];
179         final LoggingItemAnimator animator = new LoggingItemAnimator();
180         animator.setRemoveDuration(500);
181         runTestOnUiThread(new Runnable() {
182             @Override
183             public void run() {
184                 mRecyclerView.setItemAnimator(animator);
185                 targetChild[0] = mRecyclerView.getChildAt(0);
186                 assertEquals(
187                         expectedImportantForAccessibility,
188                         ViewCompat.getImportantForAccessibility(targetChild[0]));
189             }
190         });
191 
192         assertNotNull("test sanity", targetChild[0]);
193 
194         // now delete that item.
195         mLayoutManager.expectLayouts(2);
196         mTestAdapter.deleteAndNotify(0, 1);
197 
198         mLayoutManager.waitForLayout(2);
199 
200         runTestOnUiThread(new Runnable() {
201             @Override
202             public void run() {
203                 // The view is still a child of mRecyclerView, and is invisible for accessibility.
204                 assertTrue(targetChild[0].getParent() == mRecyclerView);
205                 assertEquals(
206                         ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
207                         ViewCompat.getImportantForAccessibility(targetChild[0]));
208             }
209         });
210 
211         waitForAnimations(2);
212 
213         // Delete animation is now complete.
214         runTestOnUiThread(new Runnable() {
215             @Override
216             public void run() {
217                 // The view is in recycled state, and back to the expected accessibility.
218                 assertTrue(targetChild[0].getParent() == null);
219                 assertEquals(
220                         expectedImportantForAccessibility,
221                         ViewCompat.getImportantForAccessibility(targetChild[0]));
222             }
223         });
224 
225         // Add 1 element, which should use same view.
226         mLayoutManager.expectLayouts(2);
227         mTestAdapter.addAndNotify(1);
228         mLayoutManager.waitForLayout(2);
229 
230         runTestOnUiThread(new Runnable() {
231             @Override
232             public void run() {
233                 // The view should be reused, and have the expected accessibility.
234                 assertTrue(
235                         "the item must be reused", targetChild[0] == mRecyclerView.getChildAt(0));
236                 assertEquals(
237                         expectedImportantForAccessibility,
238                         ViewCompat.getImportantForAccessibility(targetChild[0]));
239             }
240         });
241     }
242 
testImportantForAccessibilityWhileDetelingAuto()243     public void testImportantForAccessibilityWhileDetelingAuto() throws Throwable {
244         runTestImportantForAccessibilityWhileDeteling(
245                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO,
246                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
247     }
248 
testImportantForAccessibilityWhileDetelingNo()249     public void testImportantForAccessibilityWhileDetelingNo() throws Throwable {
250         runTestImportantForAccessibilityWhileDeteling(
251                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO,
252                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
253     }
254 
testImportantForAccessibilityWhileDetelingNoHideDescandants()255     public void testImportantForAccessibilityWhileDetelingNoHideDescandants() throws Throwable {
256         runTestImportantForAccessibilityWhileDeteling(
257                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
258                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
259     }
260 
testImportantForAccessibilityWhileDetelingYes()261     public void testImportantForAccessibilityWhileDetelingYes() throws Throwable {
262         runTestImportantForAccessibilityWhileDeteling(
263                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
264                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
265     }
266 
testPreLayoutPositionCleanup()267     public void testPreLayoutPositionCleanup() throws Throwable {
268         setupBasic(4, 0, 4);
269         mLayoutManager.expectLayouts(2);
270         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
271             @Override
272             void beforePreLayout(RecyclerView.Recycler recycler,
273                     AnimationLayoutManager lm, RecyclerView.State state) {
274                 mLayoutMin = 0;
275                 mLayoutItemCount = 3;
276             }
277 
278             @Override
279             void beforePostLayout(RecyclerView.Recycler recycler,
280                     AnimationLayoutManager layoutManager,
281                     RecyclerView.State state) {
282                 mLayoutMin = 0;
283                 mLayoutItemCount = 4;
284             }
285         };
286         mTestAdapter.addAndNotify(0, 1);
287         mLayoutManager.waitForLayout(2);
288 
289 
290 
291     }
292 
testAddRemoveSamePass()293     public void testAddRemoveSamePass() throws Throwable {
294         final List<RecyclerView.ViewHolder> mRecycledViews
295                 = new ArrayList<RecyclerView.ViewHolder>();
296         TestAdapter adapter = new TestAdapter(50) {
297             @Override
298             public void onViewRecycled(TestViewHolder holder) {
299                 super.onViewRecycled(holder);
300                 mRecycledViews.add(holder);
301             }
302         };
303         adapter.setHasStableIds(true);
304         setupBasic(50, 3, 5, adapter);
305         mRecyclerView.setItemViewCacheSize(0);
306         final ArrayList<RecyclerView.ViewHolder> addVH
307                 = new ArrayList<RecyclerView.ViewHolder>();
308         final ArrayList<RecyclerView.ViewHolder> removeVH
309                 = new ArrayList<RecyclerView.ViewHolder>();
310 
311         final ArrayList<RecyclerView.ViewHolder> moveVH
312                 = new ArrayList<RecyclerView.ViewHolder>();
313 
314         final View[] testView = new View[1];
315         mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
316             @Override
317             public boolean animateAdd(RecyclerView.ViewHolder holder) {
318                 addVH.add(holder);
319                 return true;
320             }
321 
322             @Override
323             public boolean animateRemove(RecyclerView.ViewHolder holder) {
324                 removeVH.add(holder);
325                 return true;
326             }
327 
328             @Override
329             public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY,
330                     int toX, int toY) {
331                 moveVH.add(holder);
332                 return true;
333             }
334         });
335         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
336             @Override
337             void afterPreLayout(RecyclerView.Recycler recycler,
338                     AnimationLayoutManager layoutManager,
339                     RecyclerView.State state) {
340                 super.afterPreLayout(recycler, layoutManager, state);
341                 testView[0] = recycler.getViewForPosition(45);
342                 testView[0].measure(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST),
343                         View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST));
344                 testView[0].layout(10, 10, 10 + testView[0].getMeasuredWidth(),
345                         10 + testView[0].getMeasuredHeight());
346                 layoutManager.addView(testView[0], 4);
347             }
348 
349             @Override
350             void afterPostLayout(RecyclerView.Recycler recycler,
351                     AnimationLayoutManager layoutManager,
352                     RecyclerView.State state) {
353                 super.afterPostLayout(recycler, layoutManager, state);
354                 testView[0].layout(50, 50, 50 + testView[0].getMeasuredWidth(),
355                         50 + testView[0].getMeasuredHeight());
356                 layoutManager.addDisappearingView(testView[0], 4);
357             }
358         };
359         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 3;
360         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 5;
361         mRecycledViews.clear();
362         mLayoutManager.expectLayouts(2);
363         mTestAdapter.deleteAndNotify(3, 1);
364         mLayoutManager.waitForLayout(2);
365 
366         for (RecyclerView.ViewHolder vh : addVH) {
367             assertNotSame("add-remove item should not animate add", testView[0], vh.itemView);
368         }
369         for (RecyclerView.ViewHolder vh : moveVH) {
370             assertNotSame("add-remove item should not animate move", testView[0], vh.itemView);
371         }
372         for (RecyclerView.ViewHolder vh : removeVH) {
373             assertNotSame("add-remove item should not animate remove", testView[0], vh.itemView);
374         }
375         boolean found = false;
376         for (RecyclerView.ViewHolder vh : mRecycledViews) {
377             found |= vh.itemView == testView[0];
378         }
379         assertTrue("added-removed view should be recycled", found);
380     }
381 
testChangeAnimations()382     public void testChangeAnimations()  throws Throwable {
383         final boolean[] booleans = {true, false};
384         for (boolean supportsChange : booleans) {
385             for (boolean changeType : booleans) {
386                 for (boolean hasStableIds : booleans) {
387                     for (boolean deleteSomeItems : booleans) {
388                         changeAnimTest(supportsChange, changeType, hasStableIds, deleteSomeItems);
389                     }
390                     removeRecyclerView();
391                 }
392             }
393         }
394     }
changeAnimTest(final boolean supportsChangeAnim, final boolean changeType, final boolean hasStableIds, final boolean deleteSomeItems)395     public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType,
396             final boolean hasStableIds, final boolean deleteSomeItems)  throws Throwable {
397         final int changedIndex = 3;
398         final int defaultType = 1;
399         final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType);
400         final String logPrefix = "supportsChangeAnim:" + supportsChangeAnim +
401                 ", change view type:" + changeType +
402                 ", has stable ids:" + hasStableIds +
403                 ", force predictive:" + deleteSomeItems;
404         TestAdapter testAdapter = new TestAdapter(10) {
405             @Override
406             public int getItemViewType(int position) {
407                 return position == changedIndex ? changedIndexNewType.get() : defaultType;
408             }
409 
410             @Override
411             public TestViewHolder onCreateViewHolder(ViewGroup parent,
412                     int viewType) {
413                 TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
414                 if (DEBUG) {
415                     Log.d(TAG, logPrefix + " onCreateVH" + vh.toString());
416                 }
417                 return vh;
418             }
419 
420             @Override
421             public void onBindViewHolder(TestViewHolder holder,
422                     int position) {
423                 super.onBindViewHolder(holder, position);
424                 if (DEBUG) {
425                     Log.d(TAG, logPrefix + " onBind to " + position + "" + holder.toString());
426                 }
427             }
428         };
429         testAdapter.setHasStableIds(hasStableIds);
430         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
431         mRecyclerView.getItemAnimator().setSupportsChangeAnimations(supportsChangeAnim);
432 
433         final RecyclerView.ViewHolder toBeChangedVH =
434                 mRecyclerView.findViewHolderForLayoutPosition(changedIndex);
435         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
436             @Override
437             void afterPreLayout(RecyclerView.Recycler recycler,
438                     AnimationLayoutManager layoutManager,
439                     RecyclerView.State state) {
440                 RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
441                         changedIndex);
442                 if (supportsChangeAnim) {
443                     assertTrue(logPrefix + " changed view holder should have correct flag"
444                             , vh.isChanged());
445                 } else {
446                     assertFalse(logPrefix + " changed view holder should have correct flag"
447                             , vh.isChanged());
448                 }
449             }
450 
451             @Override
452             void afterPostLayout(RecyclerView.Recycler recycler,
453                     AnimationLayoutManager layoutManager, RecyclerView.State state) {
454                 RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
455                         changedIndex);
456                 assertFalse(logPrefix + "VH should not be marked as changed", vh.isChanged());
457                 if (supportsChangeAnim) {
458                     assertNotSame(logPrefix + "a new VH should be given if change is supported",
459                             toBeChangedVH, vh);
460                 } else if (!changeType && hasStableIds) {
461                     assertSame(logPrefix + "if change animations are not supported but we have "
462                             + "stable ids, same view holder should be returned", toBeChangedVH, vh);
463                 }
464                 super.beforePostLayout(recycler, layoutManager, state);
465             }
466         };
467         mLayoutManager.expectLayouts(1);
468         if (changeType) {
469             changedIndexNewType.set(defaultType + 1);
470         }
471         if (deleteSomeItems) {
472             runTestOnUiThread(new Runnable() {
473                 @Override
474                 public void run() {
475                     try {
476                         mTestAdapter.deleteAndNotify(changedIndex + 2, 1);
477                         mTestAdapter.notifyItemChanged(3);
478                     } catch (Throwable throwable) {
479                         throwable.printStackTrace();
480                     }
481 
482                 }
483             });
484         } else {
485             mTestAdapter.notifyItemChanged(3);
486         }
487 
488         mLayoutManager.waitForLayout(2);
489     }
490 
listEquals(List list1, List list2)491     private static boolean listEquals(List list1, List list2) {
492         if (list1.size() != list2.size()) {
493             return false;
494         }
495         for (int i= 0; i < list1.size(); i++) {
496             if (!list1.get(i).equals(list2.get(i))) {
497                 return false;
498             }
499         }
500         return true;
501     }
502 
testChangeWithPayload(final boolean supportsChangeAnim, Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)503     private void testChangeWithPayload(final boolean supportsChangeAnim,
504             Object[][] notifyPayloads,  Object[][] expectedPayloadsInOnBind)
505                     throws Throwable {
506         final List<Object> expectedPayloads = new ArrayList<Object>();
507         final int changedIndex = 3;
508         TestAdapter testAdapter = new TestAdapter(10) {
509             @Override
510             public int getItemViewType(int position) {
511                 return 1;
512             }
513 
514             @Override
515             public TestViewHolder onCreateViewHolder(ViewGroup parent,
516                     int viewType) {
517                 TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
518                 if (DEBUG) {
519                     Log.d(TAG, " onCreateVH" + vh.toString());
520                 }
521                 return vh;
522             }
523 
524             @Override
525             public void onBindViewHolder(TestViewHolder holder,
526                     int position, List<Object> payloads) {
527                 super.onBindViewHolder(holder, position);
528                 if (DEBUG) {
529                     Log.d(TAG, " onBind to " + position + "" + holder.toString());
530                 }
531                 assertTrue(listEquals(payloads, expectedPayloads));
532             }
533         };
534         testAdapter.setHasStableIds(false);
535         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
536         mRecyclerView.getItemAnimator().setSupportsChangeAnimations(supportsChangeAnim);
537 
538         int numTests = notifyPayloads.length;
539         for (int i= 0; i < numTests; i++) {
540             mLayoutManager.expectLayouts(1);
541             expectedPayloads.clear();
542             for (int j = 0; j < expectedPayloadsInOnBind[i].length; j++) {
543                 expectedPayloads.add(expectedPayloadsInOnBind[i][j]);
544             }
545             final Object[] payloadsToSend = notifyPayloads[i];
546             runTestOnUiThread(new Runnable() {
547                 @Override
548                 public void run() {
549                     for (int j = 0; j < payloadsToSend.length; j++) {
550                         mTestAdapter.notifyItemChanged(changedIndex, payloadsToSend[j]);
551                     }
552                 }
553             });
554             mLayoutManager.waitForLayout(2);
555         }
556     }
557 
testCrossFadingChangeAnimationWithPayload()558     public void testCrossFadingChangeAnimationWithPayload()  throws Throwable {
559         // for crossfading change animation,  will receive EMPTY payload in onBindViewHolder
560         testChangeWithPayload(true,
561                 new Object[][]{
562                     new Object[]{"abc"},
563                     new Object[]{"abc", null, "cdf"},
564                     new Object[]{"abc", null},
565                     new Object[]{null, "abc"},
566                     new Object[]{"abc", "cdf"}
567                 },
568                 new Object[][]{
569                     new Object[0],
570                     new Object[0],
571                     new Object[0],
572                     new Object[0],
573                     new Object[0]
574                 });
575     }
576 
testNoChangeAnimationWithPayload()577     public void testNoChangeAnimationWithPayload()  throws Throwable {
578         // for Change Animation disabled, payload should match the payloads unless
579         // null payload is fired.
580         testChangeWithPayload(false,
581                 new Object[][]{
582                     new Object[]{"abc"},
583                     new Object[]{"abc", null, "cdf"},
584                     new Object[]{"abc", null},
585                     new Object[]{null, "abc"},
586                     new Object[]{"abc", "cdf"}
587                 },
588                 new Object[][]{
589                 new Object[]{"abc"},
590                 new Object[0],
591                 new Object[0],
592                 new Object[0],
593                 new Object[]{"abc", "cdf"}
594                 });
595     }
596 
testRecycleDuringAnimations()597     public void testRecycleDuringAnimations() throws Throwable {
598         final AtomicInteger childCount = new AtomicInteger(0);
599         final TestAdapter adapter = new TestAdapter(1000) {
600             @Override
601             public TestViewHolder onCreateViewHolder(ViewGroup parent,
602                     int viewType) {
603                 childCount.incrementAndGet();
604                 return super.onCreateViewHolder(parent, viewType);
605             }
606         };
607         setupBasic(1000, 10, 20, adapter);
608         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 10;
609         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 20;
610 
611         mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() {
612             @Override
613             public void putRecycledView(RecyclerView.ViewHolder scrap) {
614                 super.putRecycledView(scrap);
615                 childCount.decrementAndGet();
616             }
617 
618             @Override
619             public RecyclerView.ViewHolder getRecycledView(int viewType) {
620                 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
621                 if (recycledView != null) {
622                     childCount.incrementAndGet();
623                 }
624                 return recycledView;
625             }
626         });
627 
628         // now keep adding children to trigger more children being created etc.
629         for (int i = 0; i < 100; i ++) {
630             adapter.addAndNotify(15, 1);
631             Thread.sleep(50);
632         }
633         getInstrumentation().waitForIdleSync();
634         waitForAnimations(2);
635         assertEquals("Children count should add up", childCount.get(),
636                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
637     }
638 
testNotifyDataSetChanged()639     public void testNotifyDataSetChanged() throws Throwable {
640         setupBasic(10, 3, 4);
641         int layoutCount = mLayoutManager.mTotalLayoutCount;
642         mLayoutManager.expectLayouts(1);
643         runTestOnUiThread(new Runnable() {
644             @Override
645             public void run() {
646                 try {
647                     mTestAdapter.deleteAndNotify(4, 1);
648                     mTestAdapter.dispatchDataSetChanged();
649                 } catch (Throwable throwable) {
650                     throwable.printStackTrace();
651                 }
652 
653             }
654         });
655         mLayoutManager.waitForLayout(2);
656         getInstrumentation().waitForIdleSync();
657         assertEquals("on notify data set changed, predictive animations should not run",
658                 layoutCount + 1, mLayoutManager.mTotalLayoutCount);
659         mLayoutManager.expectLayouts(2);
660         mTestAdapter.addAndNotify(4, 2);
661         // make sure animations recover
662         mLayoutManager.waitForLayout(2);
663     }
664 
testStableIdNotifyDataSetChanged()665     public void testStableIdNotifyDataSetChanged() throws Throwable {
666         final int itemCount = 20;
667         List<Item> initialSet = new ArrayList<Item>();
668         final TestAdapter adapter = new TestAdapter(itemCount) {
669             @Override
670             public long getItemId(int position) {
671                 return mItems.get(position).mId;
672             }
673         };
674         adapter.setHasStableIds(true);
675         initialSet.addAll(adapter.mItems);
676         positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
677             @Override
678             void onRun(TestAdapter testAdapter) throws Throwable {
679                 Item item5 = adapter.mItems.get(5);
680                 Item item6 = adapter.mItems.get(6);
681                 item5.mAdapterIndex = 6;
682                 item6.mAdapterIndex = 5;
683                 adapter.mItems.remove(5);
684                 adapter.mItems.add(6, item5);
685                 adapter.dispatchDataSetChanged();
686                 //hacky, we support only 1 layout pass
687                 mLayoutManager.layoutLatch.countDown();
688             }
689         }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
690                 PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
691                 PositionConstraint.scrap(9, -1, 9));
692         // now mix items.
693     }
694 
695 
testGetItemForDeletedView()696     public void testGetItemForDeletedView() throws Throwable {
697         getItemForDeletedViewTest(false);
698         getItemForDeletedViewTest(true);
699     }
700 
getItemForDeletedViewTest(boolean stableIds)701     public void getItemForDeletedViewTest(boolean stableIds) throws Throwable {
702         final Set<Integer> itemViewTypeQueries = new HashSet<Integer>();
703         final Set<Integer> itemIdQueries = new HashSet<Integer>();
704         TestAdapter adapter = new TestAdapter(10) {
705             @Override
706             public int getItemViewType(int position) {
707                 itemViewTypeQueries.add(position);
708                 return super.getItemViewType(position);
709             }
710 
711             @Override
712             public long getItemId(int position) {
713                 itemIdQueries.add(position);
714                 return mItems.get(position).mId;
715             }
716         };
717         adapter.setHasStableIds(stableIds);
718         setupBasic(10, 0, 10, adapter);
719         assertEquals("getItemViewType for all items should be called", 10,
720                 itemViewTypeQueries.size());
721         if (adapter.hasStableIds()) {
722             assertEquals("getItemId should be called when adapter has stable ids", 10,
723                     itemIdQueries.size());
724         } else {
725             assertEquals("getItemId should not be called when adapter does not have stable ids", 0,
726                     itemIdQueries.size());
727         }
728         itemViewTypeQueries.clear();
729         itemIdQueries.clear();
730         mLayoutManager.expectLayouts(2);
731         // delete last two
732         final int deleteStart = 8;
733         final int deleteCount = adapter.getItemCount() - deleteStart;
734         adapter.deleteAndNotify(deleteStart, deleteCount);
735         mLayoutManager.waitForLayout(2);
736         for (int i = 0; i < deleteStart; i++) {
737             assertTrue("getItemViewType for existing item " + i + " should be called",
738                     itemViewTypeQueries.contains(i));
739             if (adapter.hasStableIds()) {
740                 assertTrue("getItemId for existing item " + i
741                         + " should be called when adapter has stable ids",
742                         itemIdQueries.contains(i));
743             }
744         }
745         for (int i = deleteStart; i < deleteStart + deleteCount; i++) {
746             assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called",
747                     itemViewTypeQueries.contains(i));
748             if (adapter.hasStableIds()) {
749                 assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called",
750                         itemIdQueries.contains(i));
751             }
752         }
753     }
754 
testDeleteInvisibleMultiStep()755     public void testDeleteInvisibleMultiStep() throws Throwable {
756         setupBasic(1000, 1, 7);
757         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
758         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
759         mLayoutManager.expectLayouts(1);
760         // try to trigger race conditions
761         int targetItemCount = mTestAdapter.getItemCount();
762         for (int i = 0; i < 100; i++) {
763             mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});
764             checkForMainThreadException();
765             targetItemCount -= 2;
766         }
767         // wait until main thread runnables are consumed
768         while (targetItemCount != mTestAdapter.getItemCount()) {
769             Thread.sleep(100);
770         }
771         mLayoutManager.waitForLayout(2);
772     }
773 
testAddManyMultiStep()774     public void testAddManyMultiStep() throws Throwable {
775         setupBasic(10, 1, 7);
776         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
777         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
778         mLayoutManager.expectLayouts(1);
779         // try to trigger race conditions
780         int targetItemCount = mTestAdapter.getItemCount();
781         for (int i = 0; i < 100; i++) {
782             mTestAdapter.addAndNotify(0, 1);
783             mTestAdapter.addAndNotify(7, 1);
784             targetItemCount += 2;
785         }
786         // wait until main thread runnables are consumed
787         while (targetItemCount != mTestAdapter.getItemCount()) {
788             Thread.sleep(100);
789         }
790         mLayoutManager.waitForLayout(2);
791     }
792 
testBasicDelete()793     public void testBasicDelete() throws Throwable {
794         setupBasic(10);
795         final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() {
796             @Override
797             public void postDispatchLayout() {
798                 // verify this only in first layout
799                 assertEquals("deleted views should still be children of RV",
800                         mLayoutManager.getChildCount() + mDeletedViewCount
801                         , mRecyclerView.getChildCount());
802             }
803 
804             @Override
805             void afterPreLayout(RecyclerView.Recycler recycler,
806                     AnimationLayoutManager layoutManager,
807                     RecyclerView.State state) {
808                 super.afterPreLayout(recycler, layoutManager, state);
809                 mLayoutItemCount = 3;
810                 mLayoutMin = 0;
811             }
812         };
813         callbacks.mLayoutItemCount = 10;
814         callbacks.setExpectedItemCounts(10, 3);
815         mLayoutManager.setOnLayoutCallbacks(callbacks);
816 
817         mLayoutManager.expectLayouts(2);
818         mTestAdapter.deleteAndNotify(0, 7);
819         mLayoutManager.waitForLayout(2);
820         callbacks.reset();// when animations end another layout will happen
821     }
822 
823 
testAdapterChangeDuringScrolling()824     public void testAdapterChangeDuringScrolling() throws Throwable {
825         setupBasic(10);
826         final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
827         final AtomicInteger onScrollItemCount = new AtomicInteger(0);
828 
829         mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
830             @Override
831             void onLayoutChildren(RecyclerView.Recycler recycler,
832                     AnimationLayoutManager lm, RecyclerView.State state) {
833                 onLayoutItemCount.set(state.getItemCount());
834                 super.onLayoutChildren(recycler, lm, state);
835             }
836 
837             @Override
838             public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
839                 onScrollItemCount.set(state.getItemCount());
840                 super.onScroll(dx, recycler, state);
841             }
842         });
843         runTestOnUiThread(new Runnable() {
844             @Override
845             public void run() {
846                 mTestAdapter.mItems.remove(5);
847                 mTestAdapter.notifyItemRangeRemoved(5, 1);
848                 mRecyclerView.scrollBy(0, 100);
849                 assertTrue("scrolling while there are pending adapter updates should "
850                         + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
851                 assertEquals("scroll by should be called w/ updated adapter count",
852                         mTestAdapter.mItems.size(), onScrollItemCount.get());
853 
854             }
855         });
856     }
857 
testNotifyDataSetChangedDuringScroll()858     public void testNotifyDataSetChangedDuringScroll() throws Throwable {
859         setupBasic(10);
860         final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
861         final AtomicInteger onScrollItemCount = new AtomicInteger(0);
862 
863         mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
864             @Override
865             void onLayoutChildren(RecyclerView.Recycler recycler,
866                     AnimationLayoutManager lm, RecyclerView.State state) {
867                 onLayoutItemCount.set(state.getItemCount());
868                 super.onLayoutChildren(recycler, lm, state);
869             }
870 
871             @Override
872             public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
873                 onScrollItemCount.set(state.getItemCount());
874                 super.onScroll(dx, recycler, state);
875             }
876         });
877         runTestOnUiThread(new Runnable() {
878             @Override
879             public void run() {
880                 mTestAdapter.mItems.remove(5);
881                 mTestAdapter.notifyDataSetChanged();
882                 mRecyclerView.scrollBy(0, 100);
883                 assertTrue("scrolling while there are pending adapter updates should "
884                         + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
885                 assertEquals("scroll by should be called w/ updated adapter count",
886                         mTestAdapter.mItems.size(), onScrollItemCount.get());
887 
888             }
889         });
890     }
891 
testAddInvisibleAndVisible()892     public void testAddInvisibleAndVisible() throws Throwable {
893         setupBasic(10, 1, 7);
894         mLayoutManager.expectLayouts(2);
895         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
896         mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible
897         mLayoutManager.waitForLayout(2);
898     }
899 
testAddInvisible()900     public void testAddInvisible() throws Throwable {
901         setupBasic(10, 1, 7);
902         mLayoutManager.expectLayouts(1);
903         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
904         mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0
905         mLayoutManager.waitForLayout(2);
906     }
907 
testBasicAdd()908     public void testBasicAdd() throws Throwable {
909         setupBasic(10);
910         mLayoutManager.expectLayouts(2);
911         setExpectedItemCounts(10, 13);
912         mTestAdapter.addAndNotify(2, 3);
913         mLayoutManager.waitForLayout(2);
914     }
915 
testAppCancelAnimationInDetach()916     public void testAppCancelAnimationInDetach() throws Throwable {
917         final View[] addedView = new View[2];
918         TestAdapter adapter = new TestAdapter(1) {
919             @Override
920             public void onViewDetachedFromWindow(TestViewHolder holder) {
921                 if ((addedView[0] == holder.itemView || addedView[1] == holder.itemView)
922                         && ViewCompat.hasTransientState(holder.itemView)) {
923                     ViewCompat.animate(holder.itemView).cancel();
924                 }
925                 super.onViewDetachedFromWindow(holder);
926             }
927         };
928         // original 1 item
929         setupBasic(1, 0, 1, adapter);
930         mRecyclerView.getItemAnimator().setAddDuration(10000);
931         mLayoutManager.expectLayouts(2);
932         // add 2 items
933         setExpectedItemCounts(1, 3);
934         mTestAdapter.addAndNotify(0, 2);
935         mLayoutManager.waitForLayout(2, false);
936         checkForMainThreadException();
937         // wait till "add animation" starts
938         int limit = 200;
939         while (addedView[0] == null || addedView[1] == null) {
940             Thread.sleep(100);
941             runTestOnUiThread(new Runnable() {
942                 @Override
943                 public void run() {
944                     if (mRecyclerView.getChildCount() == 3) {
945                         View view = mRecyclerView.getChildAt(0);
946                         if (ViewCompat.hasTransientState(view)) {
947                             addedView[0] = view;
948                         }
949                         view = mRecyclerView.getChildAt(1);
950                         if (ViewCompat.hasTransientState(view)) {
951                             addedView[1] = view;
952                         }
953                     }
954                 }
955             });
956             assertTrue("add should start on time", --limit > 0);
957         }
958 
959         // Layout from item2, exclude the current adding items
960         mLayoutManager.expectLayouts(1);
961         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
962             @Override
963             void beforePostLayout(RecyclerView.Recycler recycler,
964                     AnimationLayoutManager layoutManager,
965                     RecyclerView.State state) {
966                 mLayoutMin = 2;
967                 mLayoutItemCount = 1;
968             }
969         };
970         requestLayoutOnUIThread(mRecyclerView);
971         mLayoutManager.waitForLayout(2);
972     }
973 
testAdapterChangeFrozen()974     public void testAdapterChangeFrozen() throws Throwable {
975         setupBasic(10, 1, 7);
976         assertTrue(mRecyclerView.getChildCount() == 7);
977 
978         mLayoutManager.expectLayouts(2);
979         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
980         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
981         freezeLayout(true);
982         mTestAdapter.addAndNotify(0, 1);
983 
984         mLayoutManager.assertNoLayout("RV should keep old child during frozen", 2);
985         assertEquals(7, mRecyclerView.getChildCount());
986 
987         freezeLayout(false);
988         mLayoutManager.waitForLayout(2);
989         assertEquals("RV should get updated after waken from frozen",
990                 8, mRecyclerView.getChildCount());
991     }
992 
getTestRecyclerView()993     public TestRecyclerView getTestRecyclerView() {
994         return (TestRecyclerView) mRecyclerView;
995     }
996 
testRemoveScrapInvalidate()997     public void testRemoveScrapInvalidate() throws Throwable {
998         setupBasic(10);
999         TestRecyclerView testRecyclerView = getTestRecyclerView();
1000         mLayoutManager.expectLayouts(1);
1001         testRecyclerView.expectDraw(1);
1002         runTestOnUiThread(new Runnable() {
1003             @Override
1004             public void run() {
1005                 mTestAdapter.mItems.clear();
1006                 mTestAdapter.notifyDataSetChanged();
1007             }
1008         });
1009         mLayoutManager.waitForLayout(2);
1010         testRecyclerView.waitForDraw(2);
1011     }
1012 
testDeleteVisibleAndInvisible()1013     public void testDeleteVisibleAndInvisible() throws Throwable {
1014         setupBasic(11, 3, 5); //layout items  3 4 5 6 7
1015         mLayoutManager.expectLayouts(2);
1016         setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list
1017         setExpectedItemCounts(9, 8);
1018         mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
1019         mLayoutManager.waitForLayout(2);
1020     }
1021 
testFindPositionOffset()1022     public void testFindPositionOffset() throws Throwable {
1023         setupBasic(10);
1024         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
1025             @Override
1026             void beforePreLayout(RecyclerView.Recycler recycler,
1027                     AnimationLayoutManager lm, RecyclerView.State state) {
1028                 super.beforePreLayout(recycler, lm, state);
1029                 // [0,2,4]
1030                 assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0));
1031                 assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2));
1032                 assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4));
1033             }
1034         };
1035         runTestOnUiThread(new Runnable() {
1036             @Override
1037             public void run() {
1038                 // [0,1,2,3,4]
1039                 // delete 1
1040                 mTestAdapter.notifyItemRangeRemoved(1, 1);
1041                 // delete 3
1042                 mTestAdapter.notifyItemRangeRemoved(2, 1);
1043             }
1044         });
1045         mLayoutManager.waitForLayout(2);
1046     }
1047 
setLayoutRange(int start, int count)1048     private void setLayoutRange(int start, int count) {
1049         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
1050         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
1051     }
1052 
setExpectedItemCounts(int preLayout, int postLayout)1053     private void setExpectedItemCounts(int preLayout, int postLayout) {
1054         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
1055     }
1056 
testDeleteInvisible()1057     public void testDeleteInvisible() throws Throwable {
1058         setupBasic(10, 1, 7);
1059         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
1060         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
1061         mLayoutManager.expectLayouts(1);
1062         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
1063         mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8
1064         mLayoutManager.waitForLayout(2);
1065     }
1066 
findByPos(RecyclerView recyclerView, RecyclerView.Recycler recycler, RecyclerView.State state, int position)1067     private CollectPositionResult findByPos(RecyclerView recyclerView,
1068             RecyclerView.Recycler recycler, RecyclerView.State state, int position) {
1069         View view = recycler.getViewForPosition(position, true);
1070         RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
1071         if (vh.wasReturnedFromScrap()) {
1072             vh.clearReturnedFromScrapFlag(); //keep data consistent.
1073             return CollectPositionResult.fromScrap(vh);
1074         } else {
1075             return CollectPositionResult.fromAdapter(vh);
1076         }
1077     }
1078 
collectPositions(RecyclerView recyclerView, RecyclerView.Recycler recycler, RecyclerView.State state, int... positions)1079     public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView,
1080             RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) {
1081         Map<Integer, CollectPositionResult> positionToAdapterMapping
1082                 = new HashMap<Integer, CollectPositionResult>();
1083         for (int position : positions) {
1084             if (position < 0) {
1085                 continue;
1086             }
1087             positionToAdapterMapping.put(position,
1088                     findByPos(recyclerView, recycler, state, position));
1089         }
1090         return positionToAdapterMapping;
1091     }
1092 
testAddDelete2()1093     public void testAddDelete2() throws Throwable {
1094         positionStatesTest(5, 0, 5, new AdapterOps() {
1095             // 0 1 2 3 4
1096             // 0 1 2 a b 3 4
1097             // 0 1 b 3 4
1098             // pre: 0 1 2 3 4
1099             // pre w/ adap: 0 1 2 b 3 4
1100             @Override
1101             void onRun(TestAdapter adapter) throws Throwable {
1102                 adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
1103             }
1104         }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
1105                 PositionConstraint.scrap(3, 3, 3)
1106         );
1107     }
1108 
testAddDelete1()1109     public void testAddDelete1() throws Throwable {
1110         positionStatesTest(5, 0, 5, new AdapterOps() {
1111             // 0 1 2 3 4
1112             // 0 1 2 a b 3 4
1113             // 0 2 a b 3 4
1114             // 0 c d 2 a b 3 4
1115             // 0 c d 2 a 4
1116             // c d 2 a 4
1117             // pre: 0 1 2 3 4
1118             @Override
1119             void onRun(TestAdapter adapter) throws Throwable {
1120                 adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
1121                         new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
1122             }
1123         }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
1124                 PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
1125                 PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
1126                 PositionConstraint.adapter(1), PositionConstraint.adapter(3)
1127         );
1128     }
1129 
testAddSameIndexTwice()1130     public void testAddSameIndexTwice() throws Throwable {
1131         positionStatesTest(12, 2, 7, new AdapterOps() {
1132             @Override
1133             void onRun(TestAdapter adapter) throws Throwable {
1134                 adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
1135                         new int[]{11, 1});
1136             }
1137         }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
1138                 PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
1139                 PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
1140                 PositionConstraint.scrap(8, 8, 13)
1141         );
1142     }
1143 
testDeleteTwice()1144     public void testDeleteTwice() throws Throwable {
1145         positionStatesTest(12, 2, 7, new AdapterOps() {
1146             @Override
1147             void onRun(TestAdapter adapter) throws Throwable {
1148                 adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
1149                         new int[]{0, 1});// delete item ids 0,2,9,1
1150             }
1151         }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
1152                 PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
1153                 PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
1154                 PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
1155         );
1156     }
1157 
1158 
positionStatesTest(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount, AdapterOps adapterChanges, final PositionConstraint... constraints)1159     public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
1160             int firstLayoutItemCount, AdapterOps adapterChanges,
1161             final PositionConstraint... constraints) throws Throwable {
1162         positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
1163                 adapterChanges,  constraints);
1164     }
positionStatesTest(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges, final PositionConstraint... constraints)1165     public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
1166             int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges,
1167             final PositionConstraint... constraints) throws Throwable {
1168         setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
1169         mLayoutManager.expectLayouts(2);
1170         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
1171             @Override
1172             void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1173                     RecyclerView.State state) {
1174                 super.beforePreLayout(recycler, lm, state);
1175                 //harmless
1176                 lm.detachAndScrapAttachedViews(recycler);
1177                 final int[] ids = new int[constraints.length];
1178                 for (int i = 0; i < constraints.length; i++) {
1179                     ids[i] = constraints[i].mPreLayoutPos;
1180                 }
1181                 Map<Integer, CollectPositionResult> positions
1182                         = collectPositions(lm.mRecyclerView, recycler, state, ids);
1183                 for (PositionConstraint constraint : constraints) {
1184                     if (constraint.mPreLayoutPos != -1) {
1185                         constraint.validate(state, positions.get(constraint.mPreLayoutPos),
1186                                 lm.getLog());
1187                     }
1188                 }
1189             }
1190 
1191             @Override
1192             void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1193                     RecyclerView.State state) {
1194                 super.beforePostLayout(recycler, lm, state);
1195                 lm.detachAndScrapAttachedViews(recycler);
1196                 final int[] ids = new int[constraints.length];
1197                 for (int i = 0; i < constraints.length; i++) {
1198                     ids[i] = constraints[i].mPostLayoutPos;
1199                 }
1200                 Map<Integer, CollectPositionResult> positions
1201                         = collectPositions(lm.mRecyclerView, recycler, state, ids);
1202                 for (PositionConstraint constraint : constraints) {
1203                     if (constraint.mPostLayoutPos >= 0) {
1204                         constraint.validate(state, positions.get(constraint.mPostLayoutPos),
1205                                 lm.getLog());
1206                     }
1207                 }
1208             }
1209         };
1210         adapterChanges.run(mTestAdapter);
1211         mLayoutManager.waitForLayout(2);
1212         checkForMainThreadException();
1213         for (PositionConstraint constraint : constraints) {
1214             constraint.assertValidate();
1215         }
1216     }
1217 
testAddThenRecycleRemovedView()1218     public void testAddThenRecycleRemovedView() throws Throwable {
1219         setupBasic(10);
1220         final AtomicInteger step = new AtomicInteger(0);
1221         final List<RecyclerView.ViewHolder> animateRemoveList = new ArrayList<RecyclerView.ViewHolder>();
1222         DefaultItemAnimator animator = new DefaultItemAnimator() {
1223             @Override
1224             public boolean animateRemove(RecyclerView.ViewHolder holder) {
1225                 animateRemoveList.add(holder);
1226                 return super.animateRemove(holder);
1227             }
1228         };
1229         mRecyclerView.setItemAnimator(animator);
1230         final List<RecyclerView.ViewHolder> pooledViews = new ArrayList<RecyclerView.ViewHolder>();
1231         mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() {
1232             @Override
1233             public void putRecycledView(RecyclerView.ViewHolder scrap) {
1234                 pooledViews.add(scrap);
1235                 super.putRecycledView(scrap);
1236             }
1237         });
1238         final RecyclerView.ViewHolder[] targetVh = new RecyclerView.ViewHolder[1];
1239         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
1240             @Override
1241             void doLayout(RecyclerView.Recycler recycler,
1242                     AnimationLayoutManager lm, RecyclerView.State state) {
1243                 switch (step.get()) {
1244                     case 1:
1245                         super.doLayout(recycler, lm, state);
1246                         if (state.isPreLayout()) {
1247                             View view = mLayoutManager.getChildAt(1);
1248                             RecyclerView.ViewHolder holder =
1249                                     mRecyclerView.getChildViewHolderInt(view);
1250                             targetVh[0] = holder;
1251                             assertTrue("test sanity", holder.isRemoved());
1252                             mLayoutManager.removeAndRecycleView(view, recycler);
1253                         }
1254                         break;
1255                 }
1256             }
1257         };
1258         step.set(1);
1259         animateRemoveList.clear();
1260         mLayoutManager.expectLayouts(2);
1261         mTestAdapter.deleteAndNotify(1, 1);
1262         mLayoutManager.waitForLayout(2);
1263         assertTrue("test sanity, view should be recycled", pooledViews.contains(targetVh[0]));
1264         assertTrue("since LM force recycled a view, animate disappearance should not be called",
1265                 animateRemoveList.isEmpty());
1266     }
1267 
1268     class AnimationLayoutManager extends TestLayoutManager {
1269 
1270         private int mTotalLayoutCount = 0;
1271         private String log;
1272 
1273         OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() {
1274         };
1275 
1276 
1277 
1278         @Override
supportsPredictiveItemAnimations()1279         public boolean supportsPredictiveItemAnimations() {
1280             return true;
1281         }
1282 
getLog()1283         public String getLog() {
1284             return log;
1285         }
1286 
prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done)1287         private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) {
1288             StringBuilder builder = new StringBuilder();
1289             builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done);
1290             builder.append("\nViewHolders:\n");
1291             for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) {
1292                 builder.append(vh).append("\n");
1293             }
1294             builder.append("scrap:\n");
1295             for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
1296                 builder.append(vh).append("\n");
1297             }
1298 
1299             if (state.isPreLayout() && !done) {
1300                 log = "\n" + builder.toString();
1301             } else {
1302                 log += "\n" + builder.toString();
1303             }
1304             return log;
1305         }
1306 
1307         @Override
expectLayouts(int count)1308         public void expectLayouts(int count) {
1309             super.expectLayouts(count);
1310             mOnLayoutCallbacks.mLayoutCount = 0;
1311         }
1312 
setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks)1313         public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) {
1314             mOnLayoutCallbacks = onLayoutCallbacks;
1315         }
1316 
1317         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)1318         public final void onLayoutChildren(RecyclerView.Recycler recycler,
1319                 RecyclerView.State state) {
1320             try {
1321                 mTotalLayoutCount++;
1322                 prepareLog(recycler, state, false);
1323                 if (state.isPreLayout()) {
1324                     validateOldPositions(recycler, state);
1325                 } else {
1326                     validateClearedOldPositions(recycler, state);
1327                 }
1328                 mOnLayoutCallbacks.onLayoutChildren(recycler, this, state);
1329                 prepareLog(recycler, state, true);
1330             } finally {
1331                 layoutLatch.countDown();
1332             }
1333         }
1334 
validateClearedOldPositions(RecyclerView.Recycler recycler, RecyclerView.State state)1335         private void validateClearedOldPositions(RecyclerView.Recycler recycler,
1336                 RecyclerView.State state) {
1337             if (getTestRecyclerView() == null) {
1338                 return;
1339             }
1340             for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
1341                 assertEquals("there should NOT be an old position in post layout",
1342                         RecyclerView.NO_POSITION, viewHolder.mOldPosition);
1343                 assertEquals("there should NOT be a pre layout position in post layout",
1344                         RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition);
1345             }
1346         }
1347 
validateOldPositions(RecyclerView.Recycler recycler, RecyclerView.State state)1348         private void validateOldPositions(RecyclerView.Recycler recycler,
1349                 RecyclerView.State state) {
1350             if (getTestRecyclerView() == null) {
1351                 return;
1352             }
1353             for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
1354                 if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) {
1355                     assertTrue("there should be an old position in pre-layout",
1356                             viewHolder.mOldPosition != RecyclerView.NO_POSITION);
1357                 }
1358             }
1359         }
1360 
getTotalLayoutCount()1361         public int getTotalLayoutCount() {
1362             return mTotalLayoutCount;
1363         }
1364 
1365         @Override
canScrollVertically()1366         public boolean canScrollVertically() {
1367             return true;
1368         }
1369 
1370         @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)1371         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
1372                 RecyclerView.State state) {
1373             mOnLayoutCallbacks.onScroll(dy, recycler, state);
1374             return super.scrollVerticallyBy(dy, recycler, state);
1375         }
1376 
onPostDispatchLayout()1377         public void onPostDispatchLayout() {
1378             mOnLayoutCallbacks.postDispatchLayout();
1379         }
1380 
1381         @Override
waitForLayout(long timeout, TimeUnit timeUnit)1382         public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
1383             super.waitForLayout(timeout, timeUnit);
1384             checkForMainThreadException();
1385         }
1386     }
1387 
1388     abstract class OnLayoutCallbacks {
1389 
1390         int mLayoutMin = Integer.MIN_VALUE;
1391 
1392         int mLayoutItemCount = Integer.MAX_VALUE;
1393 
1394         int expectedPreLayoutItemCount = -1;
1395 
1396         int expectedPostLayoutItemCount = -1;
1397 
1398         int mDeletedViewCount;
1399 
1400         int mLayoutCount = 0;
1401 
setExpectedItemCounts(int preLayout, int postLayout)1402         void setExpectedItemCounts(int preLayout, int postLayout) {
1403             expectedPreLayoutItemCount = preLayout;
1404             expectedPostLayoutItemCount = postLayout;
1405         }
1406 
reset()1407         void reset() {
1408             mLayoutMin = Integer.MIN_VALUE;
1409             mLayoutItemCount = Integer.MAX_VALUE;
1410             expectedPreLayoutItemCount = -1;
1411             expectedPostLayoutItemCount = -1;
1412             mLayoutCount = 0;
1413         }
1414 
beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, RecyclerView.State state)1415         void beforePreLayout(RecyclerView.Recycler recycler,
1416                 AnimationLayoutManager lm, RecyclerView.State state) {
1417             mDeletedViewCount = 0;
1418             for (int i = 0; i < lm.getChildCount(); i++) {
1419                 View v = lm.getChildAt(i);
1420                 if (lm.getLp(v).isItemRemoved()) {
1421                     mDeletedViewCount++;
1422                 }
1423             }
1424         }
1425 
doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, RecyclerView.State state)1426         void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1427                 RecyclerView.State state) {
1428             if (DEBUG) {
1429                 Log.d(TAG, "item count " + state.getItemCount());
1430             }
1431             lm.detachAndScrapAttachedViews(recycler);
1432             final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin;
1433             final int count = mLayoutItemCount
1434                     == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount;
1435             lm.layoutRange(recycler, start, start + count);
1436             assertEquals("correct # of children should be laid out",
1437                     count, lm.getChildCount());
1438             lm.assertVisibleItemPositions();
1439         }
1440 
assertNoPreLayoutPosition(RecyclerView.Recycler recycler)1441         private void assertNoPreLayoutPosition(RecyclerView.Recycler recycler) {
1442             for (RecyclerView.ViewHolder vh : recycler.mAttachedScrap) {
1443                 assertPreLayoutPosition(vh);
1444             }
1445         }
1446 
assertNoPreLayoutPosition(RecyclerView.LayoutManager lm)1447         private void assertNoPreLayoutPosition(RecyclerView.LayoutManager lm) {
1448             for (int i = 0; i < lm.getChildCount(); i ++) {
1449                 final RecyclerView.ViewHolder vh = mRecyclerView
1450                         .getChildViewHolder(lm.getChildAt(i));
1451                 assertPreLayoutPosition(vh);
1452             }
1453         }
1454 
assertPreLayoutPosition(RecyclerView.ViewHolder vh)1455         private void assertPreLayoutPosition(RecyclerView.ViewHolder vh) {
1456             assertEquals("in post layout, there should not be a view holder w/ a pre "
1457                     + "layout position", RecyclerView.NO_POSITION, vh.mPreLayoutPosition);
1458             assertEquals("in post layout, there should not be a view holder w/ an old "
1459                     + "layout position", RecyclerView.NO_POSITION, vh.mOldPosition);
1460         }
1461 
onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm, RecyclerView.State state)1462         void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1463                 RecyclerView.State state) {
1464 
1465             if (state.isPreLayout()) {
1466                 if (expectedPreLayoutItemCount != -1) {
1467                     assertEquals("on pre layout, state should return abstracted adapter size",
1468                             expectedPreLayoutItemCount, state.getItemCount());
1469                 }
1470                 beforePreLayout(recycler, lm, state);
1471             } else {
1472                 if (expectedPostLayoutItemCount != -1) {
1473                     assertEquals("on post layout, state should return real adapter size",
1474                             expectedPostLayoutItemCount, state.getItemCount());
1475                 }
1476                 beforePostLayout(recycler, lm, state);
1477             }
1478             if (!state.isPreLayout()) {
1479                 assertNoPreLayoutPosition(recycler);
1480             }
1481             doLayout(recycler, lm, state);
1482             if (state.isPreLayout()) {
1483                 afterPreLayout(recycler, lm, state);
1484             } else {
1485                 afterPostLayout(recycler, lm, state);
1486                 assertNoPreLayoutPosition(lm);
1487             }
1488             mLayoutCount++;
1489         }
1490 
afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, RecyclerView.State state)1491         void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1492                 RecyclerView.State state) {
1493         }
1494 
beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, RecyclerView.State state)1495         void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1496                 RecyclerView.State state) {
1497         }
1498 
afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, RecyclerView.State state)1499         void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1500                 RecyclerView.State state) {
1501         }
1502 
postDispatchLayout()1503         void postDispatchLayout() {
1504         }
1505 
onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)1506         public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
1507 
1508         }
1509     }
1510 
1511     class TestRecyclerView extends RecyclerView {
1512 
1513         CountDownLatch drawLatch;
1514 
TestRecyclerView(Context context)1515         public TestRecyclerView(Context context) {
1516             super(context);
1517         }
1518 
TestRecyclerView(Context context, AttributeSet attrs)1519         public TestRecyclerView(Context context, AttributeSet attrs) {
1520             super(context, attrs);
1521         }
1522 
TestRecyclerView(Context context, AttributeSet attrs, int defStyle)1523         public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) {
1524             super(context, attrs, defStyle);
1525         }
1526 
1527         @Override
initAdapterManager()1528         void initAdapterManager() {
1529             super.initAdapterManager();
1530             mAdapterHelper.mOnItemProcessedCallback = new Runnable() {
1531                 @Override
1532                 public void run() {
1533                     validatePostUpdateOp();
1534                 }
1535             };
1536         }
1537 
1538         @Override
isAccessibilityEnabled()1539         boolean isAccessibilityEnabled() {
1540             return true;
1541         }
1542 
expectDraw(int count)1543         public void expectDraw(int count) {
1544             drawLatch = new CountDownLatch(count);
1545         }
1546 
waitForDraw(long timeout)1547         public void waitForDraw(long timeout) throws Throwable {
1548             drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS);
1549             assertEquals("all expected draws should happen at the expected time frame",
1550                     0, drawLatch.getCount());
1551         }
1552 
collectViewHolders()1553         List<ViewHolder> collectViewHolders() {
1554             List<ViewHolder> holders = new ArrayList<ViewHolder>();
1555             final int childCount = getChildCount();
1556             for (int i = 0; i < childCount; i++) {
1557                 ViewHolder holder = getChildViewHolderInt(getChildAt(i));
1558                 if (holder != null) {
1559                     holders.add(holder);
1560                 }
1561             }
1562             return holders;
1563         }
1564 
1565 
validateViewHolderPositions()1566         private void validateViewHolderPositions() {
1567             final Set<Integer> existingOffsets = new HashSet<Integer>();
1568             int childCount = getChildCount();
1569             StringBuilder log = new StringBuilder();
1570             for (int i = 0; i < childCount; i++) {
1571                 ViewHolder vh = getChildViewHolderInt(getChildAt(i));
1572                 TestViewHolder tvh = (TestViewHolder) vh;
1573                 log.append(tvh.mBoundItem).append(vh)
1574                         .append(" hidden:")
1575                         .append(mChildHelper.mHiddenViews.contains(vh.itemView))
1576                         .append("\n");
1577             }
1578             for (int i = 0; i < childCount; i++) {
1579                 ViewHolder vh = getChildViewHolderInt(getChildAt(i));
1580                 if (vh.isInvalid()) {
1581                     continue;
1582                 }
1583                 if (vh.getLayoutPosition() < 0) {
1584                     LayoutManager lm = getLayoutManager();
1585                     for (int j = 0; j < lm.getChildCount(); j ++) {
1586                         assertNotSame("removed view holder should not be in LM's child list",
1587                                 vh.itemView, lm.getChildAt(j));
1588                     }
1589                 } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) {
1590                     if (!existingOffsets.add(vh.getLayoutPosition())) {
1591                         throw new IllegalStateException("view holder position conflict for "
1592                                 + "existing views " + vh + "\n" + log);
1593                     }
1594                 }
1595             }
1596         }
1597 
validatePostUpdateOp()1598         void validatePostUpdateOp() {
1599             try {
1600                 validateViewHolderPositions();
1601                 if (super.mState.isPreLayout()) {
1602                     validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager());
1603                 }
1604                 validateAdapterPosition((AnimationLayoutManager) getLayoutManager());
1605             } catch (Throwable t) {
1606                 postExceptionToInstrumentation(t);
1607             }
1608         }
1609 
1610 
1611 
validateAdapterPosition(AnimationLayoutManager lm)1612         private void validateAdapterPosition(AnimationLayoutManager lm) {
1613             for (ViewHolder vh : collectViewHolders()) {
1614                 if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) {
1615                     assertEquals("adapter position calculations should match view holder "
1616                             + "pre layout:" + mState.isPreLayout()
1617                             + " positions\n" + vh + "\n" + lm.getLog(),
1618                             mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition);
1619                 }
1620             }
1621         }
1622 
1623         // ensures pre layout positions are continuous block. This is not necessarily a case
1624         // but valid in test RV
validatePreLayoutSequence(AnimationLayoutManager lm)1625         private void validatePreLayoutSequence(AnimationLayoutManager lm) {
1626             Set<Integer> preLayoutPositions = new HashSet<Integer>();
1627             for (ViewHolder vh : collectViewHolders()) {
1628                 assertTrue("pre layout positions should be distinct " + lm.getLog(),
1629                         preLayoutPositions.add(vh.mPreLayoutPosition));
1630             }
1631             int minPos = Integer.MAX_VALUE;
1632             for (Integer pos : preLayoutPositions) {
1633                 if (pos < minPos) {
1634                     minPos = pos;
1635                 }
1636             }
1637             for (int i = 1; i < preLayoutPositions.size(); i++) {
1638                 assertNotNull("next position should exist " + lm.getLog(),
1639                         preLayoutPositions.contains(minPos + i));
1640             }
1641         }
1642 
1643         @Override
dispatchDraw(Canvas canvas)1644         protected void dispatchDraw(Canvas canvas) {
1645             super.dispatchDraw(canvas);
1646             if (drawLatch != null) {
1647                 drawLatch.countDown();
1648             }
1649         }
1650 
1651         @Override
dispatchLayout()1652         void dispatchLayout() {
1653             try {
1654                 super.dispatchLayout();
1655                 if (getLayoutManager() instanceof AnimationLayoutManager) {
1656                     ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout();
1657                 }
1658             } catch (Throwable t) {
1659                 postExceptionToInstrumentation(t);
1660             }
1661 
1662         }
1663 
1664 
1665     }
1666 
1667     abstract class AdapterOps {
1668 
run(TestAdapter adapter)1669         final public void run(TestAdapter adapter) throws Throwable {
1670             onRun(adapter);
1671         }
1672 
onRun(TestAdapter testAdapter)1673         abstract void onRun(TestAdapter testAdapter) throws Throwable;
1674     }
1675 
1676     static class CollectPositionResult {
1677 
1678         // true if found in scrap
1679         public RecyclerView.ViewHolder scrapResult;
1680 
1681         public RecyclerView.ViewHolder adapterResult;
1682 
fromScrap(RecyclerView.ViewHolder viewHolder)1683         static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) {
1684             CollectPositionResult cpr = new CollectPositionResult();
1685             cpr.scrapResult = viewHolder;
1686             return cpr;
1687         }
1688 
fromAdapter(RecyclerView.ViewHolder viewHolder)1689         static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) {
1690             CollectPositionResult cpr = new CollectPositionResult();
1691             cpr.adapterResult = viewHolder;
1692             return cpr;
1693         }
1694     }
1695 
1696     static class PositionConstraint {
1697 
1698         public static enum Type {
1699             scrap,
1700             adapter,
1701             adapterScrap /*first pass adapter, second pass scrap*/
1702         }
1703 
1704         Type mType;
1705 
1706         int mOldPos; // if VH
1707 
1708         int mPreLayoutPos;
1709 
1710         int mPostLayoutPos;
1711 
1712         int mValidateCount = 0;
1713 
scrap(int oldPos, int preLayoutPos, int postLayoutPos)1714         public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) {
1715             PositionConstraint constraint = new PositionConstraint();
1716             constraint.mType = Type.scrap;
1717             constraint.mOldPos = oldPos;
1718             constraint.mPreLayoutPos = preLayoutPos;
1719             constraint.mPostLayoutPos = postLayoutPos;
1720             return constraint;
1721         }
1722 
adapterScrap(int preLayoutPos, int position)1723         public static PositionConstraint adapterScrap(int preLayoutPos, int position) {
1724             PositionConstraint constraint = new PositionConstraint();
1725             constraint.mType = Type.adapterScrap;
1726             constraint.mOldPos = RecyclerView.NO_POSITION;
1727             constraint.mPreLayoutPos = preLayoutPos;
1728             constraint.mPostLayoutPos = position;// adapter pos does not change
1729             return constraint;
1730         }
1731 
adapter(int position)1732         public static PositionConstraint adapter(int position) {
1733             PositionConstraint constraint = new PositionConstraint();
1734             constraint.mType = Type.adapter;
1735             constraint.mPreLayoutPos = RecyclerView.NO_POSITION;
1736             constraint.mOldPos = RecyclerView.NO_POSITION;
1737             constraint.mPostLayoutPos = position;// adapter pos does not change
1738             return constraint;
1739         }
1740 
assertValidate()1741         public void assertValidate() {
1742             int expectedValidate = 0;
1743             if (mPreLayoutPos >= 0) {
1744                 expectedValidate ++;
1745             }
1746             if (mPostLayoutPos >= 0) {
1747                 expectedValidate ++;
1748             }
1749             assertEquals("should run all validates", expectedValidate, mValidateCount);
1750         }
1751 
1752         @Override
toString()1753         public String toString() {
1754             return "Cons{" +
1755                     "t=" + mType.name() +
1756                     ", old=" + mOldPos +
1757                     ", pre=" + mPreLayoutPos +
1758                     ", post=" + mPostLayoutPos +
1759                     '}';
1760         }
1761 
validate(RecyclerView.State state, CollectPositionResult result, String log)1762         public void validate(RecyclerView.State state, CollectPositionResult result, String log) {
1763             mValidateCount ++;
1764             assertNotNull(this + ": result should not be null\n" + log, result);
1765             RecyclerView.ViewHolder viewHolder;
1766             if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) {
1767                 assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult);
1768                 viewHolder = result.scrapResult;
1769             } else {
1770                 assertNotNull(this + ": result should come from adapter\n"  + log,
1771                         result.adapterResult);
1772                 assertEquals(this + ": old position should be none when it came from adapter\n" + log,
1773                         RecyclerView.NO_POSITION, result.adapterResult.getOldPosition());
1774                 viewHolder = result.adapterResult;
1775             }
1776             if (state.isPreLayout()) {
1777                 assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos,
1778                         viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition :
1779                         viewHolder.mPreLayoutPosition);
1780                 assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos,
1781                         viewHolder.getLayoutPosition());
1782                 if (mType == Type.scrap) {
1783                     assertEquals(this + ": old position should match\n" + log, mOldPos,
1784                             result.scrapResult.getOldPosition());
1785                 }
1786             } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult
1787                     .isRemoved()) {
1788                 assertEquals(this + ": post-layout position should match\n" + log + "\n\n"
1789                         + viewHolder, mPostLayoutPos, viewHolder.getLayoutPosition());
1790             }
1791         }
1792     }
1793 }
1794