AnimatedAdapter.java revision e949616a8516115a544e1a8fca42d8d9e2817419
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.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.content.Context; 24import android.database.Cursor; 25import android.os.Bundle; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.ViewGroup; 29import android.widget.SimpleCursorAdapter; 30 31import com.android.mail.R; 32import com.android.mail.browse.ConversationCursor; 33import com.android.mail.browse.ConversationItemView; 34import com.android.mail.browse.ConversationItemViewCoordinates; 35import com.android.mail.browse.SwipeableConversationItemView; 36import com.android.mail.providers.Account; 37import com.android.mail.providers.AccountObserver; 38import com.android.mail.providers.Conversation; 39import com.android.mail.providers.Folder; 40import com.android.mail.providers.UIProvider; 41import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener; 42import com.android.mail.utils.LogTag; 43import com.android.mail.utils.LogUtils; 44 45import java.util.ArrayList; 46import java.util.Collection; 47import java.util.HashMap; 48import java.util.HashSet; 49 50public class AnimatedAdapter extends SimpleCursorAdapter implements 51 android.animation.Animator.AnimatorListener { 52 private static final String LAST_DELETING_ITEMS = "last_deleting_items"; 53 private static final String LEAVE_BEHIND_ITEM = "leave_behind_item"; 54 private final static int TYPE_VIEW_CONVERSATION = 0; 55 private final static int TYPE_VIEW_FOOTER = 1; 56 private final static int TYPE_VIEW_DONT_RECYCLE = -1; 57 private final HashSet<Long> mDeletingItems = new HashSet<Long>(); 58 private final ArrayList<Long> mLastDeletingItems = new ArrayList<Long>(); 59 private final HashSet<Long> mUndoingItems = new HashSet<Long>(); 60 private final HashSet<Long> mSwipeDeletingItems = new HashSet<Long>(); 61 private final HashSet<Long> mSwipeUndoingItems = new HashSet<Long>(); 62 private final HashMap<Long, SwipeableConversationItemView> mAnimatingViews = 63 new HashMap<Long, SwipeableConversationItemView>(); 64 private final HashMap<Long, LeaveBehindItem> mFadeLeaveBehindItems = 65 new HashMap<Long, LeaveBehindItem>(); 66 /** The current account */ 67 private Account mAccount; 68 private final Context mContext; 69 private final ConversationSelectionSet mBatchConversations; 70 /** 71 * The next action to perform. Do not read or write this. All accesses should 72 * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the 73 * previous action, if any. 74 */ 75 private ListItemsRemovedListener mPendingDestruction; 76 /** 77 * A destructive action that refreshes the list and performs no other action. 78 */ 79 private final ListItemsRemovedListener mRefreshAction = new ListItemsRemovedListener() { 80 @Override 81 public void onListItemsRemoved() { 82 notifyDataSetChanged(); 83 } 84 }; 85 86 public interface Listener { 87 void onAnimationEnd(AnimatedAdapter adapter); 88 } 89 90 private View mFooter; 91 private boolean mShowFooter; 92 private Folder mFolder; 93 private final SwipeableListView mListView; 94 private boolean mSwipeEnabled; 95 private LeaveBehindItem mLeaveBehindItem; 96 /** True if priority inbox markers are enabled, false otherwise. */ 97 private boolean mPriorityMarkersEnabled; 98 private ControllableActivity mActivity; 99 private final AccountObserver mAccountListener = new AccountObserver() { 100 @Override 101 public void onChanged(Account newAccount) { 102 setAccount(newAccount); 103 notifyDataSetChanged(); 104 } 105 }; 106 107 private final void setAccount(Account newAccount) { 108 mAccount = newAccount; 109 mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled; 110 mSwipeEnabled = mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO); 111 } 112 113 /** 114 * Used only for debugging. 115 */ 116 private static final String LOG_TAG = LogTag.getLogTag(); 117 118 public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor, 119 ConversationSelectionSet batch, 120 ControllableActivity activity, SwipeableListView listView) { 121 super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0); 122 mContext = context; 123 mBatchConversations = batch; 124 setAccount(mAccountListener.initialize(activity.getAccountController())); 125 mActivity = activity; 126 mShowFooter = false; 127 mListView = listView; 128 } 129 130 public final void destroy() { 131 // Set a null cursor in the adapter 132 swapCursor(null); 133 mAccountListener.unregisterAndDestroy(); 134 } 135 136 @Override 137 public int getCount() { 138 final int count = super.getCount(); 139 return mShowFooter ? count + 1 : count; 140 } 141 142 public void setUndo(boolean undo) { 143 if (undo && !mLastDeletingItems.isEmpty()) { 144 mUndoingItems.addAll(mLastDeletingItems); 145 mLastDeletingItems.clear(); 146 // Start animation 147 notifyDataSetChanged(); 148 performAndSetNextAction(mRefreshAction); 149 } 150 } 151 152 public void setSwipeUndo(boolean undo) { 153 if (undo && !mLastDeletingItems.isEmpty()) { 154 mSwipeUndoingItems.addAll(mLastDeletingItems); 155 mLastDeletingItems.clear(); 156 // Start animation 157 notifyDataSetChanged(); 158 performAndSetNextAction(mRefreshAction); 159 } 160 } 161 162 public View createConversationItemView(SwipeableConversationItemView view, Context context, 163 Conversation conv) { 164 if (view == null) { 165 view = new SwipeableConversationItemView(context, mAccount.name); 166 } 167 view.bind(conv, mActivity, mBatchConversations, mFolder, 168 mAccount != null ? mAccount.settings.hideCheckboxes : false, mSwipeEnabled, 169 mPriorityMarkersEnabled, this); 170 return view; 171 } 172 173 @Override 174 public boolean hasStableIds() { 175 return true; 176 } 177 178 @Override 179 public int getViewTypeCount() { 180 // TYPE_VIEW_CONVERSATION, TYPE_VIEW_DELETING, TYPE_VIEW_UNDOING, and 181 // TYPE_VIEW_FOOTER, TYPE_VIEW_LEAVEBEHIND. 182 return 5; 183 } 184 185 @Override 186 public int getItemViewType(int position) { 187 // Try to recycle views. 188 if (mShowFooter && position == super.getCount()) { 189 return TYPE_VIEW_FOOTER; 190 } else if (hasLeaveBehinds() || isAnimating()) { 191 // Setting as type -1 means the recycler won't take this view and 192 // return it in get view. This is a bit of a "hammer" in that it 193 // won't let even safe views be recycled here, 194 // but its safer and cheaper than trying to determine individual 195 // types. In a future release, use position/id map to try to make 196 // this cleaner / faster to determine if the view is animating. 197 return TYPE_VIEW_DONT_RECYCLE; 198 } 199 return TYPE_VIEW_CONVERSATION; 200 } 201 202 /** 203 * Deletes the selected conversations from the conversation list view with a 204 * translation and then a shrink. These conversations <b>must</b> have their 205 * {@link Conversation#position} set to the position of these conversations 206 * among the list. This will only remove the element from the list. The job 207 * of deleting the actual element is left to the the listener. This listener 208 * will be called when the animations are complete and is required to delete 209 * the conversation. 210 * @param conversations 211 * @param listener 212 */ 213 public void swipeDelete(Collection<Conversation> conversations, 214 ListItemsRemovedListener listener) { 215 delete(conversations, listener, mSwipeDeletingItems); 216 } 217 218 219 /** 220 * Deletes the selected conversations from the conversation list view by 221 * shrinking them away. These conversations <b>must</b> have their 222 * {@link Conversation#position} set to the position of these conversations 223 * among the list. This will only remove the element from the list. The job 224 * of deleting the actual element is left to the the listener. This listener 225 * will be called when the animations are complete and is required to delete 226 * the conversation. 227 * @param conversations 228 * @param listener 229 */ 230 public void delete(Collection<Conversation> conversations, ListItemsRemovedListener listener) { 231 delete(conversations, listener, mDeletingItems); 232 } 233 234 private void delete(Collection<Conversation> conversations, ListItemsRemovedListener listener, 235 HashSet<Long> list) { 236 // Clear out any remaining items and add the new ones 237 mLastDeletingItems.clear(); 238 // Since we are deleting new items, clear any remaining undo items 239 mUndoingItems.clear(); 240 241 final int startPosition = mListView.getFirstVisiblePosition(); 242 final int endPosition = mListView.getLastVisiblePosition(); 243 244 // Only animate visible items 245 for (Conversation c: conversations) { 246 if (c.position >= startPosition && c.position <= endPosition) { 247 mLastDeletingItems.add(c.id); 248 list.add(c.id); 249 } 250 } 251 252 if (list.isEmpty()) { 253 // If we have no deleted items on screen, skip the animation 254 listener.onListItemsRemoved(); 255 } else { 256 performAndSetNextAction(listener); 257 } 258 notifyDataSetChanged(); 259 } 260 261 @Override 262 public View getView(int position, View convertView, ViewGroup parent) { 263 if (mShowFooter && position == super.getCount()) { 264 return mFooter; 265 } 266 ConversationCursor cursor = (ConversationCursor) getItem(position); 267 Conversation conv = new Conversation(cursor); 268 if (isPositionUndoing(conv.id)) { 269 return getUndoingView(position, conv, parent, false /* don't show swipe background */); 270 } if (isPositionUndoingSwipe(conv.id)) { 271 return getUndoingView(position, conv, parent, true /* show swipe background */); 272 } else if (isPositionDeleting(conv.id)) { 273 return getDeletingView(position, conv, parent, false); 274 } else if (isPositionSwipeDeleting(conv.id)) { 275 return getDeletingView(position, conv, parent, true); 276 } 277 if (hasFadeLeaveBehinds()) { 278 if(isPositionFadeLeaveBehind(conv)) { 279 LeaveBehindItem fade = getFadeLeaveBehindItem(position, conv); 280 fade.startAnimation(mActivity.getViewMode(), this); 281 return fade; 282 } 283 } 284 if (hasLeaveBehinds()) { 285 if(isPositionLeaveBehind(conv)) { 286 LeaveBehindItem fadeIn = getLeaveBehindItem(conv); 287 if (hasFadeLeaveBehinds()) { 288 // Avoid the fade in and just show the text. 289 fadeIn.showTextImmediately(); 290 } else { 291 fadeIn.startFadeInAnimation(); 292 } 293 return fadeIn; 294 } 295 } 296 if (convertView != null && !(convertView instanceof SwipeableConversationItemView)) { 297 LogUtils.w(LOG_TAG, "Incorrect convert view received; nulling it out"); 298 convertView = newView(mContext, cursor, parent); 299 } else if (convertView != null) { 300 ((SwipeableConversationItemView) convertView).reset(); 301 } 302 return createConversationItemView((SwipeableConversationItemView) convertView, mContext, 303 conv); 304 } 305 306 private boolean hasLeaveBehinds() { 307 return mLeaveBehindItem != null; 308 } 309 310 private boolean hasFadeLeaveBehinds() { 311 return !mFadeLeaveBehindItems.isEmpty(); 312 } 313 314 public LeaveBehindItem setupLeaveBehind(Conversation target, ToastBarOperation undoOp, 315 int deletedRow) { 316 fadeOutLeaveBehindItems(); 317 boolean isWide = ConversationItemViewCoordinates.isWideMode(ConversationItemViewCoordinates 318 .getMode(mContext, mActivity.getViewMode())); 319 LeaveBehindItem leaveBehind = (LeaveBehindItem) LayoutInflater.from(mContext).inflate( 320 isWide? R.layout.swipe_leavebehind_wide : R.layout.swipe_leavebehind, null); 321 leaveBehind.bindOperations(deletedRow, mAccount, this, undoOp, target, mFolder); 322 mLeaveBehindItem = leaveBehind; 323 mLastDeletingItems.add(target.id); 324 return leaveBehind; 325 } 326 327 public void fadeOutLeaveBehindItems() { 328 // Remove any previously existing leave behind item. 329 final int startPosition = mListView.getFirstVisiblePosition(); 330 final int endPosition = mListView.getLastVisiblePosition(); 331 332 if (hasLeaveBehinds()) { 333 // If the item is visible, fade it out. Otherwise, just remove 334 // it. 335 Conversation conv = mLeaveBehindItem.getData(); 336 if (conv.position >= startPosition && conv.position <= endPosition) { 337 mFadeLeaveBehindItems.put(conv.id, mLeaveBehindItem); 338 } else { 339 mLeaveBehindItem.commit(); 340 } 341 clearLeaveBehind(conv.id); 342 } 343 if (!mLastDeletingItems.isEmpty()) { 344 mLastDeletingItems.clear(); 345 } 346 notifyDataSetChanged(); 347 } 348 349 public SwipeableListView getListView() { 350 return mListView; 351 } 352 353 public void commitLeaveBehindItems(boolean animate) { 354 // Remove any previously existing leave behinds. 355 boolean changed = false; 356 if (hasLeaveBehinds()) { 357 if (animate) { 358 mLeaveBehindItem.dismiss(); 359 } else { 360 mLeaveBehindItem.commit(); 361 } 362 changed = true; 363 } 364 if (hasFadeLeaveBehinds() && !animate) { 365 // Find any fading leave behind items and commit them all, too. 366 for (LeaveBehindItem item : mFadeLeaveBehindItems.values()) { 367 item.commit(); 368 } 369 mFadeLeaveBehindItems.clear(); 370 changed = true; 371 } 372 if (!mLastDeletingItems.isEmpty()) { 373 mLastDeletingItems.clear(); 374 changed = true; 375 } 376 if (changed) { 377 notifyDataSetChanged(); 378 } 379 } 380 381 private LeaveBehindItem getLeaveBehindItem(Conversation target) { 382 return mLeaveBehindItem; 383 } 384 385 private LeaveBehindItem getFadeLeaveBehindItem(int position, Conversation target) { 386 return mFadeLeaveBehindItems.get(target.id); 387 } 388 389 @Override 390 public long getItemId(int position) { 391 if (mShowFooter && position == super.getCount()) { 392 return -1; 393 } 394 return super.getItemId(position); 395 } 396 397 private View getDeletingView(int position, Conversation conversation, ViewGroup parent, 398 boolean swipe) { 399 conversation.position = position; 400 SwipeableConversationItemView deletingView = mAnimatingViews.get(conversation.id); 401 if (deletingView == null) { 402 // The undo animation consists of fading in the conversation that 403 // had been destroyed. 404 deletingView = newConversationItemView(position, parent, conversation); 405 deletingView.startDeleteAnimation(this, swipe); 406 } 407 return deletingView; 408 } 409 410 private View getUndoingView(int position, Conversation conv, ViewGroup parent, boolean swipe) { 411 conv.position = position; 412 SwipeableConversationItemView undoView = mAnimatingViews.get(conv.id); 413 if (undoView == null) { 414 // The undo animation consists of fading in the conversation that 415 // had been destroyed. 416 undoView = newConversationItemView(position, parent, conv); 417 undoView.startUndoAnimation(mActivity.getViewMode(), this, swipe); 418 } 419 return undoView; 420 } 421 422 @Override 423 public View newView(Context context, Cursor cursor, ViewGroup parent) { 424 SwipeableConversationItemView view = new SwipeableConversationItemView(context, 425 mAccount.name); 426 return view; 427 } 428 429 @Override 430 public void bindView(View view, Context context, Cursor cursor) { 431 if (! (view instanceof SwipeableConversationItemView)) { 432 return; 433 } 434 ((SwipeableConversationItemView) view).bind(cursor, mActivity, mBatchConversations, mFolder, 435 mAccount != null ? mAccount.settings.hideCheckboxes : false, 436 mSwipeEnabled, mPriorityMarkersEnabled, this); 437 } 438 439 private SwipeableConversationItemView newConversationItemView(int position, ViewGroup parent, 440 Conversation conversation) { 441 SwipeableConversationItemView view = (SwipeableConversationItemView) super.getView( 442 position, null, parent); 443 view.reset(); 444 view.bind(conversation, mActivity, mBatchConversations, mFolder, 445 mAccount != null ? mAccount.settings.hideCheckboxes : false, mSwipeEnabled, 446 mPriorityMarkersEnabled, this); 447 mAnimatingViews.put(conversation.id, view); 448 return view; 449 } 450 451 @Override 452 public Object getItem(int position) { 453 if (mShowFooter && position == super.getCount()) { 454 return mFooter; 455 } 456 return super.getItem(position); 457 } 458 459 private boolean isPositionDeleting(long id) { 460 return mDeletingItems.contains(id); 461 } 462 463 private boolean isPositionSwipeDeleting(long id) { 464 return mSwipeDeletingItems.contains(id); 465 } 466 467 private boolean isPositionUndoing(long id) { 468 return mUndoingItems.contains(id); 469 } 470 471 private boolean isPositionUndoingSwipe(long id) { 472 return mSwipeUndoingItems.contains(id); 473 } 474 475 private boolean isPositionUndoingType(long id) { 476 return isPositionUndoing(id) || isPositionUndoingSwipe(id); 477 } 478 479 private boolean isPositionLeaveBehind(Conversation conv) { 480 return hasLeaveBehinds() 481 && mLeaveBehindItem.getConversationId() == conv.id 482 && conv.isMostlyDead(); 483 } 484 485 private boolean isPositionFadeLeaveBehind(Conversation conv) { 486 return hasFadeLeaveBehinds() 487 && mFadeLeaveBehindItems.containsKey(conv.id) 488 && conv.isMostlyDead(); 489 } 490 491 private boolean isPositionTypeLeaveBehind(Conversation conv) { 492 if (hasLeaveBehinds()) { 493 return isPositionLeaveBehind(conv) || isPositionFadeLeaveBehind(conv); 494 } 495 return false; 496 } 497 498 @Override 499 public void onAnimationStart(Animator animation) { 500 if (!mUndoingItems.isEmpty()) { 501 mDeletingItems.clear(); 502 mLastDeletingItems.clear(); 503 mSwipeDeletingItems.clear(); 504 } else { 505 mUndoingItems.clear(); 506 } 507 } 508 509 /** 510 * Performs the pending destruction, if any and assigns the next pending action. 511 * @param next The next action that is to be performed, possibly null (if no next action is 512 * needed). 513 */ 514 private final void performAndSetNextAction(ListItemsRemovedListener next) { 515 if (mPendingDestruction != null) { 516 mPendingDestruction.onListItemsRemoved(); 517 } 518 mPendingDestruction = next; 519 } 520 521 @Override 522 public void onAnimationEnd(Animator animation) { 523 Object obj; 524 if (animation instanceof AnimatorSet) { 525 AnimatorSet set = (AnimatorSet) animation; 526 obj = ((ObjectAnimator) set.getChildAnimations().get(0)).getTarget(); 527 } else { 528 obj = ((ObjectAnimator) animation).getTarget(); 529 } 530 updateAnimatingConversationItems(obj, mSwipeDeletingItems); 531 updateAnimatingConversationItems(obj, mDeletingItems); 532 updateAnimatingConversationItems(obj, mSwipeUndoingItems); 533 updateAnimatingConversationItems(obj, mUndoingItems); 534 if (hasFadeLeaveBehinds() && obj instanceof LeaveBehindItem) { 535 LeaveBehindItem objItem = (LeaveBehindItem) obj; 536 clearLeaveBehind(objItem.getConversationId()); 537 objItem.commit(); 538 // The view types have changed, since the animating views are gone. 539 notifyDataSetChanged(); 540 } 541 542 if (!isAnimating()) { 543 mActivity.onAnimationEnd(this); 544 } 545 } 546 547 private void updateAnimatingConversationItems(Object obj, HashSet<Long> items) { 548 if (!items.isEmpty()) { 549 if (obj instanceof ConversationItemView) { 550 final ConversationItemView target = (ConversationItemView) obj; 551 final long id = target.getConversation().id; 552 items.remove(id); 553 mAnimatingViews.remove(id); 554 if (items.isEmpty()) { 555 performAndSetNextAction(null); 556 notifyDataSetChanged(); 557 } 558 } 559 } 560 } 561 562 @Override 563 public boolean areAllItemsEnabled() { 564 // The animating positions are not enabled. 565 return false; 566 } 567 568 @Override 569 public boolean isEnabled(int position) { 570 return !isPositionDeleting(position) && !isPositionUndoing(position); 571 } 572 573 @Override 574 public void onAnimationCancel(Animator animation) { 575 onAnimationEnd(animation); 576 } 577 578 @Override 579 public void onAnimationRepeat(Animator animation) { 580 } 581 582 public void showFooter() { 583 setFooterVisibility(true); 584 } 585 586 public void hideFooter() { 587 setFooterVisibility(false); 588 } 589 590 public void setFooterVisibility(boolean show) { 591 if (mShowFooter != show) { 592 mShowFooter = show; 593 notifyDataSetChanged(); 594 } 595 } 596 597 public void addFooter(View footerView) { 598 mFooter = footerView; 599 } 600 601 public void setFolder(Folder folder) { 602 mFolder = folder; 603 } 604 605 public void clearLeaveBehind(long itemId) { 606 if (hasLeaveBehinds() && mLeaveBehindItem.getConversationId() == itemId) { 607 mLeaveBehindItem = null; 608 } else if (hasFadeLeaveBehinds()) { 609 mFadeLeaveBehindItems.remove(itemId); 610 } else { 611 LogUtils.d(LOG_TAG, "Trying to clear a non-existant leave behind"); 612 } 613 } 614 615 public void onSaveInstanceState(Bundle outState) { 616 long[] lastDeleting = new long[mLastDeletingItems.size()]; 617 for (int i = 0; i < lastDeleting.length; i++) { 618 lastDeleting[i] = mLastDeletingItems.get(i); 619 } 620 outState.putLongArray(LAST_DELETING_ITEMS, lastDeleting); 621 if (hasLeaveBehinds()) { 622 outState.putParcelable(LEAVE_BEHIND_ITEM, mLeaveBehindItem.getLeaveBehindData()); 623 } 624 } 625 626 public void onRestoreInstanceState(Bundle outState) { 627 if (outState.containsKey(LAST_DELETING_ITEMS)) { 628 final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS); 629 for (int i = 0; i < lastDeleting.length;i++) { 630 mLastDeletingItems.add(lastDeleting[i]); 631 } 632 } 633 if (outState.containsKey(LEAVE_BEHIND_ITEM)) { 634 LeaveBehindData left = outState.getParcelable(LEAVE_BEHIND_ITEM); 635 LeaveBehindItem item = setupLeaveBehind(left.data, left.op, left.data.position); 636 mLeaveBehindItem = item; 637 } 638 } 639 640 /** 641 * Return if the adapter is in the process of animating anything. 642 */ 643 public boolean isAnimating() { 644 return !mUndoingItems.isEmpty() 645 || !mSwipeUndoingItems.isEmpty() 646 || hasFadeLeaveBehinds() 647 || !mDeletingItems.isEmpty() 648 || !mSwipeDeletingItems.isEmpty(); 649 } 650 651 /** 652 * Get the ConversationCursor associated with this adapter. 653 */ 654 public ConversationCursor getConversationCursor() { 655 return (ConversationCursor) getCursor(); 656 } 657} 658