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.graphics.Rect;
22import android.support.v4.view.ViewCompat;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.View;
26import android.view.ViewGroup;
27
28import java.util.ArrayList;
29import java.util.HashMap;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Map;
33import java.util.Set;
34import java.util.concurrent.CountDownLatch;
35import java.util.concurrent.TimeUnit;
36import java.util.concurrent.atomic.AtomicInteger;
37
38public 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
48    public RecyclerViewAnimationsTest() {
49        super(DEBUG);
50    }
51
52    @Override
53    protected void setUp() throws Exception {
54        super.setUp();
55    }
56
57    RecyclerView setupBasic(int itemCount) throws Throwable {
58        return setupBasic(itemCount, 0, itemCount);
59    }
60
61    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
62            throws Throwable {
63        return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null);
64    }
65
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
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
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
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
243    public void testImportantForAccessibilityWhileDetelingAuto() throws Throwable {
244        runTestImportantForAccessibilityWhileDeteling(
245                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO,
246                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
247    }
248
249    public void testImportantForAccessibilityWhileDetelingNo() throws Throwable {
250        runTestImportantForAccessibilityWhileDeteling(
251                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO,
252                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
253    }
254
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
261    public void testImportantForAccessibilityWhileDetelingYes() throws Throwable {
262        runTestImportantForAccessibilityWhileDeteling(
263                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
264                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
265    }
266
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
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
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    }
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
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
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
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
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
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
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
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
696    public void testGetItemForDeletedView() throws Throwable {
697        getItemForDeletedViewTest(false);
698        getItemForDeletedViewTest(true);
699    }
700
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
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
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
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
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
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
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
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
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
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
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
993    public TestRecyclerView getTestRecyclerView() {
994        return (TestRecyclerView) mRecyclerView;
995    }
996
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
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
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
1048    private void setLayoutRange(int start, int count) {
1049        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
1050        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
1051    }
1052
1053    private void setExpectedItemCounts(int preLayout, int postLayout) {
1054        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
1055    }
1056
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
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
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
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
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
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
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
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    }
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
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
1279        public boolean supportsPredictiveItemAnimations() {
1280            return true;
1281        }
1282
1283        public String getLog() {
1284            return log;
1285        }
1286
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
1308        public void expectLayouts(int count) {
1309            super.expectLayouts(count);
1310            mOnLayoutCallbacks.mLayoutCount = 0;
1311        }
1312
1313        public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) {
1314            mOnLayoutCallbacks = onLayoutCallbacks;
1315        }
1316
1317        @Override
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
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
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
1361        public int getTotalLayoutCount() {
1362            return mTotalLayoutCount;
1363        }
1364
1365        @Override
1366        public boolean canScrollVertically() {
1367            return true;
1368        }
1369
1370        @Override
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
1377        public void onPostDispatchLayout() {
1378            mOnLayoutCallbacks.postDispatchLayout();
1379        }
1380
1381        @Override
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
1402        void setExpectedItemCounts(int preLayout, int postLayout) {
1403            expectedPreLayoutItemCount = preLayout;
1404            expectedPostLayoutItemCount = postLayout;
1405        }
1406
1407        void reset() {
1408            mLayoutMin = Integer.MIN_VALUE;
1409            mLayoutItemCount = Integer.MAX_VALUE;
1410            expectedPreLayoutItemCount = -1;
1411            expectedPostLayoutItemCount = -1;
1412            mLayoutCount = 0;
1413        }
1414
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
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
1441        private void assertNoPreLayoutPosition(RecyclerView.Recycler recycler) {
1442            for (RecyclerView.ViewHolder vh : recycler.mAttachedScrap) {
1443                assertPreLayoutPosition(vh);
1444            }
1445        }
1446
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
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
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
1491        void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1492                RecyclerView.State state) {
1493        }
1494
1495        void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1496                RecyclerView.State state) {
1497        }
1498
1499        void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1500                RecyclerView.State state) {
1501        }
1502
1503        void postDispatchLayout() {
1504        }
1505
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
1515        public TestRecyclerView(Context context) {
1516            super(context);
1517        }
1518
1519        public TestRecyclerView(Context context, AttributeSet attrs) {
1520            super(context, attrs);
1521        }
1522
1523        public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) {
1524            super(context, attrs, defStyle);
1525        }
1526
1527        @Override
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
1539        boolean isAccessibilityEnabled() {
1540            return true;
1541        }
1542
1543        public void expectDraw(int count) {
1544            drawLatch = new CountDownLatch(count);
1545        }
1546
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
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
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
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
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
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
1644        protected void dispatchDraw(Canvas canvas) {
1645            super.dispatchDraw(canvas);
1646            if (drawLatch != null) {
1647                drawLatch.countDown();
1648            }
1649        }
1650
1651        @Override
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
1669        final public void run(TestAdapter adapter) throws Throwable {
1670            onRun(adapter);
1671        }
1672
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
1683        static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) {
1684            CollectPositionResult cpr = new CollectPositionResult();
1685            cpr.scrapResult = viewHolder;
1686            return cpr;
1687        }
1688
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
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
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
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
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
1753        public String toString() {
1754            return "Cons{" +
1755                    "t=" + mType.name() +
1756                    ", old=" + mOldPos +
1757                    ", pre=" + mPreLayoutPos +
1758                    ", post=" + mPostLayoutPos +
1759                    '}';
1760        }
1761
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