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