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