AnimatedAdapter.java revision bf232c3735f65b1a4746943e4a134e59e36f0bde
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.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.SimpleCursorAdapter;
28
29import com.android.mail.R;
30import com.android.mail.browse.ConversationCursor;
31import com.android.mail.browse.ConversationItemView;
32import com.android.mail.providers.Account;
33import com.android.mail.providers.Conversation;
34import com.android.mail.providers.Folder;
35import com.android.mail.providers.Settings;
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.HashMap;
43import java.util.HashSet;
44
45public class AnimatedAdapter extends SimpleCursorAdapter implements
46        android.animation.Animator.AnimatorListener, OnUndoCancelListener, Settings.ChangeListener {
47    private final static int TYPE_VIEW_CONVERSATION = 0;
48    private final static int TYPE_VIEW_DELETING = 1;
49    private final static int TYPE_VIEW_UNDOING = 2;
50    private final static int TYPE_VIEW_FOOTER = 3;
51    private final static int TYPE_VIEW_LEAVEBEHIND = 4;
52    private HashSet<Integer> mDeletingItems = new HashSet<Integer>();
53    private HashSet<Integer> mUndoingItems = new HashSet<Integer>();
54    private Account mSelectedAccount;
55    private Context mContext;
56    private ConversationSelectionSet mBatchConversations;
57    private ActionCompleteListener mActionCompleteListener;
58    private boolean mUndo = false;
59    private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>();
60    private ViewMode mViewMode;
61    private View mFooter;
62    private boolean mShowFooter;
63    private Folder mFolder;
64    private final SwipeableListView mListView;
65    private Settings mCachedSettings;
66    private boolean mSwipeEnabled;
67    private DragListener mDragListener;
68    private HashMap<Long, LeaveBehindItem> mLeaveBehindItems = new HashMap<Long, LeaveBehindItem>();
69
70    /**
71     * Used only for debugging.
72     */
73    private static final String LOG_TAG = new LogUtils().getLogTag();
74
75    public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor,
76            ConversationSelectionSet batch, Account account, Settings settings, ViewMode viewMode,
77            SwipeableListView listView, DragListener dragListener) {
78        // Use FLAG_REGISTER_CONTENT_OBSERVER to ensure special
79        // ConversationCursor notifications (triggered by UI actions) cause any
80        // connected ListView to redraw.
81        super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null,
82                FLAG_REGISTER_CONTENT_OBSERVER);
83        mContext = context;
84        mBatchConversations = batch;
85        mSelectedAccount = account;
86        mViewMode = viewMode;
87        mShowFooter = false;
88        mListView = listView;
89        mCachedSettings = settings;
90        mDragListener = dragListener;
91        mSwipeEnabled = account.supportsCapability(UIProvider.AccountCapabilities.ARCHIVE);
92    }
93
94    @Override
95    public int getCount() {
96        final int count = super.getCount();
97        return mShowFooter ? count + 1 : count;
98    }
99
100    public void setUndo(boolean undo) {
101        if (undo) {
102            mUndoingItems.addAll(mLastDeletingItems);
103            mLastDeletingItems.clear();
104            // Start animation
105            notifyDataSetChanged();
106            mActionCompleteListener = new ActionCompleteListener() {
107                @Override
108                public void onActionComplete() {
109                    notifyDataSetChanged();
110                }
111            };
112        }
113    }
114
115    @Override
116    public View newView(Context context, Cursor cursor, ViewGroup parent) {
117        ConversationItemView view = new ConversationItemView(context, mSelectedAccount.name);
118        return view;
119    }
120
121    @Override
122    public void bindView(View view, Context context, Cursor cursor) {
123        if (! (view instanceof ConversationItemView)) {
124            return;
125        }
126        ((ConversationItemView) view).bind(cursor, mViewMode, mBatchConversations, mFolder,
127                mCachedSettings != null ? mCachedSettings.hideCheckboxes : false,
128                        mSwipeEnabled, mDragListener, this);
129    }
130
131    @Override
132    public boolean hasStableIds() {
133        return true;
134    }
135
136    @Override
137    public int getViewTypeCount() {
138        // TYPE_VIEW_CONVERSATION, TYPE_VIEW_DELETING, TYPE_VIEW_UNDOING, and
139        // TYPE_VIEW_FOOTER, TYPE_VIEW_LEAVEBEHIND.
140        return 5;
141    }
142
143    @Override
144    public int getItemViewType(int position) {
145        // Try to recycle views.
146        if (isPositionDeleting(position)) {
147            return TYPE_VIEW_DELETING;
148        }
149        if (isPositionUndoing(position)) {
150            return TYPE_VIEW_UNDOING;
151        }
152        if (mShowFooter && position == super.getCount()) {
153            return TYPE_VIEW_FOOTER;
154        }
155        if (isPositionLeaveBehind(position)) {
156            return TYPE_VIEW_LEAVEBEHIND;
157        }
158        return TYPE_VIEW_CONVERSATION;
159    }
160
161    /**
162     * Deletes the selected conversations from the conversation list view. These conversations
163     * <b>must</b> have their {@link Conversation#position} set to the position of these
164     * conversations among the list. . This will only remove the
165     * element from the list. The job of deleting the actual element is left to the the listener.
166     * This listener will be called when the animations are complete and is required to
167     * delete the conversation.
168     * @param conversations
169     * @param listener
170     */
171    public void delete(Collection<Conversation> conversations, ActionCompleteListener listener) {
172        // Animate out the positions.
173        // Call when all the animations are complete.
174        final ArrayList<Integer> positions = new ArrayList<Integer>();
175        for (Conversation c : conversations) {
176            positions.add(c.position);
177        }
178        delete(positions, listener);
179    }
180
181    /**
182     * Deletes a conversation with the list positions given here. This will only remove the
183     * element from the list. The job of deleting the actual elements is left to the the listener.
184     * This listener will be called when the animations are complete and is required to
185     * delete the conversations.
186     * @param deletedRows the position in the list view to be deleted.
187     * @param listener called when the animation is complete. At this point, it is safe to remove
188     * the conversations from the database.
189     */
190    public void delete(ArrayList<Integer> deletedRows, ActionCompleteListener listener) {
191        // Clear out any remaining items and add the new ones
192        mLastDeletingItems.clear();
193
194        final int startPosition = mListView.getFirstVisiblePosition();
195        final int endPosition = mListView.getLastVisiblePosition();
196
197        // Only animate visible items
198        for (int deletedRow: deletedRows) {
199            if (deletedRow >= startPosition && deletedRow <= endPosition) {
200                mLastDeletingItems.add(deletedRow);
201                mDeletingItems.add(deletedRow);
202            }
203        }
204
205        if (mDeletingItems.isEmpty()) {
206            // If we have no deleted items on screen, skip the animation
207            listener.onActionComplete();
208        } else {
209            mActionCompleteListener = listener;
210        }
211
212        // TODO(viki): Rather than notifying for a full data set change,
213        // perhaps we can mark
214        // only the affected conversations?
215        notifyDataSetChanged();
216    }
217
218    @Override
219    public View getView(int position, View convertView, ViewGroup parent) {
220        if (mShowFooter && position == super.getCount()) {
221            return mFooter;
222        }
223        if (isPositionUndoing(position)) {
224            return getUndoingView(position, convertView, parent);
225        } else if (isPositionDeleting(position)) {
226            return getDeletingView(position, convertView, parent);
227        }
228        if (hasLeaveBehinds()) {
229            Conversation conv = new Conversation((ConversationCursor) getItem(position));
230            if(isPositionLeaveBehind(conv)) {
231                return getLeaveBehindItem(position, conv);
232            }
233        }
234        // TODO: do this in the swipe helper?
235        // If this view gets recycled, we need to reset things set by the
236        // animation.
237        if (convertView != null) {
238            if (convertView.getAlpha() < 1) {
239                convertView.setAlpha(1);
240            }
241            if (convertView.getTranslationX() != 0) {
242                convertView.setTranslationX(0);
243            }
244        }
245        return super.getView(position, convertView, parent);
246    }
247
248    private boolean hasLeaveBehinds() {
249        return !mLeaveBehindItems.isEmpty();
250    }
251
252    public void setupLeaveBehind(Conversation target, UndoOperation undoOp, int deletedRow) {
253        commitLeaveBehindItems();
254        LeaveBehindItem leaveBehind = (LeaveBehindItem) LayoutInflater.from(mContext).inflate(
255                R.layout.swipe_leavebehind, null);
256        leaveBehind.bindOperations(mSelectedAccount, this, undoOp,
257                target);
258        mLeaveBehindItems.put(target.id, leaveBehind);
259        mLastDeletingItems.add(deletedRow);
260    }
261
262    public void commitLeaveBehindItems() {
263        // Remove any previously existing leave behinds.
264        if (!mLeaveBehindItems.isEmpty()) {
265            for (LeaveBehindItem item : mLeaveBehindItems.values()) {
266                item.commit();
267            }
268            mLeaveBehindItems.clear();
269        }
270        notifyDataSetChanged();
271    }
272
273    private LeaveBehindItem getLeaveBehindItem(int position, Conversation target) {
274        return mLeaveBehindItems.get(target.id);
275    }
276
277    @Override
278    public long getItemId(int position) {
279        if (mShowFooter && position == super.getCount()) {
280            return -1;
281        }
282        return super.getItemId(position);
283    }
284
285    /**
286     * Get an animating view. This happens when a list item is in the process of being removed
287     * from the list (items being deleted).
288     * @param position the position of the view inside the list
289     * @param convertView if null, a recycled view that we can reuse
290     * @param parent the parent view
291     * @return the view to show when animating an operation.
292     */
293    private View getDeletingView(int position, View convertView, ViewGroup parent) {
294        // We are getting the wrong view, and we need to gracefully carry on.
295        if (convertView != null && !(convertView instanceof AnimatingItemView)) {
296            LogUtils.d(LOG_TAG, "AnimatedAdapter.getAnimatingView received the wrong view!");
297            convertView = null;
298        }
299        Conversation conversation = new Conversation((ConversationCursor) getItem(position));
300        conversation.position = position;
301        // Destroying a conversation just shows a blank shrinking item.
302        final AnimatingItemView view = new AnimatingItemView(mContext);
303        view.startAnimation(conversation, mViewMode, this);
304        return view;
305    }
306
307    private View getUndoingView(int position, View convertView, ViewGroup parent) {
308        Conversation conversation = new Conversation((ConversationCursor) getItem(position));
309        conversation.position = position;
310        // The undo animation consists of fading in the conversation that
311        // had been destroyed.
312        final ConversationItemView convView = (ConversationItemView) super.getView(position, null,
313                parent);
314        convView.bind(conversation, mViewMode, mBatchConversations, mFolder,
315                mCachedSettings != null ? mCachedSettings.hideCheckboxes : false, mSwipeEnabled,
316                mDragListener, this);
317        convView.startUndoAnimation(mViewMode, this);
318        return convView;
319    }
320
321    @Override
322    public Object getItem(int position) {
323        if (mShowFooter && position == super.getCount()) {
324            return mFooter;
325        }
326        return super.getItem(position);
327    }
328
329    private boolean isPositionDeleting(int position) {
330        return mDeletingItems.contains(position);
331    }
332
333    private boolean isPositionUndoing(int position) {
334        return mUndoingItems.contains(position);
335    }
336
337    private boolean isPositionLeaveBehind(Conversation conv) {
338        return mLeaveBehindItems.containsKey(conv.id) && conv.isMostlyDead();
339    }
340
341    private boolean isPositionLeaveBehind(int position) {
342        if (hasLeaveBehinds()) {
343            Object item = getItem(position);
344            if (item instanceof ConversationCursor) {
345                Conversation conv = new Conversation((ConversationCursor) item);
346                return mLeaveBehindItems.containsKey(conv.id) && conv.isMostlyDead();
347            }
348        }
349        return false;
350    }
351
352    @Override
353    public void onAnimationStart(Animator animation) {
354        if (!mUndoingItems.isEmpty()) {
355            mDeletingItems.clear();
356            mLastDeletingItems.clear();
357        } else {
358            mUndoingItems.clear();
359        }
360    }
361
362    @Override
363    public void onAnimationEnd(Animator animation) {
364        if (!mUndoingItems.isEmpty()) {
365            // See if we have received all the animations we expected; if
366            // so, call the listener and reset it.
367            final int position = ((ConversationItemView) ((ObjectAnimator) animation).getTarget())
368                    .getPosition();
369            mUndoingItems.remove(position);
370            if (mUndoingItems.isEmpty()) {
371                if (mActionCompleteListener != null) {
372                    mActionCompleteListener.onActionComplete();
373                    mActionCompleteListener = null;
374                }
375            }
376        } else if (!mDeletingItems.isEmpty()) {
377            // See if we have received all the animations we expected; if
378            // so, call the listener and reset it.
379            AnimatingItemView target = ((AnimatingItemView) ((ObjectAnimator) animation)
380                    .getTarget());
381            final int position = target.getData().position;
382            mDeletingItems.remove(position);
383            if (mDeletingItems.isEmpty()) {
384                if (mActionCompleteListener != null) {
385                    mActionCompleteListener.onActionComplete();
386                    mActionCompleteListener = null;
387                }
388            }
389        }
390        // The view types have changed, since the animating views are gone.
391        notifyDataSetChanged();
392    }
393
394    @Override
395    public boolean areAllItemsEnabled() {
396        // The animating positions are not enabled.
397        return false;
398    }
399
400    @Override
401    public boolean isEnabled(int position) {
402        return !isPositionDeleting(position) && !isPositionUndoing(position);
403    }
404
405    @Override
406    public void onAnimationCancel(Animator animation) {
407        onAnimationEnd(animation);
408    }
409
410    @Override
411    public void onAnimationRepeat(Animator animation) {
412    }
413
414    @Override
415    public void onUndoCancel() {
416        mLastDeletingItems.clear();
417    }
418
419    public void showFooter() {
420        if (!mShowFooter) {
421            mShowFooter = true;
422            notifyDataSetChanged();
423        }
424    }
425
426    public void hideFooter() {
427        if (mShowFooter) {
428            mShowFooter = false;
429            notifyDataSetChanged();
430        }
431    }
432
433    public void addFooter(View footerView) {
434        mFooter = footerView;
435    }
436
437    public void setFolder(Folder folder) {
438        mFolder = folder;
439    }
440
441    public void clearLeaveBehind(Conversation item) {
442        mLeaveBehindItems.remove(item.id);
443        notifyDataSetChanged();
444    }
445
446    /**
447     * @param updatedSettings
448     */
449    @Override
450    public void onSettingsChanged(Settings updatedSettings) {
451        mCachedSettings = updatedSettings;
452    }
453}
454