AnimatedAdapter.java revision 4f0814f1957b2607a6243db587aa8d6e7f1fd982
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.SwipeableConversationItemView; 35import com.android.mail.providers.Account; 36import com.android.mail.providers.AccountObserver; 37import com.android.mail.providers.Conversation; 38import com.android.mail.providers.Folder; 39import com.android.mail.providers.UIProvider; 40import com.android.mail.utils.LogTag; 41import com.android.mail.utils.LogUtils; 42 43import java.util.ArrayList; 44import java.util.Collection; 45import java.util.HashMap; 46import java.util.HashSet; 47 48public class AnimatedAdapter extends SimpleCursorAdapter implements 49 android.animation.Animator.AnimatorListener { 50 private static final String LAST_DELETING_ITEMS = "last_deleting_items"; 51 private static final String LEAVE_BEHIND_ITEM = "leave_behind_item"; 52 private final static int TYPE_VIEW_CONVERSATION = 0; 53 private final static int TYPE_VIEW_DELETING = 1; 54 private final static int TYPE_VIEW_UNDOING = 2; 55 private final static int TYPE_VIEW_FOOTER = 3; 56 private final static int TYPE_VIEW_LEAVEBEHIND = 4; 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 DestructiveAction mPendingDestruction; 76 /** 77 * A destructive action that refreshes the list and performs no other action. 78 */ 79 private final DestructiveAction mRefreshAction = new DestructiveAction() { 80 @Override 81 public void performAction() { 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 @Override 163 public View newView(Context context, Cursor cursor, ViewGroup parent) { 164 SwipeableConversationItemView view = new SwipeableConversationItemView(context, 165 mAccount.name); 166 return view; 167 } 168 169 @Override 170 public void bindView(View view, Context context, Cursor cursor) { 171 if (! (view instanceof SwipeableConversationItemView)) { 172 return; 173 } 174 ((SwipeableConversationItemView) view).bind(cursor, mActivity, mBatchConversations, mFolder, 175 mAccount != null ? mAccount.settings.hideCheckboxes : false, 176 mSwipeEnabled, mPriorityMarkersEnabled, this); 177 } 178 179 @Override 180 public boolean hasStableIds() { 181 return true; 182 } 183 184 @Override 185 public int getViewTypeCount() { 186 // TYPE_VIEW_CONVERSATION, TYPE_VIEW_DELETING, TYPE_VIEW_UNDOING, and 187 // TYPE_VIEW_FOOTER, TYPE_VIEW_LEAVEBEHIND. 188 return 5; 189 } 190 191 @Override 192 public int getItemViewType(int position) { 193 // Try to recycle views. 194 if (mShowFooter && position == super.getCount()) { 195 return TYPE_VIEW_FOOTER; 196 } 197 return TYPE_VIEW_CONVERSATION; 198 } 199 200 /** 201 * Deletes the selected conversations from the conversation list view with a 202 * translation and then a shrink. These conversations <b>must</b> have their 203 * {@link Conversation#position} set to the position of these conversations 204 * among the list. This will only remove the element from the list. The job 205 * of deleting the actual element is left to the the listener. This listener 206 * will be called when the animations are complete and is required to delete 207 * the conversation. 208 * @param conversations 209 * @param listener 210 */ 211 public void swipeDelete(Collection<Conversation> conversations, DestructiveAction listener) { 212 delete(conversations, listener, mSwipeDeletingItems); 213 } 214 215 216 /** 217 * Deletes the selected conversations from the conversation list view by 218 * shrinking them away. These conversations <b>must</b> have their 219 * {@link Conversation#position} set to the position of these conversations 220 * among the list. This will only remove the element from the list. The job 221 * of deleting the actual element is left to the the listener. This listener 222 * will be called when the animations are complete and is required to delete 223 * the conversation. 224 * @param conversations 225 * @param listener 226 */ 227 public void delete(Collection<Conversation> conversations, DestructiveAction listener) { 228 delete(conversations, listener, mDeletingItems); 229 } 230 231 private void delete(Collection<Conversation> conversations, DestructiveAction action, 232 HashSet<Long> list) { 233 // Clear out any remaining items and add the new ones 234 mLastDeletingItems.clear(); 235 236 final int startPosition = mListView.getFirstVisiblePosition(); 237 final int endPosition = mListView.getLastVisiblePosition(); 238 239 // Only animate visible items 240 for (Conversation c: conversations) { 241 if (c.position >= startPosition && c.position <= endPosition) { 242 mLastDeletingItems.add(c.id); 243 list.add(c.id); 244 } 245 } 246 247 if (list.isEmpty()) { 248 // If we have no deleted items on screen, skip the animation 249 action.performAction(); 250 } else { 251 performAndSetNextAction(action); 252 } 253 254 // TODO(viki): Rather than notifying for a full data set change, 255 // perhaps we can mark 256 // only the affected conversations? 257 notifyDataSetChanged(); 258 } 259 260 @Override 261 public View getView(int position, View convertView, ViewGroup parent) { 262 if (mShowFooter && position == super.getCount()) { 263 return mFooter; 264 } 265 Conversation conv = new Conversation((ConversationCursor) getItem(position)); 266 if (isPositionUndoing(conv.id)) { 267 return getUndoingView(position, conv, parent, false /* don't show swipe background */); 268 } if (isPositionUndoingSwipe(conv.id)) { 269 return getUndoingView(position, conv, parent, true /* show swipe background */); 270 } else if (isPositionDeleting(conv.id)) { 271 return getDeletingView(position, conv, parent, false); 272 } else if (isPositionSwipeDeleting(conv.id)) { 273 return getDeletingView(position, conv, parent, true); 274 } 275 if (hasFadeLeaveBehinds()) { 276 if(isPositionFadeLeaveBehind(conv)) { 277 LeaveBehindItem fade = getFadeLeaveBehindItem(position, conv); 278 fade.startAnimation(mActivity.getViewMode(), this); 279 return fade; 280 } 281 } 282 if (hasLeaveBehinds()) { 283 if(isPositionLeaveBehind(conv)) { 284 LeaveBehindItem fadeIn = getLeaveBehindItem(conv); 285 if (hasFadeLeaveBehinds()) { 286 // Avoid the fade in and just show the text. 287 fadeIn.showTextImmediately(); 288 } else { 289 fadeIn.startFadeInAnimation(); 290 } 291 return fadeIn; 292 } 293 } 294 if (convertView != null && !(convertView instanceof SwipeableConversationItemView)) { 295 LogUtils.w(LOG_TAG, "Incorrect convert view received; nulling it out"); 296 convertView = null; 297 } else if (convertView != null) { 298 ((SwipeableConversationItemView) convertView).reset(); 299 } 300 return super.getView(position, convertView, parent); 301 } 302 303 private boolean hasLeaveBehinds() { 304 return mLeaveBehindItem != null; 305 } 306 307 private boolean hasFadeLeaveBehinds() { 308 return !mFadeLeaveBehindItems.isEmpty(); 309 } 310 311 public LeaveBehindItem setupLeaveBehind(Conversation target, ToastBarOperation undoOp, 312 int deletedRow) { 313 fadeOutLeaveBehindItems(); 314 LeaveBehindItem leaveBehind = (LeaveBehindItem) LayoutInflater.from(mContext).inflate( 315 R.layout.swipe_leavebehind, null); 316 leaveBehind.bindOperations(deletedRow, mAccount, this, undoOp, target, mFolder); 317 mLeaveBehindItem = leaveBehind; 318 mLastDeletingItems.add(target.id); 319 return leaveBehind; 320 } 321 322 public void fadeOutLeaveBehindItems() { 323 // Remove any previously existing leave behind item. 324 final int startPosition = mListView.getFirstVisiblePosition(); 325 final int endPosition = mListView.getLastVisiblePosition(); 326 327 if (hasLeaveBehinds()) { 328 // If the item is visible, fade it out. Otherwise, just remove 329 // it. 330 Conversation conv = mLeaveBehindItem.getData(); 331 if (conv.position >= startPosition && conv.position <= endPosition) { 332 mFadeLeaveBehindItems.put(conv.id, mLeaveBehindItem); 333 } else { 334 mLeaveBehindItem.commit(); 335 } 336 clearLeaveBehind(conv.id); 337 } 338 if (!mLastDeletingItems.isEmpty()) { 339 mLastDeletingItems.clear(); 340 } 341 notifyDataSetChanged(); 342 } 343 344 public SwipeableListView getListView() { 345 return mListView; 346 } 347 348 public void commitLeaveBehindItems(boolean animate) { 349 // Remove any previously existing leave behinds. 350 boolean changed = false; 351 if (hasLeaveBehinds()) { 352 if (animate) { 353 mLeaveBehindItem.dismiss(); 354 } else { 355 mLeaveBehindItem.commit(); 356 } 357 changed = true; 358 } 359 if (hasFadeLeaveBehinds() && !animate) { 360 // Find any fading leave behind items and commit them all, too. 361 for (LeaveBehindItem item : mFadeLeaveBehindItems.values()) { 362 item.commit(); 363 } 364 mFadeLeaveBehindItems.clear(); 365 changed = true; 366 } 367 if (!mLastDeletingItems.isEmpty()) { 368 mLastDeletingItems.clear(); 369 changed = true; 370 } 371 if (changed) { 372 notifyDataSetChanged(); 373 } 374 } 375 376 private LeaveBehindItem getLeaveBehindItem(Conversation target) { 377 return mLeaveBehindItem; 378 } 379 380 private LeaveBehindItem getFadeLeaveBehindItem(int position, Conversation target) { 381 return mFadeLeaveBehindItems.get(target.id); 382 } 383 384 @Override 385 public long getItemId(int position) { 386 if (mShowFooter && position == super.getCount()) { 387 return -1; 388 } 389 return super.getItemId(position); 390 } 391 392 private View getDeletingView(int position, Conversation conversation, ViewGroup parent, 393 boolean swipe) { 394 conversation.position = position; 395 SwipeableConversationItemView deletingView = mAnimatingViews.get(conversation.id); 396 if (deletingView == null) { 397 // The undo animation consists of fading in the conversation that 398 // had been destroyed. 399 deletingView = newConversationItemView(position, parent, conversation); 400 deletingView.startDeleteAnimation(this, swipe); 401 } 402 return deletingView; 403 } 404 405 private View getUndoingView(int position, Conversation conv, ViewGroup parent, boolean swipe) { 406 conv.position = position; 407 SwipeableConversationItemView undoView = mAnimatingViews.get(conv.id); 408 if (undoView == null) { 409 // The undo animation consists of fading in the conversation that 410 // had been destroyed. 411 undoView = newConversationItemView(position, parent, conv); 412 undoView.startUndoAnimation(mActivity.getViewMode(), this, swipe); 413 } 414 return undoView; 415 } 416 417 private SwipeableConversationItemView newConversationItemView(int position, ViewGroup parent, 418 Conversation conversation) { 419 SwipeableConversationItemView view = (SwipeableConversationItemView) super.getView( 420 position, null, parent); 421 view.bind(conversation, mActivity, mBatchConversations, mFolder, 422 mAccount != null ? mAccount.settings.hideCheckboxes : false, mSwipeEnabled, 423 mPriorityMarkersEnabled, this); 424 mAnimatingViews.put(conversation.id, view); 425 return view; 426 } 427 428 @Override 429 public Object getItem(int position) { 430 if (mShowFooter && position == super.getCount()) { 431 return mFooter; 432 } 433 return super.getItem(position); 434 } 435 436 private boolean isPositionDeleting(long id) { 437 return mDeletingItems.contains(id); 438 } 439 440 private boolean isPositionSwipeDeleting(long id) { 441 return mSwipeDeletingItems.contains(id); 442 } 443 444 private boolean isPositionUndoing(long id) { 445 return mUndoingItems.contains(id); 446 } 447 448 private boolean isPositionUndoingSwipe(long id) { 449 return mSwipeUndoingItems.contains(id); 450 } 451 452 private boolean isPositionUndoingType(long id) { 453 return isPositionUndoing(id) || isPositionUndoingSwipe(id); 454 } 455 456 private boolean isPositionLeaveBehind(Conversation conv) { 457 return hasLeaveBehinds() 458 && mLeaveBehindItem.getConversationId() == conv.id 459 && conv.isMostlyDead(); 460 } 461 462 private boolean isPositionFadeLeaveBehind(Conversation conv) { 463 return hasFadeLeaveBehinds() 464 && mFadeLeaveBehindItems.containsKey(conv.id) 465 && conv.isMostlyDead(); 466 } 467 468 private boolean isPositionTypeLeaveBehind(Conversation conv) { 469 if (hasLeaveBehinds()) { 470 return isPositionLeaveBehind(conv) || isPositionFadeLeaveBehind(conv); 471 } 472 return false; 473 } 474 475 @Override 476 public void onAnimationStart(Animator animation) { 477 if (!mUndoingItems.isEmpty()) { 478 mDeletingItems.clear(); 479 mLastDeletingItems.clear(); 480 mSwipeDeletingItems.clear(); 481 } else { 482 mUndoingItems.clear(); 483 } 484 } 485 486 /** 487 * Performs the pending destruction, if any and assigns the next pending action. 488 * @param next The next action that is to be performed, possibly null (if no next action is 489 * needed). 490 */ 491 private final void performAndSetNextAction(DestructiveAction next) { 492 if (mPendingDestruction != null) { 493 mPendingDestruction.performAction(); 494 } 495 mPendingDestruction = next; 496 } 497 498 @Override 499 public void onAnimationEnd(Animator animation) { 500 Object obj; 501 if (animation instanceof AnimatorSet) { 502 AnimatorSet set = (AnimatorSet) animation; 503 obj = ((ObjectAnimator) set.getChildAnimations().get(0)).getTarget(); 504 } else { 505 obj = ((ObjectAnimator) animation).getTarget(); 506 } 507 updateAnimatingConversationItems(obj, mSwipeDeletingItems); 508 updateAnimatingConversationItems(obj, mDeletingItems); 509 updateAnimatingConversationItems(obj, mSwipeUndoingItems); 510 updateAnimatingConversationItems(obj, mUndoingItems); 511 if (hasFadeLeaveBehinds() && obj instanceof LeaveBehindItem) { 512 LeaveBehindItem objItem = (LeaveBehindItem) obj; 513 clearLeaveBehind(objItem.getConversationId()); 514 objItem.commit(); 515 // The view types have changed, since the animating views are gone. 516 notifyDataSetChanged(); 517 } 518 519 if (!isAnimating()) { 520 mActivity.onAnimationEnd(this); 521 } 522 } 523 524 private void updateAnimatingConversationItems(Object obj, HashSet<Long> items) { 525 if (!items.isEmpty()) { 526 if (obj instanceof ConversationItemView) { 527 final ConversationItemView target = (ConversationItemView) obj; 528 final long id = target.getConversation().id; 529 items.remove(id); 530 mAnimatingViews.remove(id); 531 if (items.isEmpty()) { 532 performAndSetNextAction(null); 533 notifyDataSetChanged(); 534 } 535 } 536 } 537 } 538 539 @Override 540 public boolean areAllItemsEnabled() { 541 // The animating positions are not enabled. 542 return false; 543 } 544 545 @Override 546 public boolean isEnabled(int position) { 547 return !isPositionDeleting(position) && !isPositionUndoing(position); 548 } 549 550 @Override 551 public void onAnimationCancel(Animator animation) { 552 onAnimationEnd(animation); 553 } 554 555 @Override 556 public void onAnimationRepeat(Animator animation) { 557 } 558 559 public void showFooter() { 560 setFooterVisibility(true); 561 } 562 563 public void hideFooter() { 564 setFooterVisibility(false); 565 } 566 567 public void setFooterVisibility(boolean show) { 568 if (mShowFooter != show) { 569 mShowFooter = show; 570 notifyDataSetChanged(); 571 } 572 } 573 574 public void addFooter(View footerView) { 575 mFooter = footerView; 576 } 577 578 public void setFolder(Folder folder) { 579 mFolder = folder; 580 } 581 582 public void clearLeaveBehind(long itemId) { 583 if (hasLeaveBehinds() && mLeaveBehindItem.getConversationId() == itemId) { 584 mLeaveBehindItem = null; 585 } else if (hasFadeLeaveBehinds()) { 586 mFadeLeaveBehindItems.remove(itemId); 587 } else { 588 LogUtils.d(LOG_TAG, "Trying to clear a non-existant leave behind"); 589 } 590 } 591 592 public void onSaveInstanceState(Bundle outState) { 593 long[] lastDeleting = new long[mLastDeletingItems.size()]; 594 for (int i = 0; i < lastDeleting.length; i++) { 595 lastDeleting[i] = mLastDeletingItems.get(i); 596 } 597 outState.putLongArray(LAST_DELETING_ITEMS, lastDeleting); 598 if (hasLeaveBehinds()) { 599 outState.putParcelable(LEAVE_BEHIND_ITEM, mLeaveBehindItem.getLeaveBehindData()); 600 } 601 } 602 603 public void onRestoreInstanceState(Bundle outState) { 604 if (outState.containsKey(LAST_DELETING_ITEMS)) { 605 final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS); 606 for (int i = 0; i < lastDeleting.length;i++) { 607 mLastDeletingItems.add(lastDeleting[i]); 608 } 609 } 610 if (outState.containsKey(LEAVE_BEHIND_ITEM)) { 611 LeaveBehindItem.LeaveBehindData left = outState.getParcelable(LEAVE_BEHIND_ITEM); 612 LeaveBehindItem item = setupLeaveBehind(left.data, left.op, left.data.position); 613 mLeaveBehindItem = item; 614 } 615 } 616 617 /** 618 * Return if the adapter is in the process of animating anything. 619 */ 620 public boolean isAnimating() { 621 return !mUndoingItems.isEmpty() 622 || !mSwipeUndoingItems.isEmpty() 623 || hasFadeLeaveBehinds() 624 || !mDeletingItems.isEmpty() 625 || !mSwipeDeletingItems.isEmpty(); 626 } 627 628 /** 629 * Get the ConversationCursor associated with this adapter. 630 */ 631 public ConversationCursor getConversationCursor() { 632 return (ConversationCursor) getCursor(); 633 } 634} 635