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