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 */
16package android.support.v7.widget;
17
18import android.support.annotation.NonNull;
19import android.support.v4.animation.AnimatorCompatHelper;
20import android.support.v4.view.ViewCompat;
21import android.support.v4.view.ViewPropertyAnimatorCompat;
22import android.support.v4.view.ViewPropertyAnimatorListener;
23import android.support.v7.widget.RecyclerView.ViewHolder;
24import android.view.View;
25
26import java.util.ArrayList;
27import java.util.List;
28
29/**
30 * This implementation of {@link RecyclerView.ItemAnimator} provides basic
31 * animations on remove, add, and move events that happen to the items in
32 * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
33 *
34 * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
35 */
36public class DefaultItemAnimator extends SimpleItemAnimator {
37    private static final boolean DEBUG = false;
38
39    private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
40    private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
41    private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
42    private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
43
44    private ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
45    private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
46    private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
47
48    private ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
49    private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
50    private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
51    private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
52
53    private static class MoveInfo {
54        public ViewHolder holder;
55        public int fromX, fromY, toX, toY;
56
57        private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
58            this.holder = holder;
59            this.fromX = fromX;
60            this.fromY = fromY;
61            this.toX = toX;
62            this.toY = toY;
63        }
64    }
65
66    private static class ChangeInfo {
67        public ViewHolder oldHolder, newHolder;
68        public int fromX, fromY, toX, toY;
69        private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
70            this.oldHolder = oldHolder;
71            this.newHolder = newHolder;
72        }
73
74        private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
75                int fromX, int fromY, int toX, int toY) {
76            this(oldHolder, newHolder);
77            this.fromX = fromX;
78            this.fromY = fromY;
79            this.toX = toX;
80            this.toY = toY;
81        }
82
83        @Override
84        public String toString() {
85            return "ChangeInfo{" +
86                    "oldHolder=" + oldHolder +
87                    ", newHolder=" + newHolder +
88                    ", fromX=" + fromX +
89                    ", fromY=" + fromY +
90                    ", toX=" + toX +
91                    ", toY=" + toY +
92                    '}';
93        }
94    }
95
96    @Override
97    public void runPendingAnimations() {
98        boolean removalsPending = !mPendingRemovals.isEmpty();
99        boolean movesPending = !mPendingMoves.isEmpty();
100        boolean changesPending = !mPendingChanges.isEmpty();
101        boolean additionsPending = !mPendingAdditions.isEmpty();
102        if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
103            // nothing to animate
104            return;
105        }
106        // First, remove stuff
107        for (ViewHolder holder : mPendingRemovals) {
108            animateRemoveImpl(holder);
109        }
110        mPendingRemovals.clear();
111        // Next, move stuff
112        if (movesPending) {
113            final ArrayList<MoveInfo> moves = new ArrayList<>();
114            moves.addAll(mPendingMoves);
115            mMovesList.add(moves);
116            mPendingMoves.clear();
117            Runnable mover = new Runnable() {
118                @Override
119                public void run() {
120                    for (MoveInfo moveInfo : moves) {
121                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
122                                moveInfo.toX, moveInfo.toY);
123                    }
124                    moves.clear();
125                    mMovesList.remove(moves);
126                }
127            };
128            if (removalsPending) {
129                View view = moves.get(0).holder.itemView;
130                ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
131            } else {
132                mover.run();
133            }
134        }
135        // Next, change stuff, to run in parallel with move animations
136        if (changesPending) {
137            final ArrayList<ChangeInfo> changes = new ArrayList<>();
138            changes.addAll(mPendingChanges);
139            mChangesList.add(changes);
140            mPendingChanges.clear();
141            Runnable changer = new Runnable() {
142                @Override
143                public void run() {
144                    for (ChangeInfo change : changes) {
145                        animateChangeImpl(change);
146                    }
147                    changes.clear();
148                    mChangesList.remove(changes);
149                }
150            };
151            if (removalsPending) {
152                ViewHolder holder = changes.get(0).oldHolder;
153                ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
154            } else {
155                changer.run();
156            }
157        }
158        // Next, add stuff
159        if (additionsPending) {
160            final ArrayList<ViewHolder> additions = new ArrayList<>();
161            additions.addAll(mPendingAdditions);
162            mAdditionsList.add(additions);
163            mPendingAdditions.clear();
164            Runnable adder = new Runnable() {
165                public void run() {
166                    for (ViewHolder holder : additions) {
167                        animateAddImpl(holder);
168                    }
169                    additions.clear();
170                    mAdditionsList.remove(additions);
171                }
172            };
173            if (removalsPending || movesPending || changesPending) {
174                long removeDuration = removalsPending ? getRemoveDuration() : 0;
175                long moveDuration = movesPending ? getMoveDuration() : 0;
176                long changeDuration = changesPending ? getChangeDuration() : 0;
177                long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
178                View view = additions.get(0).itemView;
179                ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
180            } else {
181                adder.run();
182            }
183        }
184    }
185
186    @Override
187    public boolean animateRemove(final ViewHolder holder) {
188        resetAnimation(holder);
189        mPendingRemovals.add(holder);
190        return true;
191    }
192
193    private void animateRemoveImpl(final ViewHolder holder) {
194        final View view = holder.itemView;
195        final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
196        mRemoveAnimations.add(holder);
197        animation.setDuration(getRemoveDuration())
198                .alpha(0).setListener(new VpaListenerAdapter() {
199            @Override
200            public void onAnimationStart(View view) {
201                dispatchRemoveStarting(holder);
202            }
203
204            @Override
205            public void onAnimationEnd(View view) {
206                animation.setListener(null);
207                ViewCompat.setAlpha(view, 1);
208                dispatchRemoveFinished(holder);
209                mRemoveAnimations.remove(holder);
210                dispatchFinishedWhenDone();
211            }
212        }).start();
213    }
214
215    @Override
216    public boolean animateAdd(final ViewHolder holder) {
217        resetAnimation(holder);
218        ViewCompat.setAlpha(holder.itemView, 0);
219        mPendingAdditions.add(holder);
220        return true;
221    }
222
223    private void animateAddImpl(final ViewHolder holder) {
224        final View view = holder.itemView;
225        final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
226        mAddAnimations.add(holder);
227        animation.alpha(1).setDuration(getAddDuration()).
228                setListener(new VpaListenerAdapter() {
229                    @Override
230                    public void onAnimationStart(View view) {
231                        dispatchAddStarting(holder);
232                    }
233                    @Override
234                    public void onAnimationCancel(View view) {
235                        ViewCompat.setAlpha(view, 1);
236                    }
237
238                    @Override
239                    public void onAnimationEnd(View view) {
240                        animation.setListener(null);
241                        dispatchAddFinished(holder);
242                        mAddAnimations.remove(holder);
243                        dispatchFinishedWhenDone();
244                    }
245                }).start();
246    }
247
248    @Override
249    public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
250            int toX, int toY) {
251        final View view = holder.itemView;
252        fromX += ViewCompat.getTranslationX(holder.itemView);
253        fromY += ViewCompat.getTranslationY(holder.itemView);
254        resetAnimation(holder);
255        int deltaX = toX - fromX;
256        int deltaY = toY - fromY;
257        if (deltaX == 0 && deltaY == 0) {
258            dispatchMoveFinished(holder);
259            return false;
260        }
261        if (deltaX != 0) {
262            ViewCompat.setTranslationX(view, -deltaX);
263        }
264        if (deltaY != 0) {
265            ViewCompat.setTranslationY(view, -deltaY);
266        }
267        mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
268        return true;
269    }
270
271    private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
272        final View view = holder.itemView;
273        final int deltaX = toX - fromX;
274        final int deltaY = toY - fromY;
275        if (deltaX != 0) {
276            ViewCompat.animate(view).translationX(0);
277        }
278        if (deltaY != 0) {
279            ViewCompat.animate(view).translationY(0);
280        }
281        // TODO: make EndActions end listeners instead, since end actions aren't called when
282        // vpas are canceled (and can't end them. why?)
283        // need listener functionality in VPACompat for this. Ick.
284        final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
285        mMoveAnimations.add(holder);
286        animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
287            @Override
288            public void onAnimationStart(View view) {
289                dispatchMoveStarting(holder);
290            }
291            @Override
292            public void onAnimationCancel(View view) {
293                if (deltaX != 0) {
294                    ViewCompat.setTranslationX(view, 0);
295                }
296                if (deltaY != 0) {
297                    ViewCompat.setTranslationY(view, 0);
298                }
299            }
300            @Override
301            public void onAnimationEnd(View view) {
302                animation.setListener(null);
303                dispatchMoveFinished(holder);
304                mMoveAnimations.remove(holder);
305                dispatchFinishedWhenDone();
306            }
307        }).start();
308    }
309
310    @Override
311    public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
312            int fromX, int fromY, int toX, int toY) {
313        if (oldHolder == newHolder) {
314            // Don't know how to run change animations when the same view holder is re-used.
315            // run a move animation to handle position changes.
316            return animateMove(oldHolder, fromX, fromY, toX, toY);
317        }
318        final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
319        final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
320        final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
321        resetAnimation(oldHolder);
322        int deltaX = (int) (toX - fromX - prevTranslationX);
323        int deltaY = (int) (toY - fromY - prevTranslationY);
324        // recover prev translation state after ending animation
325        ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
326        ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
327        ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
328        if (newHolder != null) {
329            // carry over translation values
330            resetAnimation(newHolder);
331            ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
332            ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
333            ViewCompat.setAlpha(newHolder.itemView, 0);
334        }
335        mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
336        return true;
337    }
338
339    private void animateChangeImpl(final ChangeInfo changeInfo) {
340        final ViewHolder holder = changeInfo.oldHolder;
341        final View view = holder == null ? null : holder.itemView;
342        final ViewHolder newHolder = changeInfo.newHolder;
343        final View newView = newHolder != null ? newHolder.itemView : null;
344        if (view != null) {
345            final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
346                    getChangeDuration());
347            mChangeAnimations.add(changeInfo.oldHolder);
348            oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
349            oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
350            oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
351                @Override
352                public void onAnimationStart(View view) {
353                    dispatchChangeStarting(changeInfo.oldHolder, true);
354                }
355
356                @Override
357                public void onAnimationEnd(View view) {
358                    oldViewAnim.setListener(null);
359                    ViewCompat.setAlpha(view, 1);
360                    ViewCompat.setTranslationX(view, 0);
361                    ViewCompat.setTranslationY(view, 0);
362                    dispatchChangeFinished(changeInfo.oldHolder, true);
363                    mChangeAnimations.remove(changeInfo.oldHolder);
364                    dispatchFinishedWhenDone();
365                }
366            }).start();
367        }
368        if (newView != null) {
369            final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
370            mChangeAnimations.add(changeInfo.newHolder);
371            newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
372                    alpha(1).setListener(new VpaListenerAdapter() {
373                @Override
374                public void onAnimationStart(View view) {
375                    dispatchChangeStarting(changeInfo.newHolder, false);
376                }
377                @Override
378                public void onAnimationEnd(View view) {
379                    newViewAnimation.setListener(null);
380                    ViewCompat.setAlpha(newView, 1);
381                    ViewCompat.setTranslationX(newView, 0);
382                    ViewCompat.setTranslationY(newView, 0);
383                    dispatchChangeFinished(changeInfo.newHolder, false);
384                    mChangeAnimations.remove(changeInfo.newHolder);
385                    dispatchFinishedWhenDone();
386                }
387            }).start();
388        }
389    }
390
391    private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
392        for (int i = infoList.size() - 1; i >= 0; i--) {
393            ChangeInfo changeInfo = infoList.get(i);
394            if (endChangeAnimationIfNecessary(changeInfo, item)) {
395                if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
396                    infoList.remove(changeInfo);
397                }
398            }
399        }
400    }
401
402    private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
403        if (changeInfo.oldHolder != null) {
404            endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
405        }
406        if (changeInfo.newHolder != null) {
407            endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
408        }
409    }
410    private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
411        boolean oldItem = false;
412        if (changeInfo.newHolder == item) {
413            changeInfo.newHolder = null;
414        } else if (changeInfo.oldHolder == item) {
415            changeInfo.oldHolder = null;
416            oldItem = true;
417        } else {
418            return false;
419        }
420        ViewCompat.setAlpha(item.itemView, 1);
421        ViewCompat.setTranslationX(item.itemView, 0);
422        ViewCompat.setTranslationY(item.itemView, 0);
423        dispatchChangeFinished(item, oldItem);
424        return true;
425    }
426
427    @Override
428    public void endAnimation(ViewHolder item) {
429        final View view = item.itemView;
430        // this will trigger end callback which should set properties to their target values.
431        ViewCompat.animate(view).cancel();
432        // TODO if some other animations are chained to end, how do we cancel them as well?
433        for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
434            MoveInfo moveInfo = mPendingMoves.get(i);
435            if (moveInfo.holder == item) {
436                ViewCompat.setTranslationY(view, 0);
437                ViewCompat.setTranslationX(view, 0);
438                dispatchMoveFinished(item);
439                mPendingMoves.remove(i);
440            }
441        }
442        endChangeAnimation(mPendingChanges, item);
443        if (mPendingRemovals.remove(item)) {
444            ViewCompat.setAlpha(view, 1);
445            dispatchRemoveFinished(item);
446        }
447        if (mPendingAdditions.remove(item)) {
448            ViewCompat.setAlpha(view, 1);
449            dispatchAddFinished(item);
450        }
451
452        for (int i = mChangesList.size() - 1; i >= 0; i--) {
453            ArrayList<ChangeInfo> changes = mChangesList.get(i);
454            endChangeAnimation(changes, item);
455            if (changes.isEmpty()) {
456                mChangesList.remove(i);
457            }
458        }
459        for (int i = mMovesList.size() - 1; i >= 0; i--) {
460            ArrayList<MoveInfo> moves = mMovesList.get(i);
461            for (int j = moves.size() - 1; j >= 0; j--) {
462                MoveInfo moveInfo = moves.get(j);
463                if (moveInfo.holder == item) {
464                    ViewCompat.setTranslationY(view, 0);
465                    ViewCompat.setTranslationX(view, 0);
466                    dispatchMoveFinished(item);
467                    moves.remove(j);
468                    if (moves.isEmpty()) {
469                        mMovesList.remove(i);
470                    }
471                    break;
472                }
473            }
474        }
475        for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
476            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
477            if (additions.remove(item)) {
478                ViewCompat.setAlpha(view, 1);
479                dispatchAddFinished(item);
480                if (additions.isEmpty()) {
481                    mAdditionsList.remove(i);
482                }
483            }
484        }
485
486        // animations should be ended by the cancel above.
487        //noinspection PointlessBooleanExpression,ConstantConditions
488        if (mRemoveAnimations.remove(item) && DEBUG) {
489            throw new IllegalStateException("after animation is cancelled, item should not be in "
490                    + "mRemoveAnimations list");
491        }
492
493        //noinspection PointlessBooleanExpression,ConstantConditions
494        if (mAddAnimations.remove(item) && DEBUG) {
495            throw new IllegalStateException("after animation is cancelled, item should not be in "
496                    + "mAddAnimations list");
497        }
498
499        //noinspection PointlessBooleanExpression,ConstantConditions
500        if (mChangeAnimations.remove(item) && DEBUG) {
501            throw new IllegalStateException("after animation is cancelled, item should not be in "
502                    + "mChangeAnimations list");
503        }
504
505        //noinspection PointlessBooleanExpression,ConstantConditions
506        if (mMoveAnimations.remove(item) && DEBUG) {
507            throw new IllegalStateException("after animation is cancelled, item should not be in "
508                    + "mMoveAnimations list");
509        }
510        dispatchFinishedWhenDone();
511    }
512
513    private void resetAnimation(ViewHolder holder) {
514        AnimatorCompatHelper.clearInterpolator(holder.itemView);
515        endAnimation(holder);
516    }
517
518    @Override
519    public boolean isRunning() {
520        return (!mPendingAdditions.isEmpty() ||
521                !mPendingChanges.isEmpty() ||
522                !mPendingMoves.isEmpty() ||
523                !mPendingRemovals.isEmpty() ||
524                !mMoveAnimations.isEmpty() ||
525                !mRemoveAnimations.isEmpty() ||
526                !mAddAnimations.isEmpty() ||
527                !mChangeAnimations.isEmpty() ||
528                !mMovesList.isEmpty() ||
529                !mAdditionsList.isEmpty() ||
530                !mChangesList.isEmpty());
531    }
532
533    /**
534     * Check the state of currently pending and running animations. If there are none
535     * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
536     * listeners.
537     */
538    private void dispatchFinishedWhenDone() {
539        if (!isRunning()) {
540            dispatchAnimationsFinished();
541        }
542    }
543
544    @Override
545    public void endAnimations() {
546        int count = mPendingMoves.size();
547        for (int i = count - 1; i >= 0; i--) {
548            MoveInfo item = mPendingMoves.get(i);
549            View view = item.holder.itemView;
550            ViewCompat.setTranslationY(view, 0);
551            ViewCompat.setTranslationX(view, 0);
552            dispatchMoveFinished(item.holder);
553            mPendingMoves.remove(i);
554        }
555        count = mPendingRemovals.size();
556        for (int i = count - 1; i >= 0; i--) {
557            ViewHolder item = mPendingRemovals.get(i);
558            dispatchRemoveFinished(item);
559            mPendingRemovals.remove(i);
560        }
561        count = mPendingAdditions.size();
562        for (int i = count - 1; i >= 0; i--) {
563            ViewHolder item = mPendingAdditions.get(i);
564            View view = item.itemView;
565            ViewCompat.setAlpha(view, 1);
566            dispatchAddFinished(item);
567            mPendingAdditions.remove(i);
568        }
569        count = mPendingChanges.size();
570        for (int i = count - 1; i >= 0; i--) {
571            endChangeAnimationIfNecessary(mPendingChanges.get(i));
572        }
573        mPendingChanges.clear();
574        if (!isRunning()) {
575            return;
576        }
577
578        int listCount = mMovesList.size();
579        for (int i = listCount - 1; i >= 0; i--) {
580            ArrayList<MoveInfo> moves = mMovesList.get(i);
581            count = moves.size();
582            for (int j = count - 1; j >= 0; j--) {
583                MoveInfo moveInfo = moves.get(j);
584                ViewHolder item = moveInfo.holder;
585                View view = item.itemView;
586                ViewCompat.setTranslationY(view, 0);
587                ViewCompat.setTranslationX(view, 0);
588                dispatchMoveFinished(moveInfo.holder);
589                moves.remove(j);
590                if (moves.isEmpty()) {
591                    mMovesList.remove(moves);
592                }
593            }
594        }
595        listCount = mAdditionsList.size();
596        for (int i = listCount - 1; i >= 0; i--) {
597            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
598            count = additions.size();
599            for (int j = count - 1; j >= 0; j--) {
600                ViewHolder item = additions.get(j);
601                View view = item.itemView;
602                ViewCompat.setAlpha(view, 1);
603                dispatchAddFinished(item);
604                additions.remove(j);
605                if (additions.isEmpty()) {
606                    mAdditionsList.remove(additions);
607                }
608            }
609        }
610        listCount = mChangesList.size();
611        for (int i = listCount - 1; i >= 0; i--) {
612            ArrayList<ChangeInfo> changes = mChangesList.get(i);
613            count = changes.size();
614            for (int j = count - 1; j >= 0; j--) {
615                endChangeAnimationIfNecessary(changes.get(j));
616                if (changes.isEmpty()) {
617                    mChangesList.remove(changes);
618                }
619            }
620        }
621
622        cancelAll(mRemoveAnimations);
623        cancelAll(mMoveAnimations);
624        cancelAll(mAddAnimations);
625        cancelAll(mChangeAnimations);
626
627        dispatchAnimationsFinished();
628    }
629
630    void cancelAll(List<ViewHolder> viewHolders) {
631        for (int i = viewHolders.size() - 1; i >= 0; i--) {
632            ViewCompat.animate(viewHolders.get(i).itemView).cancel();
633        }
634    }
635
636    /**
637     * {@inheritDoc}
638     * <p>
639     * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
640     * When this is the case:
641     * <ul>
642     * <li>If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both
643     * ViewHolder arguments will be the same instance.
644     * </li>
645     * <li>
646     * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)},
647     * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and
648     * run a move animation instead.
649     * </li>
650     * </ul>
651     */
652    @Override
653    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
654            @NonNull List<Object> payloads) {
655        return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
656    }
657
658    private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
659        @Override
660        public void onAnimationStart(View view) {}
661
662        @Override
663        public void onAnimationEnd(View view) {}
664
665        @Override
666        public void onAnimationCancel(View view) {}
667    }
668}
669