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