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