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