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