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
18import static android.support.v7.widget.RecyclerView.ItemAnimator.FLAG_CHANGED;
19import static android.support.v7.widget.RecyclerView.ItemAnimator.FLAG_MOVED;
20import static android.support.v7.widget.RecyclerView.ItemAnimator.FLAG_REMOVED;
21
22import static org.junit.Assert.assertEquals;
23import static org.junit.Assert.assertFalse;
24import static org.junit.Assert.assertNotNull;
25import static org.junit.Assert.assertNotSame;
26import static org.junit.Assert.assertNull;
27import static org.junit.Assert.assertSame;
28import static org.junit.Assert.assertThat;
29import static org.junit.Assert.assertTrue;
30import static org.junit.Assert.fail;
31
32import android.support.annotation.NonNull;
33import android.support.annotation.Nullable;
34import android.support.test.runner.AndroidJUnit4;
35import android.test.suitebuilder.annotation.MediumTest;
36import android.view.View;
37
38import org.hamcrest.CoreMatchers;
39import org.junit.Test;
40import org.junit.runner.RunWith;
41
42import java.util.ArrayList;
43import java.util.HashMap;
44import java.util.List;
45import java.util.Map;
46import java.util.concurrent.atomic.AtomicInteger;
47
48/**
49 * Includes tests for the new RecyclerView animations API (v2).
50 */
51@MediumTest
52@RunWith(AndroidJUnit4.class)
53public class ItemAnimatorV2ApiTest extends BaseRecyclerViewAnimationsTest {
54    @Override
55    protected RecyclerView.ItemAnimator createItemAnimator() {
56        return mAnimator;
57    }
58
59    @Test
60    public void changeMovedOutside() throws Throwable {
61        setupBasic(10);
62        final RecyclerView.ViewHolder target = mRecyclerView.findViewHolderForAdapterPosition(9);
63        mLayoutManager.expectLayouts(2);
64        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
65        mTestAdapter.changeAndNotify(9, 1);
66        mLayoutManager.waitForLayout(2);
67        // changed item should not be laid out and should just receive disappear
68        LoggingInfo pre = mAnimator.preLayoutInfoMap.get(target);
69        assertNotNull("test sanity", pre);
70        assertNull("test sanity", mAnimator.postLayoutInfoMap.get(target));
71        assertTrue(mAnimator.animateChangeList.isEmpty());
72        assertEquals(1, mAnimator.animateDisappearanceList.size());
73        assertEquals(new AnimateDisappearance(target, pre, null),
74                mAnimator.animateDisappearanceList.get(0));
75        // This is kind of problematic because layout manager will never layout the updated
76        // version of this view since it went out of bounds and it won't show up in scrap.
77        // I don't think we can do much better since other option is to bind a fresh view
78    }
79
80    @Test
81    public void changeMovedOutsideWithPredictiveAndTwoViewHolders() throws Throwable {
82        final RecyclerView.ViewHolder[] targets = new RecyclerView.ViewHolder[2];
83
84        setupBasic(10, 0, 10, new TestAdapter(10) {
85            @Override
86            public void onBindViewHolder(TestViewHolder holder,
87                    int position) {
88                super.onBindViewHolder(holder, position);
89                if (position == 0) {
90                    if (targets[0] == null) {
91                        targets[0] = holder;
92                    } else {
93                        assertThat(targets[1], CoreMatchers.nullValue());
94                        targets[1] = holder;
95                    }
96                }
97            }
98        });
99        final RecyclerView.ViewHolder singleItemTarget =
100                mRecyclerView.findViewHolderForAdapterPosition(1);
101        mAnimator.canReUseCallback = new CanReUseCallback() {
102            @Override
103            public boolean canReUse(RecyclerView.ViewHolder viewHolder, List<Object> payloads) {
104                return viewHolder == singleItemTarget;
105            }
106        };
107        mLayoutManager.expectLayouts(2);
108        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
109            @Override
110            void onLayoutChildren(RecyclerView.Recycler recycler,
111                    AnimationLayoutManager lm, RecyclerView.State state) {
112                super.onLayoutChildren(recycler, lm, state);
113                if (!state.isPreLayout()) {
114                    mLayoutManager.addDisappearingView(recycler.getViewForPosition(0));
115                    mLayoutManager.addDisappearingView(recycler.getScrapList().get(0).itemView);
116                }
117            }
118        };
119        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
120        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 2;
121        mTestAdapter.changeAndNotify(0, 2);
122        mLayoutManager.waitForLayout(2);
123        checkForMainThreadException();
124        final RecyclerView.ViewHolder oldTarget = targets[0];
125        final RecyclerView.ViewHolder newTarget = targets[1];
126        assertNotNull("test sanity", targets[0]);
127        assertNotNull("test sanity", targets[1]);
128        // changed item should not be laid out and should just receive disappear
129        LoggingInfo pre = mAnimator.preLayoutInfoMap.get(oldTarget);
130        assertNotNull("test sanity", pre);
131        assertNull("test sanity", mAnimator.postLayoutInfoMap.get(oldTarget));
132
133        assertNull("test sanity", mAnimator.preLayoutInfoMap.get(newTarget));
134        LoggingInfo post = mAnimator.postLayoutInfoMap.get(newTarget);
135        assertNotNull("test sanity", post);
136        assertEquals(1, mAnimator.animateChangeList.size());
137        assertEquals(1, mAnimator.animateDisappearanceList.size());
138
139        assertEquals(new AnimateChange(oldTarget, newTarget, pre, post),
140                mAnimator.animateChangeList.get(0));
141
142        LoggingInfo singleItemPre = mAnimator.preLayoutInfoMap.get(singleItemTarget);
143        assertNotNull("test sanity", singleItemPre);
144        LoggingInfo singleItemPost = mAnimator.postLayoutInfoMap.get(singleItemTarget);
145        assertNotNull("test sanity", singleItemPost);
146
147        assertEquals(new AnimateDisappearance(singleItemTarget, singleItemPre, singleItemPost),
148                mAnimator.animateDisappearanceList.get(0));
149    }
150    @Test
151    public void changeMovedOutsideWithPredictive() throws Throwable {
152        setupBasic(10);
153        final RecyclerView.ViewHolder target = mRecyclerView.findViewHolderForAdapterPosition(0);
154        mLayoutManager.expectLayouts(2);
155        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
156            @Override
157            void onLayoutChildren(RecyclerView.Recycler recycler,
158                    AnimationLayoutManager lm, RecyclerView.State state) {
159                super.onLayoutChildren(recycler, lm, state);
160                List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
161                assertThat(scrapList.size(), CoreMatchers.is(2));
162                mLayoutManager.addDisappearingView(scrapList.get(0).itemView);
163                mLayoutManager.addDisappearingView(scrapList.get(0).itemView);
164            }
165        };
166        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
167        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 2;
168        mTestAdapter.changeAndNotify(0, 2);
169        mLayoutManager.waitForLayout(2);
170        checkForMainThreadException();
171        // changed item should not be laid out and should just receive disappear
172        LoggingInfo pre = mAnimator.preLayoutInfoMap.get(target);
173        assertNotNull("test sanity", pre);
174        LoggingInfo postInfo = mAnimator.postLayoutInfoMap.get(target);
175        assertNotNull("test sanity", postInfo);
176        assertTrue(mAnimator.animateChangeList.isEmpty());
177        assertEquals(2, mAnimator.animateDisappearanceList.size());
178        try {
179            assertEquals(new AnimateDisappearance(target, pre, postInfo),
180                    mAnimator.animateDisappearanceList.get(0));
181        } catch (Throwable t) {
182            assertEquals(new AnimateDisappearance(target, pre, postInfo),
183                    mAnimator.animateDisappearanceList.get(1));
184        }
185
186    }
187
188    @Test
189    public void simpleAdd() throws Throwable {
190        setupBasic(10);
191        mLayoutManager.expectLayouts(2);
192        mTestAdapter.addAndNotify(2, 1);
193        mLayoutManager.waitForLayout(2);
194        RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
195        assertEquals(1, mAnimator.animateAppearanceList.size());
196        AnimateAppearance log = mAnimator.animateAppearanceList.get(0);
197        assertSame(vh, log.viewHolder);
198        assertNull(log.preInfo);
199        assertEquals(0, log.postInfo.changeFlags);
200        // the first two should not receive anything
201        for (int i = 0; i < 2; i++) {
202            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
203            assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
204        }
205        for (int i = 3; i < mTestAdapter.getItemCount(); i++) {
206            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
207            assertEquals(FLAG_MOVED, mAnimator.preLayoutInfoMap.get(other).changeFlags);
208        }
209        checkForMainThreadException();
210    }
211
212    @Test
213    public void simpleRemove() throws Throwable {
214        setupBasic(10);
215        RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
216        mLayoutManager.expectLayouts(2);
217        mTestAdapter.deleteAndNotify(2, 1);
218        mLayoutManager.waitForLayout(2);
219        checkForMainThreadException();
220        assertEquals(1, mAnimator.animateDisappearanceList.size());
221        AnimateDisappearance log = mAnimator.animateDisappearanceList.get(0);
222        assertSame(vh, log.viewHolder);
223        assertFalse(mAnimator.postLayoutInfoMap.containsKey(vh));
224        assertEquals(FLAG_REMOVED, log.preInfo.changeFlags);
225        // the first two should not receive anything
226        for (int i = 0; i < 2; i++) {
227            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
228            assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
229        }
230        for (int i = 3; i < mTestAdapter.getItemCount(); i++) {
231            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
232            assertEquals(FLAG_MOVED, mAnimator.preLayoutInfoMap.get(other).changeFlags);
233        }
234        checkForMainThreadException();
235    }
236
237    @Test
238    public void simpleUpdate() throws Throwable {
239        setupBasic(10);
240        RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
241        mLayoutManager.expectLayouts(2);
242        mTestAdapter.changeAndNotify(2, 1);
243        mLayoutManager.waitForLayout(2);
244        assertEquals(1, mAnimator.animateChangeList.size());
245        AnimateChange log = mAnimator.animateChangeList.get(0);
246        assertSame(vh, log.viewHolder);
247        assertSame(vh, log.newHolder);
248        assertTrue(mAnimator.preLayoutInfoMap.containsKey(vh));
249        assertTrue(mAnimator.postLayoutInfoMap.containsKey(vh));
250        assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
251        assertEquals(0, log.postInfo.changeFlags);
252        //others should not receive anything
253        for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
254            if (i == 2) {
255                continue;
256            }
257            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
258            assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
259        }
260        checkForMainThreadException();
261    }
262
263    @Test
264    public void updateWithDuplicateViewHolder() throws Throwable {
265        setupBasic(10);
266        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
267        mAnimator.canReUseCallback = new CanReUseCallback() {
268            @Override
269            public boolean canReUse(RecyclerView.ViewHolder viewHolder, List<Object> payloads) {
270                assertSame(viewHolder, vh);
271                return false;
272            }
273        };
274        mLayoutManager.expectLayouts(2);
275        mTestAdapter.changeAndNotify(2, 1);
276        mLayoutManager.waitForLayout(2);
277        final RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(2);
278        assertNotSame(vh, newVh);
279        assertEquals(1, mAnimator.animateChangeList.size());
280        AnimateChange log = mAnimator.animateChangeList.get(0);
281        assertSame(vh, log.viewHolder);
282        assertSame(newVh, log.newHolder);
283        assertNull(vh.itemView.getParent());
284        assertTrue(mAnimator.preLayoutInfoMap.containsKey(vh));
285        assertFalse(mAnimator.postLayoutInfoMap.containsKey(vh));
286        assertTrue(mAnimator.postLayoutInfoMap.containsKey(newVh));
287        assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
288        assertEquals(0, log.postInfo.changeFlags);
289        //others should not receive anything
290        for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
291            if (i == 2) {
292                continue;
293            }
294            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
295            assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
296        }
297        checkForMainThreadException();
298    }
299
300    @Test
301    public void updateWithOneDuplicateAndOneInPlace() throws Throwable {
302        setupBasic(10);
303        final RecyclerView.ViewHolder replaced = mRecyclerView.findViewHolderForAdapterPosition(2);
304        final RecyclerView.ViewHolder reused = mRecyclerView.findViewHolderForAdapterPosition(3);
305        mAnimator.canReUseCallback = new CanReUseCallback() {
306            @Override
307            public boolean canReUse(RecyclerView.ViewHolder viewHolder, List<Object> payloads) {
308                if (viewHolder == replaced) {
309                    return false;
310                } else if (viewHolder == reused) {
311                    return true;
312                }
313                fail("unpexpected view");
314                return false;
315            }
316        };
317        mLayoutManager.expectLayouts(2);
318        mTestAdapter.changeAndNotify(2, 2);
319        mLayoutManager.waitForLayout(2);
320        final RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(2);
321
322        assertNotSame(replaced, newVh);
323        assertSame(reused, mRecyclerView.findViewHolderForAdapterPosition(3));
324
325        assertEquals(2, mAnimator.animateChangeList.size());
326        AnimateChange logReplaced = null, logReused = null;
327        for (AnimateChange change : mAnimator.animateChangeList) {
328            if (change.newHolder == change.viewHolder) {
329                logReused = change;
330            } else {
331                logReplaced = change;
332            }
333        }
334        assertNotNull(logReplaced);
335        assertNotNull(logReused);
336        assertSame(replaced, logReplaced.viewHolder);
337        assertSame(newVh, logReplaced.newHolder);
338        assertSame(reused, logReused.viewHolder);
339        assertSame(reused, logReused.newHolder);
340
341        assertTrue(mAnimator.preLayoutInfoMap.containsKey(replaced));
342        assertTrue(mAnimator.preLayoutInfoMap.containsKey(reused));
343
344        assertTrue(mAnimator.postLayoutInfoMap.containsKey(newVh));
345        assertTrue(mAnimator.postLayoutInfoMap.containsKey(reused));
346        assertFalse(mAnimator.postLayoutInfoMap.containsKey(replaced));
347
348        assertEquals(FLAG_CHANGED, logReplaced.preInfo.changeFlags);
349        assertEquals(FLAG_CHANGED, logReused.preInfo.changeFlags);
350
351        assertEquals(0, logReplaced.postInfo.changeFlags);
352        assertEquals(0, logReused.postInfo.changeFlags);
353        //others should not receive anything
354        for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
355            if (i == 2 || i == 3) {
356                continue;
357            }
358            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
359            assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
360        }
361        checkForMainThreadException();
362    }
363
364    @Test
365    public void changeToDisappear() throws Throwable {
366        setupBasic(10);
367        RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(9);
368        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
369        mLayoutManager.expectLayouts(2);
370        mTestAdapter.changeAndNotify(9, 1);
371        mLayoutManager.waitForLayout(2);
372        assertEquals(1, mAnimator.animateDisappearanceList.size());
373        AnimateDisappearance log = mAnimator.animateDisappearanceList.get(0);
374        assertSame(vh, log.viewHolder);
375        assertFalse(mAnimator.postLayoutInfoMap.containsKey(vh));
376        assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
377        assertEquals(0, mAnimator.animateChangeList.size());
378        assertEquals(0, mAnimator.animateAppearanceList.size());
379        assertEquals(9, mAnimator.animatePersistenceList.size());
380        checkForMainThreadException();
381    }
382
383    @Test
384    public void changeToDisappearFromHead() throws Throwable {
385        setupBasic(10);
386        RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(0);
387        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
388        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
389        mLayoutManager.expectLayouts(2);
390        mTestAdapter.changeAndNotify(0, 1);
391        mLayoutManager.waitForLayout(2);
392        assertEquals(1, mAnimator.animateDisappearanceList.size());
393        AnimateDisappearance log = mAnimator.animateDisappearanceList.get(0);
394        assertSame(vh, log.viewHolder);
395        assertFalse(mAnimator.postLayoutInfoMap.containsKey(vh));
396        assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
397        assertEquals(0, mAnimator.animateChangeList.size());
398        assertEquals(0, mAnimator.animateAppearanceList.size());
399        assertEquals(9, mAnimator.animatePersistenceList.size());
400        checkForMainThreadException();
401    }
402
403    @Test
404    public void updatePayload() throws Throwable {
405        setupBasic(10);
406        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
407        final Object payload = new Object();
408        mAnimator.canReUseCallback = new CanReUseCallback() {
409            @Override
410            public boolean canReUse(RecyclerView.ViewHolder viewHolder, List<Object> payloads) {
411                assertSame(vh, viewHolder);
412                assertEquals(1, payloads.size());
413                assertSame(payload, payloads.get(0));
414                return true;
415            }
416        };
417        mLayoutManager.expectLayouts(2);
418        mTestAdapter.changeAndNotifyWithPayload(2, 1, payload);
419        mLayoutManager.waitForLayout(2);
420        assertEquals(1, mAnimator.animateChangeList.size());
421        AnimateChange log = mAnimator.animateChangeList.get(0);
422        assertSame(vh, log.viewHolder);
423        assertSame(vh, log.newHolder);
424        assertTrue(mAnimator.preLayoutInfoMap.containsKey(vh));
425        assertTrue(mAnimator.postLayoutInfoMap.containsKey(vh));
426        assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
427        assertEquals(0, log.postInfo.changeFlags);
428        assertNotNull(log.preInfo.payloads);
429        assertTrue(log.preInfo.payloads.contains(payload));
430        //others should not receive anything
431        for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
432            if (i == 2) {
433                continue;
434            }
435            RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
436            assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
437        }
438        checkForMainThreadException();
439    }
440
441    @Test
442    public void notifyDataSetChanged() throws Throwable {
443        TestAdapter adapter = new TestAdapter(10);
444        adapter.setHasStableIds(true);
445        setupBasic(10, 0, 10, adapter);
446        mLayoutManager.expectLayouts(1);
447        mTestAdapter.dispatchDataSetChanged();
448        mLayoutManager.waitForLayout(2);
449        assertEquals(10, mAnimator.animateChangeList.size());
450        for (AnimateChange change : mAnimator.animateChangeList) {
451            assertNotNull(change.preInfo);
452            assertNotNull(change.postInfo);
453            assertSame(change.preInfo.viewHolder, change.postInfo.viewHolder);
454        }
455        assertEquals(0, mAnimator.animatePersistenceList.size());
456        assertEquals(0, mAnimator.animateAppearanceList.size());
457        assertEquals(0, mAnimator.animateDisappearanceList.size());
458    }
459
460    @Test
461    public void notifyDataSetChangedWithoutStableIds() throws Throwable {
462        TestAdapter adapter = new TestAdapter(10);
463        adapter.setHasStableIds(false);
464        setupBasic(10, 0, 10, adapter);
465        mLayoutManager.expectLayouts(1);
466        mTestAdapter.dispatchDataSetChanged();
467        mLayoutManager.waitForLayout(2);
468        assertEquals(0, mAnimator.animateChangeList.size());
469        assertEquals(0, mAnimator.animatePersistenceList.size());
470        assertEquals(0, mAnimator.animateAppearanceList.size());
471        assertEquals(0, mAnimator.animateDisappearanceList.size());
472    }
473
474    @Test
475    public void notifyDataSetChangedWithAppearing() throws Throwable {
476        notifyDataSetChangedWithAppearing(false);
477    }
478
479    @Test
480    public void notifyDataSetChangedWithAppearingNotifyBoth() throws Throwable {
481        notifyDataSetChangedWithAppearing(true);
482    }
483
484    public void notifyDataSetChangedWithAppearing(final boolean notifyBoth) throws Throwable {
485        final TestAdapter adapter = new TestAdapter(10);
486        adapter.setHasStableIds(true);
487        setupBasic(10, 0, 10, adapter);
488        mLayoutManager.expectLayouts(1);
489        runTestOnUiThread(new Runnable() {
490            @Override
491            public void run() {
492                try {
493                    if (notifyBoth) {
494                        adapter.addAndNotify(2, 2);
495                    } else {
496                        adapter.mItems.add(2, new Item(2, "custom 1"));
497                        adapter.mItems.add(3, new Item(3, "custom 2"));
498                    }
499
500                    adapter.notifyDataSetChanged();
501                } catch (Throwable throwable) {
502                    throwable.printStackTrace();
503                }
504            }
505        });
506        mLayoutManager.waitForLayout(2);
507        assertEquals(10, mAnimator.animateChangeList.size());
508        assertEquals(0, mAnimator.animatePersistenceList.size());
509        assertEquals(2, mAnimator.animateAppearanceList.size());
510        assertEquals(0, mAnimator.animateDisappearanceList.size());
511    }
512
513    @Test
514    public void notifyDataSetChangedWithDispappearing() throws Throwable {
515        notifyDataSetChangedWithDispappearing(false);
516    }
517
518    @Test
519    public void notifyDataSetChangedWithDispappearingNotifyBoth() throws Throwable {
520        notifyDataSetChangedWithDispappearing(true);
521    }
522
523    public void notifyDataSetChangedWithDispappearing(final boolean notifyBoth) throws Throwable {
524        final TestAdapter adapter = new TestAdapter(10);
525        adapter.setHasStableIds(true);
526        setupBasic(10, 0, 10, adapter);
527        mLayoutManager.expectLayouts(1);
528        runTestOnUiThread(new Runnable() {
529            @Override
530            public void run() {
531                try {
532                    if (notifyBoth) {
533                        adapter.deleteAndNotify(2, 2);
534                    } else {
535                        adapter.mItems.remove(2);
536                        adapter.mItems.remove(2);
537                    }
538                    adapter.notifyDataSetChanged();
539                } catch (Throwable throwable) {
540                    throwable.printStackTrace();
541                }
542            }
543        });
544        mLayoutManager.waitForLayout(2);
545        assertEquals(8, mAnimator.animateChangeList.size());
546        assertEquals(0, mAnimator.animatePersistenceList.size());
547        assertEquals(0, mAnimator.animateAppearanceList.size());
548        assertEquals(2, mAnimator.animateDisappearanceList.size());
549    }
550
551    @Test
552    public void notifyUpdateWithChangedAdapterType() throws Throwable {
553        final AtomicInteger itemType = new AtomicInteger(1);
554        final TestAdapter adapter = new TestAdapter(10) {
555            @Override
556            public int getItemViewType(int position) {
557                return position == 2 ? itemType.get() : 20;
558            }
559        };
560        adapter.setHasStableIds(true);
561        setupBasic(10, 0, 10, adapter);
562        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
563
564        mAnimator.canReUseCallback = new CanReUseCallback() {
565            @Override
566            public boolean canReUse(RecyclerView.ViewHolder viewHolder, List<Object> payloads) {
567                return viewHolder != vh;
568            }
569        };
570
571        mLayoutManager.expectLayouts(1);
572        itemType.set(3);
573        adapter.dispatchDataSetChanged();
574        mLayoutManager.waitForLayout(2);
575        final RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(2);
576        // TODO we should be able to map old type to the new one but doing that change has some
577        // recycling side effects.
578        assertEquals(9, mAnimator.animateChangeList.size());
579        assertEquals(0, mAnimator.animatePersistenceList.size());
580        assertEquals(1, mAnimator.animateAppearanceList.size());
581        assertEquals(0, mAnimator.animateDisappearanceList.size());
582        assertNotSame(vh, newVh);
583        for (AnimateChange change : mAnimator.animateChangeList) {
584            if (change.viewHolder == vh) {
585                assertSame(change.newHolder, newVh);
586                assertSame(change.viewHolder, vh);
587            } else {
588                assertSame(change.newHolder, change.viewHolder);
589            }
590        }
591    }
592
593    LoggingV2Animator mAnimator = new LoggingV2Animator();
594
595    class LoggingV2Animator extends RecyclerView.ItemAnimator {
596
597        CanReUseCallback canReUseCallback = new CanReUseCallback() {
598            @Override
599            public boolean canReUse(RecyclerView.ViewHolder viewHolder, List<Object> payloads) {
600                return true;
601            }
602        };
603        Map<RecyclerView.ViewHolder, LoggingInfo> preLayoutInfoMap = new HashMap<>();
604        Map<RecyclerView.ViewHolder, LoggingInfo> postLayoutInfoMap = new HashMap<>();
605
606        List<AnimateAppearance> animateAppearanceList = new ArrayList<>();
607        List<AnimateDisappearance> animateDisappearanceList = new ArrayList<>();
608        List<AnimatePersistence> animatePersistenceList = new ArrayList<>();
609        List<AnimateChange> animateChangeList = new ArrayList<>();
610
611        @Override
612        public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder,
613                List<Object> payloads) {
614            return canReUseCallback.canReUse(viewHolder, payloads);
615        }
616
617        @NonNull
618        @Override
619        public ItemHolderInfo recordPreLayoutInformation(@NonNull RecyclerView.State state,
620                @NonNull RecyclerView.ViewHolder viewHolder,
621                @AdapterChanges int changeFlags, @NonNull List<Object> payloads) {
622            LoggingInfo loggingInfo = new LoggingInfo(viewHolder, changeFlags, payloads);
623            preLayoutInfoMap.put(viewHolder, loggingInfo);
624            return loggingInfo;
625        }
626
627        @NonNull
628        @Override
629        public ItemHolderInfo recordPostLayoutInformation(@NonNull RecyclerView.State state,
630                @NonNull RecyclerView.ViewHolder viewHolder) {
631            LoggingInfo loggingInfo = new LoggingInfo(viewHolder, 0, null);
632            postLayoutInfoMap.put(viewHolder, loggingInfo);
633            return loggingInfo;
634        }
635
636        @Override
637        public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder,
638                @NonNull ItemHolderInfo preLayoutInfo,
639                @Nullable ItemHolderInfo postLayoutInfo) {
640            animateDisappearanceList.add(new AnimateDisappearance(viewHolder,
641                    (LoggingInfo) preLayoutInfo, (LoggingInfo) postLayoutInfo));
642            assertSame(preLayoutInfoMap.get(viewHolder), preLayoutInfo);
643            assertSame(postLayoutInfoMap.get(viewHolder), postLayoutInfo);
644            dispatchAnimationFinished(viewHolder);
645
646            return false;
647        }
648
649        @Override
650        public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder,
651                ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
652            animateAppearanceList.add(
653                    new AnimateAppearance(viewHolder, (LoggingInfo) preInfo, (LoggingInfo) postInfo));
654            assertSame(preLayoutInfoMap.get(viewHolder), preInfo);
655            assertSame(postLayoutInfoMap.get(viewHolder), postInfo);
656            dispatchAnimationFinished(viewHolder);
657            return false;
658        }
659
660        @Override
661        public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,
662                @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
663            animatePersistenceList.add(new AnimatePersistence(viewHolder, (LoggingInfo) preInfo,
664                    (LoggingInfo) postInfo));
665            dispatchAnimationFinished(viewHolder);
666            assertSame(preLayoutInfoMap.get(viewHolder), preInfo);
667            assertSame(postLayoutInfoMap.get(viewHolder), postInfo);
668            return false;
669        }
670
671        @Override
672        public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder,
673                @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo,
674                @NonNull ItemHolderInfo postInfo) {
675            animateChangeList.add(new AnimateChange(oldHolder, newHolder, (LoggingInfo) preInfo,
676                    (LoggingInfo) postInfo));
677            if (oldHolder != null) {
678                dispatchAnimationFinished(oldHolder);
679                assertSame(preLayoutInfoMap.get(oldHolder), preInfo);
680            }
681            if (newHolder != null && oldHolder != newHolder) {
682                dispatchAnimationFinished(newHolder);
683                assertSame(postLayoutInfoMap.get(newHolder), postInfo);
684            }
685
686            return false;
687        }
688
689        @Override
690        public void runPendingAnimations() {
691
692        }
693
694        @Override
695        public void endAnimation(RecyclerView.ViewHolder item) {
696        }
697
698        @Override
699        public void endAnimations() {
700
701        }
702
703        @Override
704        public boolean isRunning() {
705            return false;
706        }
707    }
708
709    interface CanReUseCallback {
710
711        boolean canReUse(RecyclerView.ViewHolder viewHolder, List<Object> payloads);
712    }
713}
714