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