DefaultItemAnimator.java revision e321eb5c831282c92a28a483692d8520b766a0a5
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.support.v7.widget; 17 18import android.support.v4.view.ViewCompat; 19import android.support.v4.view.ViewPropertyAnimatorCompat; 20import android.support.v4.view.ViewPropertyAnimatorListener; 21import android.support.v7.widget.RecyclerView.ViewHolder; 22import android.util.Log; 23import android.view.View; 24 25import java.util.ArrayList; 26import java.util.List; 27 28/** 29 * This implementation of {@link RecyclerView.ItemAnimator} provides basic 30 * animations on remove, add, and move events that happen to the items in 31 * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default. 32 * 33 * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) 34 */ 35public class DefaultItemAnimator extends RecyclerView.ItemAnimator { 36 private static final boolean DEBUG = false; 37 38 private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>(); 39 private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>(); 40 private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>(); 41 private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>(); 42 43 private ArrayList<ViewHolder> mAdditions = new ArrayList<ViewHolder>(); 44 private ArrayList<MoveInfo> mMoves = new ArrayList<MoveInfo>(); 45 private ArrayList<ChangeInfo> mChanges = new ArrayList<ChangeInfo>(); 46 47 private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>(); 48 private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>(); 49 private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>(); 50 private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<ViewHolder>(); 51 52 private static class MoveInfo { 53 public ViewHolder holder; 54 public int fromX, fromY, toX, toY; 55 56 private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) { 57 this.holder = holder; 58 this.fromX = fromX; 59 this.fromY = fromY; 60 this.toX = toX; 61 this.toY = toY; 62 } 63 } 64 65 private static class ChangeInfo { 66 public ViewHolder oldHolder, newHolder; 67 public int fromX, fromY, toX, toY; 68 private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) { 69 this.oldHolder = oldHolder; 70 this.newHolder = newHolder; 71 } 72 73 private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder, 74 int fromX, int fromY, int toX, int toY) { 75 this(oldHolder, newHolder); 76 this.fromX = fromX; 77 this.fromY = fromY; 78 this.toX = toX; 79 this.toY = toY; 80 } 81 82 @Override 83 public String toString() { 84 return "ChangeInfo{" + 85 "oldHolder=" + oldHolder + 86 ", newHolder=" + newHolder + 87 ", fromX=" + fromX + 88 ", fromY=" + fromY + 89 ", toX=" + toX + 90 ", toY=" + toY + 91 '}'; 92 } 93 } 94 95 @Override 96 public void runPendingAnimations() { 97 boolean removalsPending = !mPendingRemovals.isEmpty(); 98 boolean movesPending = !mPendingMoves.isEmpty(); 99 boolean changesPending = !mPendingChanges.isEmpty(); 100 boolean additionsPending = !mPendingAdditions.isEmpty(); 101 if (!removalsPending && !movesPending && !additionsPending && !changesPending) { 102 // nothing to animate 103 return; 104 } 105 // First, remove stuff 106 for (ViewHolder holder : mPendingRemovals) { 107 animateRemoveImpl(holder); 108 } 109 mPendingRemovals.clear(); 110 // Next, move stuff 111 if (movesPending) { 112 mMoves.addAll(mPendingMoves); 113 mPendingMoves.clear(); 114 Runnable mover = new Runnable() { 115 @Override 116 public void run() { 117 for (MoveInfo moveInfo : mMoves) { 118 animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, 119 moveInfo.toX, moveInfo.toY); 120 } 121 mMoves.clear(); 122 } 123 }; 124 if (removalsPending) { 125 View view = mMoves.get(0).holder.itemView; 126 ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); 127 } else { 128 mover.run(); 129 } 130 } 131 // Next, change stuff, to run in parallel with move animations 132 if (changesPending) { 133 mChanges.addAll(mPendingChanges); 134 mPendingChanges.clear(); 135 Runnable changer = new Runnable() { 136 @Override 137 public void run() { 138 for (ChangeInfo change : mChanges) { 139 animateChangeImpl(change); 140 } 141 mChanges.clear(); 142 } 143 }; 144 if (removalsPending) { 145 ViewHolder holder = mChanges.get(0).oldHolder; 146 ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); 147 } else { 148 changer.run(); 149 } 150 } 151 // Next, add stuff 152 if (additionsPending) { 153 mAdditions.addAll(mPendingAdditions); 154 mPendingAdditions.clear(); 155 Runnable adder = new Runnable() { 156 public void run() { 157 for (ViewHolder holder : mAdditions) { 158 animateAddImpl(holder); 159 } 160 mAdditions.clear(); 161 } 162 }; 163 if (removalsPending || movesPending) { 164 long removeDuration = removalsPending ? getRemoveDuration() : 0; 165 long moveDuration = movesPending ? getMoveDuration() : 0; 166 long changeDuration = changesPending ? getChangeDuration() : 0; 167 long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); 168 View view = mAdditions.get(0).itemView; 169 ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); 170 } else { 171 adder.run(); 172 } 173 } 174 } 175 176 @Override 177 public boolean animateRemove(final ViewHolder holder) { 178 endAnimation(holder); 179 mPendingRemovals.add(holder); 180 return true; 181 } 182 183 private void animateRemoveImpl(final ViewHolder holder) { 184 final View view = holder.itemView; 185 ViewCompat.animate(view).setDuration(getRemoveDuration()). 186 alpha(0).setListener(new VpaListenerAdapter() { 187 @Override 188 public void onAnimationStart(View view) { 189 dispatchRemoveStarting(holder); 190 } 191 @Override 192 public void onAnimationEnd(View view) { 193 ViewCompat.setAlpha(view, 1); 194 dispatchRemoveFinished(holder); 195 mRemoveAnimations.remove(holder); 196 dispatchFinishedWhenDone(); 197 } 198 }).start(); 199 mRemoveAnimations.add(holder); 200 } 201 202 @Override 203 public boolean animateAdd(final ViewHolder holder) { 204 endAnimation(holder); 205 ViewCompat.setAlpha(holder.itemView, 0); 206 mPendingAdditions.add(holder); 207 return true; 208 } 209 210 private void animateAddImpl(final ViewHolder holder) { 211 final View view = holder.itemView; 212 mAddAnimations.add(holder); 213 ViewCompat.animate(view).alpha(1).setDuration(getAddDuration()). 214 setListener(new VpaListenerAdapter() { 215 @Override 216 public void onAnimationStart(View view) { 217 dispatchAddStarting(holder); 218 } 219 @Override 220 public void onAnimationCancel(View view) { 221 ViewCompat.setAlpha(view, 1); 222 } 223 224 @Override 225 public void onAnimationEnd(View view) { 226 dispatchAddFinished(holder); 227 mAddAnimations.remove(holder); 228 dispatchFinishedWhenDone(); 229 } 230 }).start(); 231 } 232 233 @Override 234 public boolean animateMove(final ViewHolder holder, int fromX, int fromY, 235 int toX, int toY) { 236 final View view = holder.itemView; 237 fromX += ViewCompat.getTranslationX(holder.itemView); 238 fromY += ViewCompat.getTranslationY(holder.itemView); 239 endAnimation(holder); 240 int deltaX = toX - fromX; 241 int deltaY = toY - fromY; 242 if (deltaX == 0 && deltaY == 0) { 243 dispatchMoveFinished(holder); 244 return false; 245 } 246 if (deltaX != 0) { 247 ViewCompat.setTranslationX(view, -deltaX); 248 } 249 if (deltaY != 0) { 250 ViewCompat.setTranslationY(view, -deltaY); 251 } 252 mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); 253 return true; 254 } 255 256 private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { 257 final View view = holder.itemView; 258 final int deltaX = toX - fromX; 259 final int deltaY = toY - fromY; 260 if (deltaX != 0) { 261 ViewCompat.animate(view).translationX(0); 262 } 263 if (deltaY != 0) { 264 ViewCompat.animate(view).translationY(0); 265 } 266 // TODO: make EndActions end listeners instead, since end actions aren't called when 267 // vpas are canceled (and can't end them. why?) 268 // need listener functionality in VPACompat for this. Ick. 269 mMoveAnimations.add(holder); 270 ViewCompat.animate(view).setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { 271 @Override 272 public void onAnimationStart(View view) { 273 dispatchMoveStarting(holder); 274 } 275 @Override 276 public void onAnimationCancel(View view) { 277 if (deltaX != 0) { 278 ViewCompat.setTranslationX(view, 0); 279 } 280 if (deltaY != 0) { 281 ViewCompat.setTranslationY(view, 0); 282 } 283 } 284 @Override 285 public void onAnimationEnd(View view) { 286 dispatchMoveFinished(holder); 287 mMoveAnimations.remove(holder); 288 dispatchFinishedWhenDone(); 289 } 290 }).start(); 291 } 292 293 @Override 294 public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, 295 int fromX, int fromY, int toX, int toY) { 296 final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); 297 final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); 298 final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); 299 endAnimation(oldHolder); 300 int deltaX = (int) (toX - fromX - prevTranslationX); 301 int deltaY = (int) (toY - fromY - prevTranslationY); 302 // recover prev translation state after ending animation 303 ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); 304 ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); 305 ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); 306 if (newHolder != null && newHolder.itemView != null) { 307 // carry over translation values 308 endAnimation(newHolder); 309 ViewCompat.setTranslationX(newHolder.itemView, -deltaX); 310 ViewCompat.setTranslationY(newHolder.itemView, -deltaY); 311 ViewCompat.setAlpha(newHolder.itemView, 0); 312 } 313 mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); 314 return true; 315 } 316 317 private void animateChangeImpl(final ChangeInfo changeInfo) { 318 final ViewHolder holder = changeInfo.oldHolder; 319 final View view = holder.itemView; 320 final ViewHolder newHolder = changeInfo.newHolder; 321 final View newView = newHolder != null ? newHolder.itemView : null; 322 mChangeAnimations.add(changeInfo.oldHolder); 323 324 ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( 325 getChangeDuration()); 326 oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); 327 oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); 328 oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { 329 @Override 330 public void onAnimationStart(View view) { 331 dispatchChangeStarting(changeInfo.oldHolder, true); 332 } 333 @Override 334 public void onAnimationEnd(View view) { 335 ViewCompat.setAlpha(view, 1); 336 ViewCompat.setTranslationX(view, 0); 337 ViewCompat.setTranslationY(view, 0); 338 dispatchChangeFinished(changeInfo.oldHolder, true); 339 mChangeAnimations.remove(changeInfo.oldHolder); 340 dispatchFinishedWhenDone(); 341 } 342 }).start(); 343 if (newView != null) { 344 mChangeAnimations.add(changeInfo.newHolder); 345 ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); 346 newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). 347 alpha(1).setListener(new VpaListenerAdapter() { 348 @Override 349 public void onAnimationStart(View view) { 350 dispatchChangeStarting(changeInfo.newHolder, false); 351 } 352 @Override 353 public void onAnimationEnd(View view) { 354 ViewCompat.setAlpha(newView, 1); 355 ViewCompat.setTranslationX(newView, 0); 356 ViewCompat.setTranslationY(newView, 0); 357 dispatchChangeFinished(changeInfo.newHolder, false); 358 mChangeAnimations.remove(changeInfo.newHolder); 359 dispatchFinishedWhenDone(); 360 } 361 }).start(); 362 } 363 } 364 365 private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) { 366 for (int i = infoList.size() - 1; i >= 0; i--) { 367 ChangeInfo changeInfo = infoList.get(i); 368 if (endChangeAnimationIfNecessary(changeInfo, item)) { 369 if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { 370 infoList.remove(changeInfo); 371 } 372 } 373 } 374 } 375 376 private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { 377 if (changeInfo.oldHolder != null) { 378 endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); 379 } 380 if (changeInfo.newHolder != null) { 381 endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); 382 } 383 } 384 private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) { 385 boolean oldItem = false; 386 if (changeInfo.newHolder == item) { 387 changeInfo.newHolder = null; 388 } else if (changeInfo.oldHolder == item) { 389 changeInfo.oldHolder = null; 390 oldItem = true; 391 } else { 392 return false; 393 } 394 ViewCompat.setAlpha(item.itemView, 1); 395 ViewCompat.setTranslationX(item.itemView, 0); 396 ViewCompat.setTranslationY(item.itemView, 0); 397 dispatchChangeFinished(item, oldItem); 398 return true; 399 } 400 401 @Override 402 public void endAnimation(ViewHolder item) { 403 final View view = item.itemView; 404 // this will trigger end callback which should set properties to their target values. 405 ViewCompat.animate(view).cancel(); 406 // TODO if some other animations are chained to end, how do we cancel them as well? 407 for (int i = mPendingMoves.size() - 1; i >= 0; i--) { 408 MoveInfo moveInfo = mPendingMoves.get(i); 409 if (moveInfo.holder == item) { 410 ViewCompat.setTranslationY(view, 0); 411 ViewCompat.setTranslationX(view, 0); 412 dispatchMoveFinished(item); 413 mPendingMoves.remove(item); 414 } 415 } 416 endChangeAnimation(mPendingChanges, item); 417 if (mPendingRemovals.remove(item)) { 418 ViewCompat.setAlpha(view, 1); 419 dispatchRemoveFinished(item); 420 } 421 if (mPendingAdditions.remove(item)) { 422 ViewCompat.setAlpha(view, 1); 423 dispatchAddFinished(item); 424 } 425 endChangeAnimation(mChanges, item); 426 427 for (int i = mMoves.size() - 1; i >= 0; i--) { 428 MoveInfo moveInfo = mMoves.get(i); 429 if (moveInfo.holder == item) { 430 ViewCompat.setTranslationY(view, 0); 431 ViewCompat.setTranslationX(view, 0); 432 dispatchMoveFinished(item); 433 mMoves.remove(i); 434 break; 435 } 436 } 437 if (mAdditions.remove(item)) { 438 ViewCompat.setAlpha(view, 1); 439 dispatchAddFinished(item); 440 } 441 442 // animations should be ended by the cancel above. 443 if (mRemoveAnimations.remove(item) && DEBUG) { 444 throw new IllegalStateException("after animation is cancelled, item should not be in " 445 + "mRemoveAnimations list"); 446 } 447 448 if (mAddAnimations.remove(item) && DEBUG) { 449 throw new IllegalStateException("after animation is cancelled, item should not be in " 450 + "mAddAnimations list"); 451 } 452 453 if (mChangeAnimations.remove(item) && DEBUG) { 454 throw new IllegalStateException("after animation is cancelled, item should not be in " 455 + "mChangeAnimations list"); 456 } 457 458 if (mMoveAnimations.remove(item) && DEBUG) { 459 throw new IllegalStateException("after animation is cancelled, item should not be in " 460 + "mMoveAnimations list"); 461 } 462 dispatchFinishedWhenDone(); 463 } 464 465 @Override 466 public boolean isRunning() { 467 return (!mPendingAdditions.isEmpty() || 468 !mMoveAnimations.isEmpty() || 469 !mRemoveAnimations.isEmpty() || 470 !mAddAnimations.isEmpty() || 471 !mChangeAnimations.isEmpty() || 472 !mMoves.isEmpty() || 473 !mAdditions.isEmpty() || 474 !mChanges.isEmpty()); 475 } 476 477 /** 478 * Check the state of currently pending and running animations. If there are none 479 * pending/running, call {@link #dispatchAnimationsFinished()} to notify any 480 * listeners. 481 */ 482 private void dispatchFinishedWhenDone() { 483 if (!isRunning()) { 484 dispatchAnimationsFinished(); 485 } 486 } 487 488 @Override 489 public void endAnimations() { 490 int count = mPendingMoves.size(); 491 for (int i = count - 1; i >= 0; i--) { 492 MoveInfo item = mPendingMoves.get(i); 493 View view = item.holder.itemView; 494 ViewCompat.setTranslationY(view, 0); 495 ViewCompat.setTranslationX(view, 0); 496 dispatchMoveFinished(item.holder); 497 mPendingMoves.remove(i); 498 } 499 count = mPendingRemovals.size(); 500 for (int i = count - 1; i >= 0; i--) { 501 ViewHolder item = mPendingRemovals.get(i); 502 dispatchRemoveFinished(item); 503 mPendingRemovals.remove(i); 504 } 505 count = mPendingAdditions.size(); 506 for (int i = count - 1; i >= 0; i--) { 507 ViewHolder item = mPendingAdditions.get(i); 508 View view = item.itemView; 509 ViewCompat.setAlpha(view, 1); 510 dispatchAddFinished(item); 511 mPendingAdditions.remove(i); 512 } 513 count = mPendingChanges.size(); 514 for (int i = count - 1; i >= 0; i--) { 515 endChangeAnimationIfNecessary(mPendingChanges.get(i)); 516 } 517 mPendingChanges.clear(); 518 if (!isRunning()) { 519 return; 520 } 521 522 count = mMoves.size(); 523 for (int i = count - 1; i >= 0; i--) { 524 MoveInfo moveInfo = mMoves.get(i); 525 ViewHolder item = moveInfo.holder; 526 View view = item.itemView; 527 ViewCompat.setTranslationY(view, 0); 528 ViewCompat.setTranslationX(view, 0); 529 dispatchMoveFinished(moveInfo.holder); 530 mMoves.remove(i); 531 } 532 count = mAdditions.size(); 533 for (int i = count - 1; i >= 0; i--) { 534 ViewHolder item = mAdditions.get(i); 535 View view = item.itemView; 536 ViewCompat.setAlpha(view, 1); 537 dispatchAddFinished(item); 538 mAdditions.remove(i); 539 } 540 count = mChanges.size(); 541 for (int i = count - 1; i >= 0; i--) { 542 endChangeAnimationIfNecessary(mChanges.get(i)); 543 } 544 mChanges.clear(); 545 546 cancelAll(mRemoveAnimations); 547 cancelAll(mMoveAnimations); 548 cancelAll(mAddAnimations); 549 cancelAll(mChangeAnimations); 550 551 dispatchAnimationsFinished(); 552 } 553 554 void cancelAll(List<ViewHolder> viewHolders) { 555 for (int i = viewHolders.size() - 1; i >= 0; i--) { 556 ViewCompat.animate(viewHolders.get(i).itemView).cancel(); 557 } 558 } 559 560 private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { 561 @Override 562 public void onAnimationStart(View view) {} 563 564 @Override 565 public void onAnimationEnd(View view) {} 566 567 @Override 568 public void onAnimationCancel(View view) {} 569 }; 570} 571