AnimatedAdapter.java revision 4022889525ce3ef25caabe4a8b50c7140a4bd9ed
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.View; 25import android.view.ViewGroup; 26import android.widget.AdapterView; 27import android.widget.ListView; 28import android.widget.SimpleCursorAdapter; 29 30import com.android.mail.browse.ConversationCursor; 31import com.android.mail.browse.ConversationItemView; 32import com.android.mail.browse.ConversationListFooterView; 33import com.android.mail.providers.Account; 34import com.android.mail.providers.Conversation; 35import com.android.mail.providers.Folder; 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.HashSet; 43 44public class AnimatedAdapter extends SimpleCursorAdapter implements 45 android.animation.Animator.AnimatorListener, OnUndoCancelListener { 46 private static int ITEM_VIEW_TYPE_FOOTER = 1; 47 private HashSet<Integer> mDeletingItems = new HashSet<Integer>(); 48 private Account mSelectedAccount; 49 private Context mContext; 50 private ConversationSelectionSet mBatchConversations; 51 private ActionCompleteListener mActionCompleteListener; 52 private boolean mUndo = false; 53 private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>(); 54 private ViewMode mViewMode; 55 private View mFooter; 56 private boolean mShowFooter; 57 private Folder mFolder; 58 private final ListView mListView; 59 /** 60 * Used only for debugging. 61 */ 62 private static final String LOG_TAG = new LogUtils().getLogTag(); 63 64 public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor, 65 ConversationSelectionSet batch, Account account, ViewMode viewMode, ListView listView) { 66 // Use FLAG_REGISTER_CONTENT_OBSERVER to ensure special ConversationCursor notifications 67 // (triggered by UI actions) cause any connected ListView to redraw. 68 super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null, 69 FLAG_REGISTER_CONTENT_OBSERVER); 70 mContext = context; 71 mBatchConversations = batch; 72 mSelectedAccount = account; 73 mViewMode = viewMode; 74 mShowFooter = false; 75 mListView = listView; 76 } 77 78 @Override 79 public int getCount() { 80 int count = super.getCount(); 81 return mShowFooter ? count + 1 : count; 82 } 83 84 public void setUndo(boolean state) { 85 mUndo = state; 86 if (mUndo) { 87 mDeletingItems.clear(); 88 mDeletingItems.addAll(mLastDeletingItems); 89 // Start animation 90 notifyDataSetChanged(); 91 mActionCompleteListener = new ActionCompleteListener() { 92 @Override 93 public void onActionComplete() { 94 notifyDataSetChanged(); 95 } 96 }; 97 } 98 } 99 100 @Override 101 public View newView(Context context, Cursor cursor, ViewGroup parent) { 102 ConversationItemView view = new ConversationItemView(context, mSelectedAccount.name); 103 return view; 104 } 105 106 @Override 107 public void bindView(View view, Context context, Cursor cursor) { 108 if (!isPositionAnimating(view) && !isPositionFooter(view)) { 109 ((ConversationItemView) view).bind(cursor, mViewMode, 110 mBatchConversations, mFolder); 111 } 112 } 113 114 @Override 115 public boolean hasStableIds() { 116 return true; 117 } 118 119 @Override 120 public int getViewTypeCount() { 121 // Our normal view and the animating (not recycled) view 122 return 3; 123 } 124 125 @Override 126 public int getItemViewType(int position) { 127 // Don't recycle animating views 128 if (isPositionAnimating(position)) { 129 return AdapterView.ITEM_VIEW_TYPE_IGNORE; 130 } else if (mShowFooter && position == super.getCount()) { 131 return ITEM_VIEW_TYPE_FOOTER; 132 } 133 return 0; 134 } 135 136 /** 137 * Deletes the selected conversations from the conversation list view. These conversations 138 * <b>must</b> have their {@link Conversation#position} set to the position of these 139 * conversations among the list. . This will only remove the 140 * element from the list. The job of deleting the actual element is left to the the listener. 141 * This listener will be called when the animations are complete and is required to 142 * delete the conversation. 143 * @param conversations 144 * @param listener 145 */ 146 public void delete(Collection<Conversation> conversations, 147 ActionCompleteListener listener) { 148 // Animate out the positions. 149 // Call when all the animations are complete. 150 final ArrayList<Integer> positions = new ArrayList<Integer>(); 151 for (Conversation c : conversations) { 152 positions.add(c.position); 153 } 154 delete(positions, listener); 155 } 156 157 /** 158 * Deletes a conversation with the list positions given here. This will only remove the 159 * element from the list. The job of deleting the actual elements is left to the the listener. 160 * This listener will be called when the animations are complete and is required to 161 * delete the conversations. 162 * @param deletedRows the position in the list view to be deleted. 163 * @param listener called when the animation is complete. At this point, it is safe to remove 164 * the conversations from the database. 165 */ 166 public void delete(ArrayList<Integer> deletedRows, ActionCompleteListener listener) { 167 // Clear out any remaining items and add the new ones 168 mLastDeletingItems.clear(); 169 170 int startPosition = mListView.getFirstVisiblePosition(); 171 int endPosition = mListView.getLastVisiblePosition(); 172 173 // Only animate visible items 174 for (int deletedRow: deletedRows) { 175 if (deletedRow >= startPosition && deletedRow <= endPosition) { 176 mLastDeletingItems.add(deletedRow); 177 mDeletingItems.add(deletedRow); 178 } 179 } 180 181 if (mDeletingItems.isEmpty()) { 182 // If we have no deleted items on screen, skip the animation 183 listener.onActionComplete(); 184 } else { 185 mActionCompleteListener = listener; 186 } 187 188 // TODO(viki): Rather than notifying for a full data set change, 189 // perhaps we can mark 190 // only the affected conversations? 191 notifyDataSetChanged(); 192 } 193 194 @Override 195 public View getView(int position, View convertView, ViewGroup parent) { 196 if (mShowFooter && position == super.getCount()) { 197 return mFooter; 198 } 199 if (isPositionAnimating(position)) { 200 return getAnimatingView(position, convertView, parent); 201 } 202 // TODO: do this in the swipe helper? 203 // If this view gets recycled, we need to reset things set by the 204 // animation. 205 if (convertView != null) { 206 if (convertView.getAlpha() < 1) { 207 convertView.setAlpha(1); 208 } 209 if (convertView.getTranslationX() != 0) { 210 convertView.setTranslationX(0); 211 } 212 } 213 return super.getView(position, convertView, parent); 214 } 215 216 /** 217 * Get an animating view. This happens when a list item is in the process of being removed 218 * from the list (items being deleted). 219 * @param position the position of the view inside the list 220 * @param convertView if null, a recycled view that we can reuse 221 * @param parent the parent view 222 * @return the view to show when animating an operation. 223 */ 224 private View getAnimatingView(int position, View convertView, ViewGroup parent) { 225 // We are getting the wrong view, and we need to gracefully carry on. 226 if (!(convertView instanceof AnimatingItemView)) { 227 LogUtils.d(LOG_TAG, "AnimatedAdapter.getAnimatingView received the wrong view!"); 228 convertView = null; 229 } 230 Conversation conversation = new Conversation((ConversationCursor) getItem(position)); 231 conversation.position = position; 232 if (mUndo) { 233 // The undo animation consists of fading in the conversation that 234 // had been destroyed. 235 ConversationItemView convView = (ConversationItemView) super.getView(position, null, 236 parent); 237 convView.startUndoAnimation(mViewMode, this); 238 return convView; 239 } else { 240 // Destroying a conversation just shows a blank shrinking item. 241 final AnimatingItemView view = new AnimatingItemView(mContext); 242 view.startAnimation(conversation, mViewMode, this); 243 return view; 244 } 245 } 246 247 private boolean isPositionAnimating(int position) { 248 return mDeletingItems.contains(position) 249 || (mUndo && mLastDeletingItems.contains(position)); 250 } 251 252 private boolean isPositionAnimating(View view) { 253 return (view instanceof AnimatingItemView); 254 } 255 256 private boolean isPositionFooter(View view) { 257 return (view instanceof ConversationListFooterView); 258 } 259 260 @Override 261 public void onAnimationStart(Animator animation) { 262 if (mUndo) { 263 mDeletingItems.clear(); 264 mLastDeletingItems.clear(); 265 mUndo = false; 266 } 267 } 268 269 @Override 270 public void onAnimationEnd(Animator animation) { 271 if (mUndo && !mLastDeletingItems.isEmpty()) { 272 // See if we have received all the animations we expected; if 273 // so, call the listener and reset it. 274 int position = ((ConversationItemView) ((ObjectAnimator) animation).getTarget()) 275 .getPosition(); 276 mLastDeletingItems.remove(position); 277 if (mLastDeletingItems.isEmpty()) { 278 if (mActionCompleteListener != null) { 279 mActionCompleteListener.onActionComplete(); 280 mActionCompleteListener = null; 281 } 282 mUndo = false; 283 } 284 } else if (!mDeletingItems.isEmpty()) { 285 // See if we have received all the animations we expected; if 286 // so, call the listener and reset it. 287 AnimatingItemView target = ((AnimatingItemView) ((ObjectAnimator) animation) 288 .getTarget()); 289 int position = target.getData().position; 290 mDeletingItems.remove(position); 291 if (mDeletingItems.isEmpty()) { 292 if (mActionCompleteListener != null) { 293 mActionCompleteListener.onActionComplete(); 294 mActionCompleteListener = null; 295 } 296 } 297 } 298 } 299 300 @Override 301 public boolean areAllItemsEnabled() { 302 return false; 303 } 304 305 @Override 306 public boolean isEnabled(int position) { 307 return !isPositionAnimating(position); 308 } 309 310 @Override 311 public void onAnimationCancel(Animator animation) { 312 onAnimationEnd(animation); 313 } 314 315 @Override 316 public void onAnimationRepeat(Animator animation) { 317 // TODO Auto-generated method stub 318 } 319 320 @Override 321 public void onUndoCancel() { 322 mLastDeletingItems.clear(); 323 } 324 325 public void showFooter() { 326 if (!mShowFooter) { 327 mShowFooter = true; 328 notifyDataSetChanged(); 329 } 330 } 331 332 public void hideFooter() { 333 if (mShowFooter) { 334 mShowFooter = false; 335 notifyDataSetChanged(); 336 } 337 } 338 339 public void addFooter(View footerView) { 340 mFooter = footerView; 341 } 342 343 public void setFolder(Folder folder) { 344 mFolder = folder; 345 } 346} 347