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