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