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