DefaultItemAnimator.java revision c35968d173f900d8024bdf38174e2225c9a7f311
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.ViewPropertyAnimatorListener;
20import android.support.v7.widget.RecyclerView.ViewHolder;
21import android.view.View;
22
23import java.util.ArrayList;
24
25/**
26 * This implementation of {@link RecyclerView.ItemAnimator} provides basic
27 * animations on remove, add, and move events that happen to the items in
28 * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
29 *
30 * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
31 */
32public class DefaultItemAnimator extends RecyclerView.ItemAnimator {
33
34    private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>();
35    private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>();
36    private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
37
38    private ArrayList<ViewHolder> mAdditions = new ArrayList<ViewHolder>();
39    private ArrayList<MoveInfo> mMoves = new ArrayList<MoveInfo>();
40
41    private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
42    private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>();
43    private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>();
44
45    private static class MoveInfo {
46        public ViewHolder holder;
47        public int fromX, fromY, toX, toY;
48
49        private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
50            this.holder = holder;
51            this.fromX = fromX;
52            this.fromY = fromY;
53            this.toX = toX;
54            this.toY = toY;
55        }
56    }
57
58    @Override
59    public void runPendingAnimations() {
60        boolean removalsPending = !mPendingRemovals.isEmpty();
61        boolean movesPending = !mPendingMoves.isEmpty();
62        boolean additionsPending = !mPendingAdditions.isEmpty();
63        if (!removalsPending && !movesPending && !additionsPending) {
64            // nothing to animate
65            return;
66        }
67        // First, remove stuff
68        for (ViewHolder holder : mPendingRemovals) {
69            animateRemoveImpl(holder);
70        }
71        mPendingRemovals.clear();
72        // Next, move stuff
73        if (movesPending) {
74            mMoves.addAll(mPendingMoves);
75            mPendingMoves.clear();
76            Runnable mover = new Runnable() {
77                @Override
78                public void run() {
79                    for (MoveInfo moveInfo : mMoves) {
80                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
81                                moveInfo.toX, moveInfo.toY);
82                    }
83                    mMoves.clear();
84                }
85            };
86            if (removalsPending) {
87                View view = mMoves.get(0).holder.itemView;
88                ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
89            } else {
90                mover.run();
91            }
92        }
93        // Next, add stuff
94        if (additionsPending) {
95            mAdditions.addAll(mPendingAdditions);
96            mPendingAdditions.clear();
97            Runnable adder = new Runnable() {
98                public void run() {
99                    for (ViewHolder holder : mAdditions) {
100                        animateAddImpl(holder);
101                    }
102                    mAdditions.clear();
103                }
104            };
105            if (removalsPending || movesPending) {
106                View view = mAdditions.get(0).itemView;
107                ViewCompat.postOnAnimationDelayed(view, adder,
108                        (removalsPending ? getRemoveDuration() : 0) +
109                                (movesPending ? getMoveDuration() : 0));
110            } else {
111                adder.run();
112            }
113        }
114    }
115
116    @Override
117    public boolean animateRemove(final ViewHolder holder) {
118        mPendingRemovals.add(holder);
119        return true;
120    }
121
122    private void animateRemoveImpl(final ViewHolder holder) {
123        final View view = holder.itemView;
124        ViewCompat.animate(view).cancel();
125        ViewCompat.animate(view).setDuration(getRemoveDuration()).
126                alpha(0).setListener(new VpaListenerAdapter() {
127            @Override
128            public void onAnimationEnd(View view) {
129                ViewCompat.setAlpha(view, 1);
130                dispatchRemoveFinished(holder);
131                mRemoveAnimations.remove(holder);
132                dispatchFinishedWhenDone();
133            }
134        }).start();
135        mRemoveAnimations.add(holder);
136    }
137
138    @Override
139    public boolean animateAdd(final ViewHolder holder) {
140        ViewCompat.setAlpha(holder.itemView, 0);
141        mPendingAdditions.add(holder);
142        return true;
143    }
144
145    private void animateAddImpl(final ViewHolder holder) {
146        final View view = holder.itemView;
147        ViewCompat.animate(view).cancel();
148        ViewCompat.animate(view).alpha(1).setDuration(getAddDuration()).
149                setListener(new VpaListenerAdapter() {
150                    @Override
151                    public void onAnimationCancel(View view) {
152                        ViewCompat.setAlpha(view, 1);
153                    }
154
155                    @Override
156                    public void onAnimationEnd(View view) {
157                        dispatchAddFinished(holder);
158                        mAddAnimations.remove(holder);
159                        dispatchFinishedWhenDone();
160                    }
161                }).start();
162        mAddAnimations.add(holder);
163    }
164
165    @Override
166    public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
167            int toX, int toY) {
168        final View view = holder.itemView;
169        int deltaX = toX - fromX;
170        int deltaY = toY - fromY;
171        if (deltaX == 0 && deltaY == 0) {
172            dispatchMoveFinished(holder);
173            return false;
174        }
175        if (deltaX != 0) {
176            ViewCompat.setTranslationX(view, -deltaX);
177        }
178        if (deltaY != 0) {
179            ViewCompat.setTranslationY(view, -deltaY);
180        }
181        mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
182        return true;
183    }
184
185    private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
186        final View view = holder.itemView;
187        final int deltaX = toX - fromX;
188        final int deltaY = toY - fromY;
189        ViewCompat.animate(view).cancel();
190        if (deltaX != 0) {
191            ViewCompat.animate(view).translationX(0);
192        }
193        if (deltaY != 0) {
194            ViewCompat.animate(view).translationY(0);
195        }
196        // TODO: make EndActions end listeners instead, since end actions aren't called when
197        // vpas are canceled (and can't end them. why?)
198        // need listener functionality in VPACompat for this. Ick.
199        ViewCompat.animate(view).setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
200            @Override
201            public void onAnimationCancel(View view) {
202                if (deltaX != 0) {
203                    ViewCompat.setTranslationX(view, 0);
204                }
205                if (deltaY != 0) {
206                    ViewCompat.setTranslationY(view, 0);
207                }
208            }
209            @Override
210            public void onAnimationEnd(View view) {
211                dispatchMoveFinished(holder);
212                mMoveAnimations.remove(holder);
213                dispatchFinishedWhenDone();
214            }
215        }).start();
216        mMoveAnimations.add(holder);
217    }
218
219    @Override
220    public void endAnimation(ViewHolder item) {
221        final View view = item.itemView;
222        ViewCompat.animate(view).cancel();
223        if (mPendingMoves.contains(item)) {
224            ViewCompat.setTranslationY(view, 0);
225            ViewCompat.setTranslationX(view, 0);
226            dispatchMoveFinished(item);
227            mPendingMoves.remove(item);
228        }
229        if (mPendingRemovals.contains(item)) {
230            dispatchRemoveFinished(item);
231            mPendingRemovals.remove(item);
232        }
233        if (mPendingAdditions.contains(item)) {
234            ViewCompat.setAlpha(view, 1);
235            dispatchAddFinished(item);
236            mPendingAdditions.remove(item);
237        }
238        if (mMoveAnimations.contains(item)) {
239            ViewCompat.setTranslationY(view, 0);
240            ViewCompat.setTranslationX(view, 0);
241            dispatchMoveFinished(item);
242            mMoveAnimations.remove(item);
243        }
244        if (mRemoveAnimations.contains(item)) {
245            ViewCompat.setAlpha(view, 1);
246            dispatchRemoveFinished(item);
247            mRemoveAnimations.remove(item);
248        }
249        if (mAddAnimations.contains(item)) {
250            ViewCompat.setAlpha(view, 1);
251            dispatchAddFinished(item);
252            mAddAnimations.remove(item);
253        }
254        dispatchFinishedWhenDone();
255    }
256
257    @Override
258    public boolean isRunning() {
259        return (!mMoveAnimations.isEmpty() ||
260                !mRemoveAnimations.isEmpty() ||
261                !mAddAnimations.isEmpty() ||
262                !mMoves.isEmpty() ||
263                !mAdditions.isEmpty());
264    }
265
266    /**
267     * Check the state of currently pending and running animations. If there are none
268     * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
269     * listeners.
270     */
271    private void dispatchFinishedWhenDone() {
272        if (!isRunning()) {
273            dispatchAnimationsFinished();
274        }
275    }
276
277    @Override
278    public void endAnimations() {
279        int count = mPendingMoves.size();
280        for (int i = count - 1; i >= 0; i--) {
281            MoveInfo item = mPendingMoves.get(i);
282            View view = item.holder.itemView;
283            ViewCompat.animate(view).cancel();
284            ViewCompat.setTranslationY(view, 0);
285            ViewCompat.setTranslationX(view, 0);
286            dispatchMoveFinished(item.holder);
287            mPendingMoves.remove(item);
288        }
289        count = mPendingRemovals.size();
290        for (int i = count - 1; i >= 0; i--) {
291            ViewHolder item = mPendingRemovals.get(i);
292            dispatchRemoveFinished(item);
293            mPendingRemovals.remove(item);
294        }
295        count = mPendingAdditions.size();
296        for (int i = count - 1; i >= 0; i--) {
297            ViewHolder item = mPendingAdditions.get(i);
298            View view = item.itemView;
299            ViewCompat.setAlpha(view, 1);
300            dispatchAddFinished(item);
301            mPendingAdditions.remove(item);
302        }
303        if (!isRunning()) {
304            return;
305        }
306        count = mMoveAnimations.size();
307        for (int i = count - 1; i >= 0; i--) {
308            ViewHolder item = mMoveAnimations.get(i);
309            View view = item.itemView;
310            ViewCompat.animate(view).cancel();
311            ViewCompat.setTranslationY(view, 0);
312            ViewCompat.setTranslationX(view, 0);
313            dispatchMoveFinished(item);
314            mMoveAnimations.remove(item);
315        }
316        count = mRemoveAnimations.size();
317        for (int i = count - 1; i >= 0; i--) {
318            ViewHolder item = mRemoveAnimations.get(i);
319            View view = item.itemView;
320            ViewCompat.animate(view).cancel();
321            ViewCompat.setAlpha(view, 1);
322            dispatchRemoveFinished(item);
323            mRemoveAnimations.remove(item);
324        }
325        count = mAddAnimations.size();
326        for (int i = count - 1; i >= 0; i--) {
327            ViewHolder item = mAddAnimations.get(i);
328            View view = item.itemView;
329            ViewCompat.animate(view).cancel();
330            ViewCompat.setAlpha(view, 1);
331            dispatchAddFinished(item);
332            mAddAnimations.remove(item);
333        }
334        mMoves.clear();
335        mAdditions.clear();
336        dispatchAnimationsFinished();
337    }
338
339    private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
340        @Override
341        public void onAnimationStart(View view) {}
342
343        @Override
344        public void onAnimationEnd(View view) {}
345
346        @Override
347        public void onAnimationCancel(View view) {}
348    };
349}
350