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 || changesPending) {
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 == null ? null : holder.itemView;
335        final ViewHolder newHolder = changeInfo.newHolder;
336        final View newView = newHolder != null ? newHolder.itemView : null;
337        if (view != null) {
338            mChangeAnimations.add(changeInfo.oldHolder);
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
349                @Override
350                public void onAnimationEnd(View view) {
351                    oldViewAnim.setListener(null);
352                    ViewCompat.setAlpha(view, 1);
353                    ViewCompat.setTranslationX(view, 0);
354                    ViewCompat.setTranslationY(view, 0);
355                    dispatchChangeFinished(changeInfo.oldHolder, true);
356                    mChangeAnimations.remove(changeInfo.oldHolder);
357                    dispatchFinishedWhenDone();
358                }
359            }).start();
360        }
361        if (newView != null) {
362            mChangeAnimations.add(changeInfo.newHolder);
363            final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
364            newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
365                    alpha(1).setListener(new VpaListenerAdapter() {
366                @Override
367                public void onAnimationStart(View view) {
368                    dispatchChangeStarting(changeInfo.newHolder, false);
369                }
370                @Override
371                public void onAnimationEnd(View view) {
372                    newViewAnimation.setListener(null);
373                    ViewCompat.setAlpha(newView, 1);
374                    ViewCompat.setTranslationX(newView, 0);
375                    ViewCompat.setTranslationY(newView, 0);
376                    dispatchChangeFinished(changeInfo.newHolder, false);
377                    mChangeAnimations.remove(changeInfo.newHolder);
378                    dispatchFinishedWhenDone();
379                }
380            }).start();
381        }
382    }
383
384    private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
385        for (int i = infoList.size() - 1; i >= 0; i--) {
386            ChangeInfo changeInfo = infoList.get(i);
387            if (endChangeAnimationIfNecessary(changeInfo, item)) {
388                if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
389                    infoList.remove(changeInfo);
390                }
391            }
392        }
393    }
394
395    private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
396        if (changeInfo.oldHolder != null) {
397            endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
398        }
399        if (changeInfo.newHolder != null) {
400            endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
401        }
402    }
403    private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
404        boolean oldItem = false;
405        if (changeInfo.newHolder == item) {
406            changeInfo.newHolder = null;
407        } else if (changeInfo.oldHolder == item) {
408            changeInfo.oldHolder = null;
409            oldItem = true;
410        } else {
411            return false;
412        }
413        ViewCompat.setAlpha(item.itemView, 1);
414        ViewCompat.setTranslationX(item.itemView, 0);
415        ViewCompat.setTranslationY(item.itemView, 0);
416        dispatchChangeFinished(item, oldItem);
417        return true;
418    }
419
420    @Override
421    public void endAnimation(ViewHolder item) {
422        final View view = item.itemView;
423        // this will trigger end callback which should set properties to their target values.
424        ViewCompat.animate(view).cancel();
425        // TODO if some other animations are chained to end, how do we cancel them as well?
426        for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
427            MoveInfo moveInfo = mPendingMoves.get(i);
428            if (moveInfo.holder == item) {
429                ViewCompat.setTranslationY(view, 0);
430                ViewCompat.setTranslationX(view, 0);
431                dispatchMoveFinished(item);
432                mPendingMoves.remove(i);
433            }
434        }
435        endChangeAnimation(mPendingChanges, item);
436        if (mPendingRemovals.remove(item)) {
437            ViewCompat.setAlpha(view, 1);
438            dispatchRemoveFinished(item);
439        }
440        if (mPendingAdditions.remove(item)) {
441            ViewCompat.setAlpha(view, 1);
442            dispatchAddFinished(item);
443        }
444
445        for (int i = mChangesList.size() - 1; i >= 0; i--) {
446            ArrayList<ChangeInfo> changes = mChangesList.get(i);
447            endChangeAnimation(changes, item);
448            if (changes.isEmpty()) {
449                mChangesList.remove(i);
450            }
451        }
452        for (int i = mMovesList.size() - 1; i >= 0; i--) {
453            ArrayList<MoveInfo> moves = mMovesList.get(i);
454            for (int j = moves.size() - 1; j >= 0; j--) {
455                MoveInfo moveInfo = moves.get(j);
456                if (moveInfo.holder == item) {
457                    ViewCompat.setTranslationY(view, 0);
458                    ViewCompat.setTranslationX(view, 0);
459                    dispatchMoveFinished(item);
460                    moves.remove(j);
461                    if (moves.isEmpty()) {
462                        mMovesList.remove(i);
463                    }
464                    break;
465                }
466            }
467        }
468        for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
469            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
470            if (additions.remove(item)) {
471                ViewCompat.setAlpha(view, 1);
472                dispatchAddFinished(item);
473                if (additions.isEmpty()) {
474                    mAdditionsList.remove(i);
475                }
476            }
477        }
478
479        // animations should be ended by the cancel above.
480        if (mRemoveAnimations.remove(item) && DEBUG) {
481            throw new IllegalStateException("after animation is cancelled, item should not be in "
482                    + "mRemoveAnimations list");
483        }
484
485        if (mAddAnimations.remove(item) && DEBUG) {
486            throw new IllegalStateException("after animation is cancelled, item should not be in "
487                    + "mAddAnimations list");
488        }
489
490        if (mChangeAnimations.remove(item) && DEBUG) {
491            throw new IllegalStateException("after animation is cancelled, item should not be in "
492                    + "mChangeAnimations list");
493        }
494
495        if (mMoveAnimations.remove(item) && DEBUG) {
496            throw new IllegalStateException("after animation is cancelled, item should not be in "
497                    + "mMoveAnimations list");
498        }
499        dispatchFinishedWhenDone();
500    }
501
502    @Override
503    public boolean isRunning() {
504        return (!mPendingAdditions.isEmpty() ||
505                !mPendingChanges.isEmpty() ||
506                !mPendingMoves.isEmpty() ||
507                !mPendingRemovals.isEmpty() ||
508                !mMoveAnimations.isEmpty() ||
509                !mRemoveAnimations.isEmpty() ||
510                !mAddAnimations.isEmpty() ||
511                !mChangeAnimations.isEmpty() ||
512                !mMovesList.isEmpty() ||
513                !mAdditionsList.isEmpty() ||
514                !mChangesList.isEmpty());
515    }
516
517    /**
518     * Check the state of currently pending and running animations. If there are none
519     * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
520     * listeners.
521     */
522    private void dispatchFinishedWhenDone() {
523        if (!isRunning()) {
524            dispatchAnimationsFinished();
525        }
526    }
527
528    @Override
529    public void endAnimations() {
530        int count = mPendingMoves.size();
531        for (int i = count - 1; i >= 0; i--) {
532            MoveInfo item = mPendingMoves.get(i);
533            View view = item.holder.itemView;
534            ViewCompat.setTranslationY(view, 0);
535            ViewCompat.setTranslationX(view, 0);
536            dispatchMoveFinished(item.holder);
537            mPendingMoves.remove(i);
538        }
539        count = mPendingRemovals.size();
540        for (int i = count - 1; i >= 0; i--) {
541            ViewHolder item = mPendingRemovals.get(i);
542            dispatchRemoveFinished(item);
543            mPendingRemovals.remove(i);
544        }
545        count = mPendingAdditions.size();
546        for (int i = count - 1; i >= 0; i--) {
547            ViewHolder item = mPendingAdditions.get(i);
548            View view = item.itemView;
549            ViewCompat.setAlpha(view, 1);
550            dispatchAddFinished(item);
551            mPendingAdditions.remove(i);
552        }
553        count = mPendingChanges.size();
554        for (int i = count - 1; i >= 0; i--) {
555            endChangeAnimationIfNecessary(mPendingChanges.get(i));
556        }
557        mPendingChanges.clear();
558        if (!isRunning()) {
559            return;
560        }
561
562        int listCount = mMovesList.size();
563        for (int i = listCount - 1; i >= 0; i--) {
564            ArrayList<MoveInfo> moves = mMovesList.get(i);
565            count = moves.size();
566            for (int j = count - 1; j >= 0; j--) {
567                MoveInfo moveInfo = moves.get(j);
568                ViewHolder item = moveInfo.holder;
569                View view = item.itemView;
570                ViewCompat.setTranslationY(view, 0);
571                ViewCompat.setTranslationX(view, 0);
572                dispatchMoveFinished(moveInfo.holder);
573                moves.remove(j);
574                if (moves.isEmpty()) {
575                    mMovesList.remove(moves);
576                }
577            }
578        }
579        listCount = mAdditionsList.size();
580        for (int i = listCount - 1; i >= 0; i--) {
581            ArrayList<ViewHolder> additions = mAdditionsList.get(i);
582            count = additions.size();
583            for (int j = count - 1; j >= 0; j--) {
584                ViewHolder item = additions.get(j);
585                View view = item.itemView;
586                ViewCompat.setAlpha(view, 1);
587                dispatchAddFinished(item);
588                additions.remove(j);
589                if (additions.isEmpty()) {
590                    mAdditionsList.remove(additions);
591                }
592            }
593        }
594        listCount = mChangesList.size();
595        for (int i = listCount - 1; i >= 0; i--) {
596            ArrayList<ChangeInfo> changes = mChangesList.get(i);
597            count = changes.size();
598            for (int j = count - 1; j >= 0; j--) {
599                endChangeAnimationIfNecessary(changes.get(j));
600                if (changes.isEmpty()) {
601                    mChangesList.remove(changes);
602                }
603            }
604        }
605
606        cancelAll(mRemoveAnimations);
607        cancelAll(mMoveAnimations);
608        cancelAll(mAddAnimations);
609        cancelAll(mChangeAnimations);
610
611        dispatchAnimationsFinished();
612    }
613
614    void cancelAll(List<ViewHolder> viewHolders) {
615        for (int i = viewHolders.size() - 1; i >= 0; i--) {
616            ViewCompat.animate(viewHolders.get(i).itemView).cancel();
617        }
618    }
619
620    private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
621        @Override
622        public void onAnimationStart(View view) {}
623
624        @Override
625        public void onAnimationEnd(View view) {}
626
627        @Override
628        public void onAnimationCancel(View view) {}
629    };
630}
631