AnimatedAdapter.java revision 4022889525ce3ef25caabe4a8b50c7140a4bd9ed
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.animation.Animator;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.database.Cursor;
24import android.view.View;
25import android.view.ViewGroup;
26import android.widget.AdapterView;
27import android.widget.ListView;
28import android.widget.SimpleCursorAdapter;
29
30import com.android.mail.browse.ConversationCursor;
31import com.android.mail.browse.ConversationItemView;
32import com.android.mail.browse.ConversationListFooterView;
33import com.android.mail.providers.Account;
34import com.android.mail.providers.Conversation;
35import com.android.mail.providers.Folder;
36import com.android.mail.providers.UIProvider;
37import com.android.mail.ui.UndoBarView.OnUndoCancelListener;
38import com.android.mail.utils.LogUtils;
39
40import java.util.ArrayList;
41import java.util.Collection;
42import java.util.HashSet;
43
44public class AnimatedAdapter extends SimpleCursorAdapter implements
45        android.animation.Animator.AnimatorListener, OnUndoCancelListener {
46    private static int ITEM_VIEW_TYPE_FOOTER = 1;
47    private HashSet<Integer> mDeletingItems = new HashSet<Integer>();
48    private Account mSelectedAccount;
49    private Context mContext;
50    private ConversationSelectionSet mBatchConversations;
51    private ActionCompleteListener mActionCompleteListener;
52    private boolean mUndo = false;
53    private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>();
54    private ViewMode mViewMode;
55    private View mFooter;
56    private boolean mShowFooter;
57    private Folder mFolder;
58    private final ListView mListView;
59    /**
60     * Used only for debugging.
61     */
62    private static final String LOG_TAG = new LogUtils().getLogTag();
63
64    public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor,
65            ConversationSelectionSet batch, Account account, ViewMode viewMode, ListView listView) {
66        // Use FLAG_REGISTER_CONTENT_OBSERVER to ensure special ConversationCursor notifications
67        // (triggered by UI actions) cause any connected ListView to redraw.
68        super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null,
69                FLAG_REGISTER_CONTENT_OBSERVER);
70        mContext = context;
71        mBatchConversations = batch;
72        mSelectedAccount = account;
73        mViewMode = viewMode;
74        mShowFooter = false;
75        mListView = listView;
76    }
77
78    @Override
79    public int getCount() {
80        int count = super.getCount();
81        return mShowFooter ? count + 1 : count;
82    }
83
84    public void setUndo(boolean state) {
85        mUndo = state;
86        if (mUndo) {
87            mDeletingItems.clear();
88            mDeletingItems.addAll(mLastDeletingItems);
89            // Start animation
90            notifyDataSetChanged();
91            mActionCompleteListener = new ActionCompleteListener() {
92                @Override
93                public void onActionComplete() {
94                    notifyDataSetChanged();
95                }
96            };
97        }
98    }
99
100    @Override
101    public View newView(Context context, Cursor cursor, ViewGroup parent) {
102        ConversationItemView view = new ConversationItemView(context, mSelectedAccount.name);
103        return view;
104    }
105
106    @Override
107    public void bindView(View view, Context context, Cursor cursor) {
108        if (!isPositionAnimating(view) && !isPositionFooter(view)) {
109            ((ConversationItemView) view).bind(cursor, mViewMode,
110                    mBatchConversations, mFolder);
111        }
112    }
113
114    @Override
115    public boolean hasStableIds() {
116        return true;
117    }
118
119    @Override
120    public int getViewTypeCount() {
121        // Our normal view and the animating (not recycled) view
122        return 3;
123    }
124
125    @Override
126    public int getItemViewType(int position) {
127        // Don't recycle animating views
128        if (isPositionAnimating(position)) {
129            return AdapterView.ITEM_VIEW_TYPE_IGNORE;
130        } else if (mShowFooter && position == super.getCount()) {
131            return ITEM_VIEW_TYPE_FOOTER;
132        }
133        return 0;
134    }
135
136    /**
137     * Deletes the selected conversations from the conversation list view. These conversations
138     * <b>must</b> have their {@link Conversation#position} set to the position of these
139     * conversations among the list. . This will only remove the
140     * element from the list. The job of deleting the actual element is left to the the listener.
141     * This listener will be called when the animations are complete and is required to
142     * delete the conversation.
143     * @param conversations
144     * @param listener
145     */
146    public void delete(Collection<Conversation> conversations,
147            ActionCompleteListener listener) {
148        // Animate out the positions.
149        // Call when all the animations are complete.
150        final ArrayList<Integer> positions = new ArrayList<Integer>();
151        for (Conversation c : conversations) {
152            positions.add(c.position);
153        }
154        delete(positions, listener);
155    }
156
157    /**
158     * Deletes a conversation with the list positions given here. This will only remove the
159     * element from the list. The job of deleting the actual elements is left to the the listener.
160     * This listener will be called when the animations are complete and is required to
161     * delete the conversations.
162     * @param deletedRows the position in the list view to be deleted.
163     * @param listener called when the animation is complete. At this point, it is safe to remove
164     * the conversations from the database.
165     */
166    public void delete(ArrayList<Integer> deletedRows, ActionCompleteListener listener) {
167        // Clear out any remaining items and add the new ones
168        mLastDeletingItems.clear();
169
170        int startPosition = mListView.getFirstVisiblePosition();
171        int endPosition = mListView.getLastVisiblePosition();
172
173        // Only animate visible items
174        for (int deletedRow: deletedRows) {
175            if (deletedRow >= startPosition && deletedRow <= endPosition) {
176                mLastDeletingItems.add(deletedRow);
177                mDeletingItems.add(deletedRow);
178            }
179        }
180
181        if (mDeletingItems.isEmpty()) {
182            // If we have no deleted items on screen, skip the animation
183            listener.onActionComplete();
184        } else {
185            mActionCompleteListener = listener;
186        }
187
188        // TODO(viki): Rather than notifying for a full data set change,
189        // perhaps we can mark
190        // only the affected conversations?
191        notifyDataSetChanged();
192    }
193
194    @Override
195    public View getView(int position, View convertView, ViewGroup parent) {
196        if (mShowFooter && position == super.getCount()) {
197            return mFooter;
198        }
199        if (isPositionAnimating(position)) {
200            return getAnimatingView(position, convertView, parent);
201        }
202        // TODO: do this in the swipe helper?
203        // If this view gets recycled, we need to reset things set by the
204        // animation.
205        if (convertView != null) {
206            if (convertView.getAlpha() < 1) {
207                convertView.setAlpha(1);
208            }
209            if (convertView.getTranslationX() != 0) {
210                convertView.setTranslationX(0);
211            }
212        }
213        return super.getView(position, convertView, parent);
214    }
215
216    /**
217     * Get an animating view. This happens when a list item is in the process of being removed
218     * from the list (items being deleted).
219     * @param position the position of the view inside the list
220     * @param convertView if null, a recycled view that we can reuse
221     * @param parent the parent view
222     * @return the view to show when animating an operation.
223     */
224    private View getAnimatingView(int position, View convertView, ViewGroup parent) {
225        // We are getting the wrong view, and we need to gracefully carry on.
226        if (!(convertView instanceof AnimatingItemView)) {
227            LogUtils.d(LOG_TAG, "AnimatedAdapter.getAnimatingView received the wrong view!");
228            convertView = null;
229        }
230        Conversation conversation = new Conversation((ConversationCursor) getItem(position));
231        conversation.position = position;
232        if (mUndo) {
233            // The undo animation consists of fading in the conversation that
234            // had been destroyed.
235            ConversationItemView convView = (ConversationItemView) super.getView(position, null,
236                    parent);
237            convView.startUndoAnimation(mViewMode, this);
238            return convView;
239        } else {
240            // Destroying a conversation just shows a blank shrinking item.
241            final AnimatingItemView view = new AnimatingItemView(mContext);
242            view.startAnimation(conversation, mViewMode, this);
243            return view;
244        }
245    }
246
247    private boolean isPositionAnimating(int position) {
248        return mDeletingItems.contains(position)
249                || (mUndo && mLastDeletingItems.contains(position));
250    }
251
252    private boolean isPositionAnimating(View view) {
253        return (view instanceof AnimatingItemView);
254    }
255
256    private boolean isPositionFooter(View view) {
257        return (view instanceof ConversationListFooterView);
258    }
259
260    @Override
261    public void onAnimationStart(Animator animation) {
262        if (mUndo) {
263            mDeletingItems.clear();
264            mLastDeletingItems.clear();
265            mUndo = false;
266        }
267    }
268
269    @Override
270    public void onAnimationEnd(Animator animation) {
271        if (mUndo && !mLastDeletingItems.isEmpty()) {
272            // See if we have received all the animations we expected; if
273            // so, call the listener and reset it.
274            int position = ((ConversationItemView) ((ObjectAnimator) animation).getTarget())
275                    .getPosition();
276            mLastDeletingItems.remove(position);
277            if (mLastDeletingItems.isEmpty()) {
278                if (mActionCompleteListener != null) {
279                    mActionCompleteListener.onActionComplete();
280                    mActionCompleteListener = null;
281                }
282                mUndo = false;
283            }
284        } else if (!mDeletingItems.isEmpty()) {
285            // See if we have received all the animations we expected; if
286            // so, call the listener and reset it.
287            AnimatingItemView target = ((AnimatingItemView) ((ObjectAnimator) animation)
288                    .getTarget());
289            int position = target.getData().position;
290            mDeletingItems.remove(position);
291            if (mDeletingItems.isEmpty()) {
292                if (mActionCompleteListener != null) {
293                    mActionCompleteListener.onActionComplete();
294                    mActionCompleteListener = null;
295                }
296            }
297        }
298    }
299
300    @Override
301    public boolean areAllItemsEnabled() {
302        return false;
303    }
304
305    @Override
306    public boolean isEnabled(int position) {
307        return !isPositionAnimating(position);
308    }
309
310    @Override
311    public void onAnimationCancel(Animator animation) {
312        onAnimationEnd(animation);
313    }
314
315    @Override
316    public void onAnimationRepeat(Animator animation) {
317        // TODO Auto-generated method stub
318    }
319
320    @Override
321    public void onUndoCancel() {
322        mLastDeletingItems.clear();
323    }
324
325    public void showFooter() {
326        if (!mShowFooter) {
327            mShowFooter = true;
328            notifyDataSetChanged();
329        }
330    }
331
332    public void hideFooter() {
333        if (mShowFooter) {
334            mShowFooter = false;
335            notifyDataSetChanged();
336        }
337    }
338
339    public void addFooter(View footerView) {
340        mFooter = footerView;
341    }
342
343    public void setFolder(Folder folder) {
344        mFolder = folder;
345    }
346}
347