AnimatedAdapter.java revision b8ef4ac43f3570245ec476e2ee9f8a9d1d3ff36c
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.ObjectAnimator; 22import android.content.Context; 23import android.database.Cursor; 24import android.os.Bundle; 25import android.view.LayoutInflater; 26import android.view.View; 27import android.view.ViewGroup; 28import android.widget.SimpleCursorAdapter; 29 30import com.android.mail.R; 31import com.android.mail.browse.ConversationCursor; 32import com.android.mail.browse.ConversationItemView; 33import com.android.mail.providers.Account; 34import com.android.mail.providers.Conversation; 35import com.android.mail.providers.Folder; 36import com.android.mail.providers.Settings; 37import com.android.mail.providers.UIProvider; 38import com.android.mail.utils.LogTag; 39import com.android.mail.utils.LogUtils; 40 41import java.util.ArrayList; 42import java.util.Collection; 43import java.util.HashMap; 44import java.util.HashSet; 45 46public class AnimatedAdapter extends SimpleCursorAdapter implements 47 android.animation.Animator.AnimatorListener, Settings.ChangeListener { 48 private static final String LAST_DELETING_ITEMS = "last-deleting-items"; 49 private final static int TYPE_VIEW_CONVERSATION = 0; 50 private final static int TYPE_VIEW_DELETING = 1; 51 private final static int TYPE_VIEW_UNDOING = 2; 52 private final static int TYPE_VIEW_FOOTER = 3; 53 private final static int TYPE_VIEW_LEAVEBEHIND = 4; 54 private HashSet<Integer> mDeletingItems = new HashSet<Integer>(); 55 private HashSet<Integer> mUndoingItems = new HashSet<Integer>(); 56 private Account mSelectedAccount; 57 private Context mContext; 58 private ConversationSelectionSet mBatchConversations; 59 /** 60 * The next action to perform. Do not read or write this. All accesses should 61 * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the 62 * previous action, if any. 63 */ 64 private DestructiveAction mPendingDestruction; 65 /** 66 * A destructive action that refreshes the list and performs no other action. 67 */ 68 private final DestructiveAction mRefreshAction = new DestructiveAction() { 69 @Override 70 public void performAction() { 71 notifyDataSetChanged(); 72 } 73 }; 74 75 private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>(); 76 private ViewMode mViewMode; 77 private View mFooter; 78 private boolean mShowFooter; 79 private Folder mFolder; 80 private final SwipeableListView mListView; 81 private Settings mCachedSettings; 82 private boolean mSwipeEnabled; 83 private HashMap<Long, LeaveBehindItem> mLeaveBehindItems = new HashMap<Long, LeaveBehindItem>(); 84 85 /** 86 * Used only for debugging. 87 */ 88 private static final String LOG_TAG = LogTag.getLogTag(); 89 90 public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor, 91 ConversationSelectionSet batch, Account account, Settings settings, ViewMode viewMode, 92 SwipeableListView listView) { 93 // Use FLAG_REGISTER_CONTENT_OBSERVER to ensure special 94 // ConversationCursor notifications (triggered by UI actions) cause any 95 // connected ListView to redraw. 96 super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null, 97 FLAG_REGISTER_CONTENT_OBSERVER); 98 mContext = context; 99 mBatchConversations = batch; 100 mSelectedAccount = account; 101 mViewMode = viewMode; 102 mShowFooter = false; 103 mListView = listView; 104 mCachedSettings = settings; 105 mSwipeEnabled = account.supportsCapability(UIProvider.AccountCapabilities.UNDO); 106 } 107 108 @Override 109 public int getCount() { 110 final int count = super.getCount(); 111 return mShowFooter ? count + 1 : count; 112 } 113 114 public void setUndo(boolean undo) { 115 if (undo) { 116 mUndoingItems.addAll(mLastDeletingItems); 117 mLastDeletingItems.clear(); 118 // Start animation 119 notifyDataSetChanged(); 120 performAndSetNextAction(mRefreshAction); 121 } 122 } 123 124 @Override 125 public View newView(Context context, Cursor cursor, ViewGroup parent) { 126 ConversationItemView view = new ConversationItemView(context, mSelectedAccount.name); 127 return view; 128 } 129 130 @Override 131 public void bindView(View view, Context context, Cursor cursor) { 132 if (! (view instanceof ConversationItemView)) { 133 return; 134 } 135 ((ConversationItemView) view).bind(cursor, mViewMode, mBatchConversations, mFolder, 136 mCachedSettings != null ? mCachedSettings.hideCheckboxes : false, 137 mSwipeEnabled, this); 138 } 139 140 @Override 141 public boolean hasStableIds() { 142 return true; 143 } 144 145 @Override 146 public int getViewTypeCount() { 147 // TYPE_VIEW_CONVERSATION, TYPE_VIEW_DELETING, TYPE_VIEW_UNDOING, and 148 // TYPE_VIEW_FOOTER, TYPE_VIEW_LEAVEBEHIND. 149 return 5; 150 } 151 152 @Override 153 public int getItemViewType(int position) { 154 // Try to recycle views. 155 if (isPositionDeleting(position)) { 156 return TYPE_VIEW_DELETING; 157 } 158 if (isPositionUndoing(position)) { 159 return TYPE_VIEW_UNDOING; 160 } 161 if (mShowFooter && position == super.getCount()) { 162 return TYPE_VIEW_FOOTER; 163 } 164 if (isPositionLeaveBehind(position)) { 165 return TYPE_VIEW_LEAVEBEHIND; 166 } 167 return TYPE_VIEW_CONVERSATION; 168 } 169 170 /** 171 * Deletes the selected conversations from the conversation list view. These conversations 172 * <b>must</b> have their {@link Conversation#position} set to the position of these 173 * conversations among the list. . This will only remove the 174 * element from the list. The job of deleting the actual element is left to the the listener. 175 * This listener will be called when the animations are complete and is required to 176 * delete the conversation. 177 * @param conversations 178 * @param listener 179 */ 180 public void delete(Collection<Conversation> conversations, DestructiveAction listener) { 181 // Animate out the positions. 182 // Call when all the animations are complete. 183 final ArrayList<Integer> positions = new ArrayList<Integer>(); 184 for (Conversation c : conversations) { 185 positions.add(c.position); 186 } 187 delete(positions, listener); 188 } 189 190 /** 191 * Deletes a conversation with the list positions given here. This will only remove the 192 * element from the list. The job of deleting the actual elements is left to the the listener. 193 * This listener will be called when the animations are complete and is required to 194 * delete the conversations. 195 * @param deletedRows the position in the list view to be deleted. 196 * @param action the destructive action that modifies the database. 197 */ 198 public void delete(ArrayList<Integer> deletedRows, DestructiveAction action) { 199 // Clear out any remaining items and add the new ones 200 mLastDeletingItems.clear(); 201 202 final int startPosition = mListView.getFirstVisiblePosition(); 203 final int endPosition = mListView.getLastVisiblePosition(); 204 205 // Only animate visible items 206 for (int deletedRow: deletedRows) { 207 if (deletedRow >= startPosition && deletedRow <= endPosition) { 208 mLastDeletingItems.add(deletedRow); 209 mDeletingItems.add(deletedRow); 210 } 211 } 212 213 if (mDeletingItems.isEmpty()) { 214 // If we have no deleted items on screen, skip the animation 215 action.performAction(); 216 } else { 217 performAndSetNextAction(action); 218 } 219 220 // TODO(viki): Rather than notifying for a full data set change, 221 // perhaps we can mark 222 // only the affected conversations? 223 notifyDataSetChanged(); 224 } 225 226 @Override 227 public View getView(int position, View convertView, ViewGroup parent) { 228 if (mShowFooter && position == super.getCount()) { 229 return mFooter; 230 } 231 if (isPositionUndoing(position)) { 232 return getUndoingView(position, convertView, parent); 233 } else if (isPositionDeleting(position)) { 234 return getDeletingView(position, convertView, parent); 235 } 236 if (hasLeaveBehinds()) { 237 Conversation conv = new Conversation((ConversationCursor) getItem(position)); 238 if(isPositionLeaveBehind(conv)) { 239 return getLeaveBehindItem(position, conv); 240 } 241 } 242 // TODO: do this in the swipe helper? 243 // If this view gets recycled, we need to reset things set by the 244 // animation. 245 if (convertView != null) { 246 if (convertView.getAlpha() < 1) { 247 convertView.setAlpha(1); 248 } 249 if (convertView.getTranslationX() != 0) { 250 convertView.setTranslationX(0); 251 } 252 } 253 return super.getView(position, convertView, parent); 254 } 255 256 private boolean hasLeaveBehinds() { 257 return !mLeaveBehindItems.isEmpty(); 258 } 259 260 public void setupLeaveBehind(Conversation target, ToastBarOperation undoOp, int deletedRow) { 261 commitLeaveBehindItems(); 262 LeaveBehindItem leaveBehind = (LeaveBehindItem) LayoutInflater.from(mContext).inflate( 263 R.layout.swipe_leavebehind, null); 264 leaveBehind.bindOperations(mSelectedAccount, this, undoOp, 265 target); 266 mLeaveBehindItems.put(target.id, leaveBehind); 267 mLastDeletingItems.add(deletedRow); 268 } 269 270 public void commitLeaveBehindItems() { 271 // Remove any previously existing leave behinds. 272 if (!mLeaveBehindItems.isEmpty()) { 273 for (LeaveBehindItem item : mLeaveBehindItems.values()) { 274 item.commit(); 275 } 276 mLeaveBehindItems.clear(); 277 } 278 notifyDataSetChanged(); 279 } 280 281 private LeaveBehindItem getLeaveBehindItem(int position, Conversation target) { 282 return mLeaveBehindItems.get(target.id); 283 } 284 285 @Override 286 public long getItemId(int position) { 287 if (mShowFooter && position == super.getCount()) { 288 return -1; 289 } 290 return super.getItemId(position); 291 } 292 293 /** 294 * Get an animating view. This happens when a list item is in the process of being removed 295 * from the list (items being deleted). 296 * @param position the position of the view inside the list 297 * @param convertView if null, a recycled view that we can reuse 298 * @param parent the parent view 299 * @return the view to show when animating an operation. 300 */ 301 private View getDeletingView(int position, View convertView, ViewGroup parent) { 302 // We are getting the wrong view, and we need to gracefully carry on. 303 if (convertView != null && !(convertView instanceof AnimatingItemView)) { 304 LogUtils.d(LOG_TAG, "AnimatedAdapter.getAnimatingView received the wrong view!"); 305 convertView = null; 306 } 307 Conversation conversation = new Conversation((ConversationCursor) getItem(position)); 308 conversation.position = position; 309 // Destroying a conversation just shows a blank shrinking item. 310 final AnimatingItemView view = new AnimatingItemView(mContext); 311 view.startAnimation(conversation, mViewMode, this); 312 return view; 313 } 314 315 private View getUndoingView(int position, View convertView, ViewGroup parent) { 316 Conversation conversation = new Conversation((ConversationCursor) getItem(position)); 317 conversation.position = position; 318 // The undo animation consists of fading in the conversation that 319 // had been destroyed. 320 final ConversationItemView convView = (ConversationItemView) super.getView(position, null, 321 parent); 322 convView.bind(conversation, mViewMode, mBatchConversations, mFolder, 323 mCachedSettings != null ? mCachedSettings.hideCheckboxes : false, mSwipeEnabled, 324 this); 325 convView.startUndoAnimation(mViewMode, this); 326 return convView; 327 } 328 329 @Override 330 public Object getItem(int position) { 331 if (mShowFooter && position == super.getCount()) { 332 return mFooter; 333 } 334 return super.getItem(position); 335 } 336 337 private boolean isPositionDeleting(int position) { 338 return mDeletingItems.contains(position); 339 } 340 341 private boolean isPositionUndoing(int position) { 342 return mUndoingItems.contains(position); 343 } 344 345 private boolean isPositionLeaveBehind(Conversation conv) { 346 return mLeaveBehindItems.containsKey(conv.id) && conv.isMostlyDead(); 347 } 348 349 private boolean isPositionLeaveBehind(int position) { 350 if (hasLeaveBehinds()) { 351 Object item = getItem(position); 352 if (item instanceof ConversationCursor) { 353 Conversation conv = new Conversation((ConversationCursor) item); 354 return mLeaveBehindItems.containsKey(conv.id) && conv.isMostlyDead(); 355 } 356 } 357 return false; 358 } 359 360 @Override 361 public void onAnimationStart(Animator animation) { 362 if (!mUndoingItems.isEmpty()) { 363 mDeletingItems.clear(); 364 mLastDeletingItems.clear(); 365 } else { 366 mUndoingItems.clear(); 367 } 368 } 369 370 /** 371 * Performs the pending destruction, if any and assigns the next pending action. 372 * @param next The next action that is to be performed, possibly null (if no next action is 373 * needed). 374 */ 375 private final void performAndSetNextAction(DestructiveAction next) { 376 if (mPendingDestruction != null) { 377 mPendingDestruction.performAction(); 378 } 379 mPendingDestruction = next; 380 } 381 382 @Override 383 public void onAnimationEnd(Animator animation) { 384 if (!mUndoingItems.isEmpty()) { 385 // See if we have received all the animations we expected; if 386 // so, call the listener and reset it. 387 final int position = ((ConversationItemView) ((ObjectAnimator) animation).getTarget()) 388 .getPosition(); 389 mUndoingItems.remove(position); 390 if (mUndoingItems.isEmpty()) { 391 performAndSetNextAction(null); 392 } 393 } else if (!mDeletingItems.isEmpty()) { 394 // See if we have received all the animations we expected; if 395 // so, call the listener and reset it. 396 final AnimatingItemView target = ((AnimatingItemView) ((ObjectAnimator) animation) 397 .getTarget()); 398 final int position = target.getData().position; 399 mDeletingItems.remove(position); 400 if (mDeletingItems.isEmpty()) { 401 performAndSetNextAction(null); 402 } 403 } 404 // The view types have changed, since the animating views are gone. 405 notifyDataSetChanged(); 406 } 407 408 @Override 409 public boolean areAllItemsEnabled() { 410 // The animating positions are not enabled. 411 return false; 412 } 413 414 @Override 415 public boolean isEnabled(int position) { 416 return !isPositionDeleting(position) && !isPositionUndoing(position); 417 } 418 419 @Override 420 public void onAnimationCancel(Animator animation) { 421 onAnimationEnd(animation); 422 } 423 424 @Override 425 public void onAnimationRepeat(Animator animation) { 426 } 427 428 public void showFooter() { 429 if (!mShowFooter) { 430 mShowFooter = true; 431 notifyDataSetChanged(); 432 } 433 } 434 435 public void hideFooter() { 436 if (mShowFooter) { 437 mShowFooter = false; 438 notifyDataSetChanged(); 439 } 440 } 441 442 public void addFooter(View footerView) { 443 mFooter = footerView; 444 } 445 446 public void setFolder(Folder folder) { 447 mFolder = folder; 448 } 449 450 public void clearLeaveBehind(Conversation item) { 451 mLeaveBehindItems.remove(item.id); 452 notifyDataSetChanged(); 453 } 454 455 /** 456 * @param updatedSettings 457 */ 458 @Override 459 public void onSettingsChanged(Settings updatedSettings) { 460 mCachedSettings = updatedSettings; 461 } 462 463 public void onSaveInstanceState(Bundle outState) { 464 int[] lastDeleting = new int[mLastDeletingItems.size()]; 465 for (int i = 0; i < lastDeleting.length;i++) { 466 lastDeleting[i] = mLastDeletingItems.get(i); 467 } 468 outState.putIntArray(LAST_DELETING_ITEMS, lastDeleting); 469 } 470 471 472 public void onRestoreInstanceState(Bundle outState) { 473 if (outState.containsKey(LAST_DELETING_ITEMS)) { 474 final int[] lastDeleting = outState.getIntArray(LAST_DELETING_ITEMS); 475 for (int i = 0; i < lastDeleting.length;i++) { 476 mLastDeletingItems.add(lastDeleting[i]); 477 } 478 } 479 } 480} 481