AnimatedAdapter.java revision c30fe4172676a5ea3fdc0da8a0fbb917d9cf878e
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.view.LayoutInflater; 25import android.view.View; 26import android.view.ViewGroup; 27import android.widget.SimpleCursorAdapter; 28 29import com.android.mail.R; 30import com.android.mail.browse.ConversationCursor; 31import com.android.mail.browse.ConversationItemView; 32import com.android.mail.providers.Account; 33import com.android.mail.providers.Conversation; 34import com.android.mail.providers.Folder; 35import com.android.mail.providers.Settings; 36import com.android.mail.providers.UIProvider; 37import com.android.mail.ui.UndoBarView.OnUndoCancelListener; 38import com.android.mail.utils.LogUtils; 39 40import java.util.ArrayList; 41import java.util.Collection; 42import java.util.HashMap; 43import java.util.HashSet; 44 45public class AnimatedAdapter extends SimpleCursorAdapter implements 46 android.animation.Animator.AnimatorListener, OnUndoCancelListener, Settings.ChangeListener { 47 private final static int TYPE_VIEW_CONVERSATION = 0; 48 private final static int TYPE_VIEW_DELETING = 1; 49 private final static int TYPE_VIEW_UNDOING = 2; 50 private final static int TYPE_VIEW_FOOTER = 3; 51 private final static int TYPE_VIEW_LEAVEBEHIND = 4; 52 private HashSet<Integer> mDeletingItems = new HashSet<Integer>(); 53 private HashSet<Integer> mUndoingItems = new HashSet<Integer>(); 54 private Account mSelectedAccount; 55 private Context mContext; 56 private ConversationSelectionSet mBatchConversations; 57 /** 58 * The next action to perform. Do not read or write this. All accesses should 59 * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the 60 * previous action, if any. 61 */ 62 private DestructiveAction mPendingDestruction; 63 /** 64 * A destructive action that refreshes the list and performs no other action. 65 */ 66 private final DestructiveAction mRefreshAction = new DestructiveAction() { 67 @Override 68 public void performAction() { 69 notifyDataSetChanged(); 70 } 71 }; 72 73 private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>(); 74 private ViewMode mViewMode; 75 private View mFooter; 76 private boolean mShowFooter; 77 private Folder mFolder; 78 private final SwipeableListView mListView; 79 private Settings mCachedSettings; 80 private boolean mSwipeEnabled; 81 private DragListener mDragListener; 82 private HashMap<Long, LeaveBehindItem> mLeaveBehindItems = new HashMap<Long, LeaveBehindItem>(); 83 84 /** 85 * Used only for debugging. 86 */ 87 private static final String LOG_TAG = new LogUtils().getLogTag(); 88 89 public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor, 90 ConversationSelectionSet batch, Account account, Settings settings, ViewMode viewMode, 91 SwipeableListView listView, DragListener dragListener) { 92 // Use FLAG_REGISTER_CONTENT_OBSERVER to ensure special 93 // ConversationCursor notifications (triggered by UI actions) cause any 94 // connected ListView to redraw. 95 super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null, 96 FLAG_REGISTER_CONTENT_OBSERVER); 97 mContext = context; 98 mBatchConversations = batch; 99 mSelectedAccount = account; 100 mViewMode = viewMode; 101 mShowFooter = false; 102 mListView = listView; 103 mCachedSettings = settings; 104 mDragListener = dragListener; 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, mDragListener, 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, UndoOperation 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 mDragListener, 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 @Override 429 public void onUndoCancel() { 430 mLastDeletingItems.clear(); 431 } 432 433 public void showFooter() { 434 if (!mShowFooter) { 435 mShowFooter = true; 436 notifyDataSetChanged(); 437 } 438 } 439 440 public void hideFooter() { 441 if (mShowFooter) { 442 mShowFooter = false; 443 notifyDataSetChanged(); 444 } 445 } 446 447 public void addFooter(View footerView) { 448 mFooter = footerView; 449 } 450 451 public void setFolder(Folder folder) { 452 mFolder = folder; 453 } 454 455 public void clearLeaveBehind(Conversation item) { 456 mLeaveBehindItems.remove(item.id); 457 notifyDataSetChanged(); 458 } 459 460 /** 461 * @param updatedSettings 462 */ 463 @Override 464 public void onSettingsChanged(Settings updatedSettings) { 465 mCachedSettings = updatedSettings; 466 } 467} 468