blob: 473176109eb0223f4dcd1a82de60d09d104cfab7 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.ui;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import com.android.mail.browse.ConversationCursor;
import com.android.mail.browse.ConversationItemView;
import com.android.mail.browse.ConversationListFooterView;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
import com.android.mail.ui.UndoBarView.OnUndoCancelListener;
import com.android.mail.utils.LogUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
public class AnimatedAdapter extends SimpleCursorAdapter implements
android.animation.Animator.AnimatorListener, OnUndoCancelListener {
private static int ITEM_VIEW_TYPE_FOOTER = 1;
private HashSet<Integer> mDeletingItems = new HashSet<Integer>();
private Account mSelectedAccount;
private Context mContext;
private ConversationSelectionSet mBatchConversations;
private ActionCompleteListener mActionCompleteListener;
private boolean mUndo = false;
private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>();
private ViewMode mViewMode;
private View mFooter;
private boolean mShowFooter;
private Folder mFolder;
private final ListView mListView;
/**
* Used only for debugging.
*/
private static final String LOG_TAG = new LogUtils().getLogTag();
public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor,
ConversationSelectionSet batch, Account account, ViewMode viewMode, ListView listView) {
// Use FLAG_REGISTER_CONTENT_OBSERVER to ensure special ConversationCursor notifications
// (triggered by UI actions) cause any connected ListView to redraw.
super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null,
FLAG_REGISTER_CONTENT_OBSERVER);
mContext = context;
mBatchConversations = batch;
mSelectedAccount = account;
mViewMode = viewMode;
mShowFooter = false;
mListView = listView;
}
@Override
public int getCount() {
int count = super.getCount();
return mShowFooter ? count + 1 : count;
}
public void setUndo(boolean state) {
mUndo = state;
if (mUndo) {
mDeletingItems.clear();
mDeletingItems.addAll(mLastDeletingItems);
// Start animation
notifyDataSetChanged();
mActionCompleteListener = new ActionCompleteListener() {
@Override
public void onActionComplete() {
notifyDataSetChanged();
}
};
}
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ConversationItemView view = new ConversationItemView(context, mSelectedAccount.name);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (!isPositionAnimating(view) && !isPositionFooter(view)) {
((ConversationItemView) view).bind(cursor, mViewMode,
mBatchConversations, mFolder);
}
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getViewTypeCount() {
// Our normal view and the animating (not recycled) view
return 3;
}
@Override
public int getItemViewType(int position) {
// Don't recycle animating views
if (isPositionAnimating(position)) {
return AdapterView.ITEM_VIEW_TYPE_IGNORE;
} else if (mShowFooter && position == super.getCount()) {
return ITEM_VIEW_TYPE_FOOTER;
}
return 0;
}
/**
* Deletes the selected conversations from the conversation list view. These conversations
* <b>must</b> have their {@link Conversation#position} set to the position of these
* conversations among the list. . This will only remove the
* element from the list. The job of deleting the actual element is left to the the listener.
* This listener will be called when the animations are complete and is required to
* delete the conversation.
* @param conversations
* @param listener
*/
public void delete(Collection<Conversation> conversations,
ActionCompleteListener listener) {
// Animate out the positions.
// Call when all the animations are complete.
final ArrayList<Integer> positions = new ArrayList<Integer>();
for (Conversation c : conversations) {
positions.add(c.position);
}
delete(positions, listener);
}
/**
* Deletes a conversation with the list positions given here. This will only remove the
* element from the list. The job of deleting the actual elements is left to the the listener.
* This listener will be called when the animations are complete and is required to
* delete the conversations.
* @param deletedRows the position in the list view to be deleted.
* @param listener called when the animation is complete. At this point, it is safe to remove
* the conversations from the database.
*/
public void delete(ArrayList<Integer> deletedRows, ActionCompleteListener listener) {
// Clear out any remaining items and add the new ones
mLastDeletingItems.clear();
int startPosition = mListView.getFirstVisiblePosition();
int endPosition = mListView.getLastVisiblePosition();
// Only animate visible items
for (int deletedRow: deletedRows) {
if (deletedRow >= startPosition && deletedRow <= endPosition) {
mLastDeletingItems.add(deletedRow);
mDeletingItems.add(deletedRow);
}
}
if (mDeletingItems.isEmpty()) {
// If we have no deleted items on screen, skip the animation
listener.onActionComplete();
} else {
mActionCompleteListener = listener;
}
// TODO(viki): Rather than notifying for a full data set change,
// perhaps we can mark
// only the affected conversations?
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (mShowFooter && position == super.getCount()) {
return mFooter;
}
if (isPositionAnimating(position)) {
return getAnimatingView(position, convertView, parent);
}
// TODO: do this in the swipe helper?
// If this view gets recycled, we need to reset things set by the
// animation.
if (convertView != null) {
if (convertView.getAlpha() < 1) {
convertView.setAlpha(1);
}
if (convertView.getTranslationX() != 0) {
convertView.setTranslationX(0);
}
}
return super.getView(position, convertView, parent);
}
/**
* Get an animating view. This happens when a list item is in the process of being removed
* from the list (items being deleted).
* @param position the position of the view inside the list
* @param convertView if null, a recycled view that we can reuse
* @param parent the parent view
* @return the view to show when animating an operation.
*/
private View getAnimatingView(int position, View convertView, ViewGroup parent) {
// We are getting the wrong view, and we need to gracefully carry on.
if (!(convertView instanceof AnimatingItemView)) {
LogUtils.d(LOG_TAG, "AnimatedAdapter.getAnimatingView received the wrong view!");
convertView = null;
}
Conversation conversation = new Conversation((ConversationCursor) getItem(position));
conversation.position = position;
if (mUndo) {
// The undo animation consists of fading in the conversation that
// had been destroyed.
ConversationItemView convView = (ConversationItemView) super.getView(position, null,
parent);
convView.startUndoAnimation(this);
return convView;
} else {
// Destroying a conversation just shows a blank shrinking item.
final AnimatingItemView view = new AnimatingItemView(mContext);
view.startAnimation(conversation, this);
return view;
}
}
private boolean isPositionAnimating(int position) {
return mDeletingItems.contains(position)
|| (mUndo && mLastDeletingItems.contains(position));
}
private boolean isPositionAnimating(View view) {
return (view instanceof AnimatingItemView);
}
private boolean isPositionFooter(View view) {
return (view instanceof ConversationListFooterView);
}
@Override
public void onAnimationStart(Animator animation) {
if (mUndo) {
mDeletingItems.clear();
mLastDeletingItems.clear();
mUndo = false;
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (mUndo && !mLastDeletingItems.isEmpty()) {
// See if we have received all the animations we expected; if
// so, call the listener and reset it.
int position = ((ConversationItemView) ((ObjectAnimator) animation).getTarget())
.getPosition();
mLastDeletingItems.remove(position);
if (mLastDeletingItems.isEmpty()) {
if (mActionCompleteListener != null) {
mActionCompleteListener.onActionComplete();
mActionCompleteListener = null;
}
mUndo = false;
}
} else if (!mDeletingItems.isEmpty()) {
// See if we have received all the animations we expected; if
// so, call the listener and reset it.
AnimatingItemView target = ((AnimatingItemView) ((ObjectAnimator) animation)
.getTarget());
int position = target.getData().position;
mDeletingItems.remove(position);
if (mDeletingItems.isEmpty()) {
if (mActionCompleteListener != null) {
mActionCompleteListener.onActionComplete();
mActionCompleteListener = null;
}
}
}
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
return !isPositionAnimating(position);
}
@Override
public void onAnimationCancel(Animator animation) {
onAnimationEnd(animation);
}
@Override
public void onAnimationRepeat(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onUndoCancel() {
mLastDeletingItems.clear();
}
public void showFooter() {
if (!mShowFooter) {
mShowFooter = true;
notifyDataSetChanged();
}
}
public void hideFooter() {
if (mShowFooter) {
mShowFooter = false;
notifyDataSetChanged();
}
}
public void addFooter(View footerView) {
mFooter = footerView;
}
public void setFolder(Folder folder) {
mFolder = folder;
}
}