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