DefaultItemAnimator.java revision d03a495b04ee74c1cb493cee0d5998b895b1c79b
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.animation.AnimatorCompatHelper; 19import android.support.v4.view.ViewCompat; 20import android.support.v4.view.ViewPropertyAnimatorCompat; 21import android.support.v4.view.ViewPropertyAnimatorListener; 22import android.support.v7.widget.RecyclerView.ViewHolder; 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<ArrayList<ViewHolder>> mAdditionsList = 44 new ArrayList<ArrayList<ViewHolder>>(); 45 private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>(); 46 private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>(); 47 48 private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>(); 49 private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>(); 50 private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>(); 51 private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<ViewHolder>(); 52 53 private static class MoveInfo { 54 public ViewHolder holder; 55 public int fromX, fromY, toX, toY; 56 57 private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) { 58 this.holder = holder; 59 this.fromX = fromX; 60 this.fromY = fromY; 61 this.toX = toX; 62 this.toY = toY; 63 } 64 } 65 66 private static class ChangeInfo { 67 public ViewHolder oldHolder, newHolder; 68 public int fromX, fromY, toX, toY; 69 private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) { 70 this.oldHolder = oldHolder; 71 this.newHolder = newHolder; 72 } 73 74 private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder, 75 int fromX, int fromY, int toX, int toY) { 76 this(oldHolder, newHolder); 77 this.fromX = fromX; 78 this.fromY = fromY; 79 this.toX = toX; 80 this.toY = toY; 81 } 82 83 @Override 84 public String toString() { 85 return "ChangeInfo{" + 86 "oldHolder=" + oldHolder + 87 ", newHolder=" + newHolder + 88 ", fromX=" + fromX + 89 ", fromY=" + fromY + 90 ", toX=" + toX + 91 ", toY=" + toY + 92 '}'; 93 } 94 } 95 96 @Override 97 public void runPendingAnimations() { 98 boolean removalsPending = !mPendingRemovals.isEmpty(); 99 boolean movesPending = !mPendingMoves.isEmpty(); 100 boolean changesPending = !mPendingChanges.isEmpty(); 101 boolean additionsPending = !mPendingAdditions.isEmpty(); 102 if (!removalsPending && !movesPending && !additionsPending && !changesPending) { 103 // nothing to animate 104 return; 105 } 106 // First, remove stuff 107 for (ViewHolder holder : mPendingRemovals) { 108 animateRemoveImpl(holder); 109 } 110 mPendingRemovals.clear(); 111 // Next, move stuff 112 if (movesPending) { 113 final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>(); 114 moves.addAll(mPendingMoves); 115 mMovesList.add(moves); 116 mPendingMoves.clear(); 117 Runnable mover = new Runnable() { 118 @Override 119 public void run() { 120 for (MoveInfo moveInfo : moves) { 121 animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, 122 moveInfo.toX, moveInfo.toY); 123 } 124 moves.clear(); 125 mMovesList.remove(moves); 126 } 127 }; 128 if (removalsPending) { 129 View view = moves.get(0).holder.itemView; 130 ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); 131 } else { 132 mover.run(); 133 } 134 } 135 // Next, change stuff, to run in parallel with move animations 136 if (changesPending) { 137 final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>(); 138 changes.addAll(mPendingChanges); 139 mChangesList.add(changes); 140 mPendingChanges.clear(); 141 Runnable changer = new Runnable() { 142 @Override 143 public void run() { 144 for (ChangeInfo change : changes) { 145 animateChangeImpl(change); 146 } 147 changes.clear(); 148 mChangesList.remove(changes); 149 } 150 }; 151 if (removalsPending) { 152 ViewHolder holder = changes.get(0).oldHolder; 153 ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); 154 } else { 155 changer.run(); 156 } 157 } 158 // Next, add stuff 159 if (additionsPending) { 160 final ArrayList<ViewHolder> additions = new ArrayList<ViewHolder>(); 161 additions.addAll(mPendingAdditions); 162 mAdditionsList.add(additions); 163 mPendingAdditions.clear(); 164 Runnable adder = new Runnable() { 165 public void run() { 166 for (ViewHolder holder : additions) { 167 animateAddImpl(holder); 168 } 169 additions.clear(); 170 mAdditionsList.remove(additions); 171 } 172 }; 173 if (removalsPending || movesPending || changesPending) { 174 long removeDuration = removalsPending ? getRemoveDuration() : 0; 175 long moveDuration = movesPending ? getMoveDuration() : 0; 176 long changeDuration = changesPending ? getChangeDuration() : 0; 177 long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); 178 View view = additions.get(0).itemView; 179 ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); 180 } else { 181 adder.run(); 182 } 183 } 184 } 185 186 @Override 187 public boolean animateRemove(final ViewHolder holder) { 188 resetAnimation(holder); 189 mPendingRemovals.add(holder); 190 return true; 191 } 192 193 private void animateRemoveImpl(final ViewHolder holder) { 194 final View view = holder.itemView; 195 final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); 196 mRemoveAnimations.add(holder); 197 animation.setDuration(getRemoveDuration()) 198 .alpha(0).setListener(new VpaListenerAdapter() { 199 @Override 200 public void onAnimationStart(View view) { 201 dispatchRemoveStarting(holder); 202 } 203 204 @Override 205 public void onAnimationEnd(View view) { 206 animation.setListener(null); 207 ViewCompat.setAlpha(view, 1); 208 dispatchRemoveFinished(holder); 209 mRemoveAnimations.remove(holder); 210 dispatchFinishedWhenDone(); 211 } 212 }).start(); 213 } 214 215 @Override 216 public boolean animateAdd(final ViewHolder holder) { 217 resetAnimation(holder); 218 ViewCompat.setAlpha(holder.itemView, 0); 219 mPendingAdditions.add(holder); 220 return true; 221 } 222 223 private void animateAddImpl(final ViewHolder holder) { 224 final View view = holder.itemView; 225 final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); 226 mAddAnimations.add(holder); 227 animation.alpha(1).setDuration(getAddDuration()). 228 setListener(new VpaListenerAdapter() { 229 @Override 230 public void onAnimationStart(View view) { 231 dispatchAddStarting(holder); 232 } 233 @Override 234 public void onAnimationCancel(View view) { 235 ViewCompat.setAlpha(view, 1); 236 } 237 238 @Override 239 public void onAnimationEnd(View view) { 240 animation.setListener(null); 241 dispatchAddFinished(holder); 242 mAddAnimations.remove(holder); 243 dispatchFinishedWhenDone(); 244 } 245 }).start(); 246 } 247 248 @Override 249 public boolean animateMove(final ViewHolder holder, int fromX, int fromY, 250 int toX, int toY) { 251 final View view = holder.itemView; 252 fromX += ViewCompat.getTranslationX(holder.itemView); 253 fromY += ViewCompat.getTranslationY(holder.itemView); 254 resetAnimation(holder); 255 int deltaX = toX - fromX; 256 int deltaY = toY - fromY; 257 if (deltaX == 0 && deltaY == 0) { 258 dispatchMoveFinished(holder); 259 return false; 260 } 261 if (deltaX != 0) { 262 ViewCompat.setTranslationX(view, -deltaX); 263 } 264 if (deltaY != 0) { 265 ViewCompat.setTranslationY(view, -deltaY); 266 } 267 mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); 268 return true; 269 } 270 271 private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { 272 final View view = holder.itemView; 273 final int deltaX = toX - fromX; 274 final int deltaY = toY - fromY; 275 if (deltaX != 0) { 276 ViewCompat.animate(view).translationX(0); 277 } 278 if (deltaY != 0) { 279 ViewCompat.animate(view).translationY(0); 280 } 281 // TODO: make EndActions end listeners instead, since end actions aren't called when 282 // vpas are canceled (and can't end them. why?) 283 // need listener functionality in VPACompat for this. Ick. 284 final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); 285 mMoveAnimations.add(holder); 286 animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { 287 @Override 288 public void onAnimationStart(View view) { 289 dispatchMoveStarting(holder); 290 } 291 @Override 292 public void onAnimationCancel(View view) { 293 if (deltaX != 0) { 294 ViewCompat.setTranslationX(view, 0); 295 } 296 if (deltaY != 0) { 297 ViewCompat.setTranslationY(view, 0); 298 } 299 } 300 @Override 301 public void onAnimationEnd(View view) { 302 animation.setListener(null); 303 dispatchMoveFinished(holder); 304 mMoveAnimations.remove(holder); 305 dispatchFinishedWhenDone(); 306 } 307 }).start(); 308 } 309 310 @Override 311 public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, 312 int fromX, int fromY, int toX, int toY) { 313 final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); 314 final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); 315 final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); 316 resetAnimation(oldHolder); 317 int deltaX = (int) (toX - fromX - prevTranslationX); 318 int deltaY = (int) (toY - fromY - prevTranslationY); 319 // recover prev translation state after ending animation 320 ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); 321 ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); 322 ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); 323 if (newHolder != null && newHolder.itemView != null) { 324 // carry over translation values 325 resetAnimation(newHolder); 326 ViewCompat.setTranslationX(newHolder.itemView, -deltaX); 327 ViewCompat.setTranslationY(newHolder.itemView, -deltaY); 328 ViewCompat.setAlpha(newHolder.itemView, 0); 329 } 330 mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); 331 return true; 332 } 333 334 private void animateChangeImpl(final ChangeInfo changeInfo) { 335 final ViewHolder holder = changeInfo.oldHolder; 336 final View view = holder == null ? null : holder.itemView; 337 final ViewHolder newHolder = changeInfo.newHolder; 338 final View newView = newHolder != null ? newHolder.itemView : null; 339 if (view != null) { 340 final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( 341 getChangeDuration()); 342 mChangeAnimations.add(changeInfo.oldHolder); 343 oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); 344 oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); 345 oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { 346 @Override 347 public void onAnimationStart(View view) { 348 dispatchChangeStarting(changeInfo.oldHolder, true); 349 } 350 351 @Override 352 public void onAnimationEnd(View view) { 353 oldViewAnim.setListener(null); 354 ViewCompat.setAlpha(view, 1); 355 ViewCompat.setTranslationX(view, 0); 356 ViewCompat.setTranslationY(view, 0); 357 dispatchChangeFinished(changeInfo.oldHolder, true); 358 mChangeAnimations.remove(changeInfo.oldHolder); 359 dispatchFinishedWhenDone(); 360 } 361 }).start(); 362 } 363 if (newView != null) { 364 final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); 365 mChangeAnimations.add(changeInfo.newHolder); 366 newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). 367 alpha(1).setListener(new VpaListenerAdapter() { 368 @Override 369 public void onAnimationStart(View view) { 370 dispatchChangeStarting(changeInfo.newHolder, false); 371 } 372 @Override 373 public void onAnimationEnd(View view) { 374 newViewAnimation.setListener(null); 375 ViewCompat.setAlpha(newView, 1); 376 ViewCompat.setTranslationX(newView, 0); 377 ViewCompat.setTranslationY(newView, 0); 378 dispatchChangeFinished(changeInfo.newHolder, false); 379 mChangeAnimations.remove(changeInfo.newHolder); 380 dispatchFinishedWhenDone(); 381 } 382 }).start(); 383 } 384 } 385 386 private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) { 387 for (int i = infoList.size() - 1; i >= 0; i--) { 388 ChangeInfo changeInfo = infoList.get(i); 389 if (endChangeAnimationIfNecessary(changeInfo, item)) { 390 if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { 391 infoList.remove(changeInfo); 392 } 393 } 394 } 395 } 396 397 private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { 398 if (changeInfo.oldHolder != null) { 399 endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); 400 } 401 if (changeInfo.newHolder != null) { 402 endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); 403 } 404 } 405 private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) { 406 boolean oldItem = false; 407 if (changeInfo.newHolder == item) { 408 changeInfo.newHolder = null; 409 } else if (changeInfo.oldHolder == item) { 410 changeInfo.oldHolder = null; 411 oldItem = true; 412 } else { 413 return false; 414 } 415 ViewCompat.setAlpha(item.itemView, 1); 416 ViewCompat.setTranslationX(item.itemView, 0); 417 ViewCompat.setTranslationY(item.itemView, 0); 418 dispatchChangeFinished(item, oldItem); 419 return true; 420 } 421 422 @Override 423 public void endAnimation(ViewHolder item) { 424 final View view = item.itemView; 425 // this will trigger end callback which should set properties to their target values. 426 ViewCompat.animate(view).cancel(); 427 // TODO if some other animations are chained to end, how do we cancel them as well? 428 for (int i = mPendingMoves.size() - 1; i >= 0; i--) { 429 MoveInfo moveInfo = mPendingMoves.get(i); 430 if (moveInfo.holder == item) { 431 ViewCompat.setTranslationY(view, 0); 432 ViewCompat.setTranslationX(view, 0); 433 dispatchMoveFinished(item); 434 mPendingMoves.remove(i); 435 } 436 } 437 endChangeAnimation(mPendingChanges, item); 438 if (mPendingRemovals.remove(item)) { 439 ViewCompat.setAlpha(view, 1); 440 dispatchRemoveFinished(item); 441 } 442 if (mPendingAdditions.remove(item)) { 443 ViewCompat.setAlpha(view, 1); 444 dispatchAddFinished(item); 445 } 446 447 for (int i = mChangesList.size() - 1; i >= 0; i--) { 448 ArrayList<ChangeInfo> changes = mChangesList.get(i); 449 endChangeAnimation(changes, item); 450 if (changes.isEmpty()) { 451 mChangesList.remove(i); 452 } 453 } 454 for (int i = mMovesList.size() - 1; i >= 0; i--) { 455 ArrayList<MoveInfo> moves = mMovesList.get(i); 456 for (int j = moves.size() - 1; j >= 0; j--) { 457 MoveInfo moveInfo = moves.get(j); 458 if (moveInfo.holder == item) { 459 ViewCompat.setTranslationY(view, 0); 460 ViewCompat.setTranslationX(view, 0); 461 dispatchMoveFinished(item); 462 moves.remove(j); 463 if (moves.isEmpty()) { 464 mMovesList.remove(i); 465 } 466 break; 467 } 468 } 469 } 470 for (int i = mAdditionsList.size() - 1; i >= 0; i--) { 471 ArrayList<ViewHolder> additions = mAdditionsList.get(i); 472 if (additions.remove(item)) { 473 ViewCompat.setAlpha(view, 1); 474 dispatchAddFinished(item); 475 if (additions.isEmpty()) { 476 mAdditionsList.remove(i); 477 } 478 } 479 } 480 481 // animations should be ended by the cancel above. 482 if (mRemoveAnimations.remove(item) && DEBUG) { 483 throw new IllegalStateException("after animation is cancelled, item should not be in " 484 + "mRemoveAnimations list"); 485 } 486 487 if (mAddAnimations.remove(item) && DEBUG) { 488 throw new IllegalStateException("after animation is cancelled, item should not be in " 489 + "mAddAnimations list"); 490 } 491 492 if (mChangeAnimations.remove(item) && DEBUG) { 493 throw new IllegalStateException("after animation is cancelled, item should not be in " 494 + "mChangeAnimations list"); 495 } 496 497 if (mMoveAnimations.remove(item) && DEBUG) { 498 throw new IllegalStateException("after animation is cancelled, item should not be in " 499 + "mMoveAnimations list"); 500 } 501 dispatchFinishedWhenDone(); 502 } 503 504 private void resetAnimation(ViewHolder holder) { 505 AnimatorCompatHelper.clearInterpolator(holder.itemView); 506 endAnimation(holder); 507 } 508 509 @Override 510 public boolean isRunning() { 511 return (!mPendingAdditions.isEmpty() || 512 !mPendingChanges.isEmpty() || 513 !mPendingMoves.isEmpty() || 514 !mPendingRemovals.isEmpty() || 515 !mMoveAnimations.isEmpty() || 516 !mRemoveAnimations.isEmpty() || 517 !mAddAnimations.isEmpty() || 518 !mChangeAnimations.isEmpty() || 519 !mMovesList.isEmpty() || 520 !mAdditionsList.isEmpty() || 521 !mChangesList.isEmpty()); 522 } 523 524 /** 525 * Check the state of currently pending and running animations. If there are none 526 * pending/running, call {@link #dispatchAnimationsFinished()} to notify any 527 * listeners. 528 */ 529 private void dispatchFinishedWhenDone() { 530 if (!isRunning()) { 531 dispatchAnimationsFinished(); 532 } 533 } 534 535 @Override 536 public void endAnimations() { 537 int count = mPendingMoves.size(); 538 for (int i = count - 1; i >= 0; i--) { 539 MoveInfo item = mPendingMoves.get(i); 540 View view = item.holder.itemView; 541 ViewCompat.setTranslationY(view, 0); 542 ViewCompat.setTranslationX(view, 0); 543 dispatchMoveFinished(item.holder); 544 mPendingMoves.remove(i); 545 } 546 count = mPendingRemovals.size(); 547 for (int i = count - 1; i >= 0; i--) { 548 ViewHolder item = mPendingRemovals.get(i); 549 dispatchRemoveFinished(item); 550 mPendingRemovals.remove(i); 551 } 552 count = mPendingAdditions.size(); 553 for (int i = count - 1; i >= 0; i--) { 554 ViewHolder item = mPendingAdditions.get(i); 555 View view = item.itemView; 556 ViewCompat.setAlpha(view, 1); 557 dispatchAddFinished(item); 558 mPendingAdditions.remove(i); 559 } 560 count = mPendingChanges.size(); 561 for (int i = count - 1; i >= 0; i--) { 562 endChangeAnimationIfNecessary(mPendingChanges.get(i)); 563 } 564 mPendingChanges.clear(); 565 if (!isRunning()) { 566 return; 567 } 568 569 int listCount = mMovesList.size(); 570 for (int i = listCount - 1; i >= 0; i--) { 571 ArrayList<MoveInfo> moves = mMovesList.get(i); 572 count = moves.size(); 573 for (int j = count - 1; j >= 0; j--) { 574 MoveInfo moveInfo = moves.get(j); 575 ViewHolder item = moveInfo.holder; 576 View view = item.itemView; 577 ViewCompat.setTranslationY(view, 0); 578 ViewCompat.setTranslationX(view, 0); 579 dispatchMoveFinished(moveInfo.holder); 580 moves.remove(j); 581 if (moves.isEmpty()) { 582 mMovesList.remove(moves); 583 } 584 } 585 } 586 listCount = mAdditionsList.size(); 587 for (int i = listCount - 1; i >= 0; i--) { 588 ArrayList<ViewHolder> additions = mAdditionsList.get(i); 589 count = additions.size(); 590 for (int j = count - 1; j >= 0; j--) { 591 ViewHolder item = additions.get(j); 592 View view = item.itemView; 593 ViewCompat.setAlpha(view, 1); 594 dispatchAddFinished(item); 595 additions.remove(j); 596 if (additions.isEmpty()) { 597 mAdditionsList.remove(additions); 598 } 599 } 600 } 601 listCount = mChangesList.size(); 602 for (int i = listCount - 1; i >= 0; i--) { 603 ArrayList<ChangeInfo> changes = mChangesList.get(i); 604 count = changes.size(); 605 for (int j = count - 1; j >= 0; j--) { 606 endChangeAnimationIfNecessary(changes.get(j)); 607 if (changes.isEmpty()) { 608 mChangesList.remove(changes); 609 } 610 } 611 } 612 613 cancelAll(mRemoveAnimations); 614 cancelAll(mMoveAnimations); 615 cancelAll(mAddAnimations); 616 cancelAll(mChangeAnimations); 617 618 dispatchAnimationsFinished(); 619 } 620 621 void cancelAll(List<ViewHolder> viewHolders) { 622 for (int i = viewHolders.size() - 1; i >= 0; i--) { 623 ViewCompat.animate(viewHolders.get(i).itemView).cancel(); 624 } 625 } 626 627 private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { 628 @Override 629 public void onAnimationStart(View view) {} 630 631 @Override 632 public void onAnimationEnd(View view) {} 633 634 @Override 635 public void onAnimationCancel(View view) {} 636 }; 637} 638