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