1 /* <lambda>null2 * Copyright (C) 2022 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 */ 16 17 package com.android.systemui.animation 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ObjectAnimator 22 import android.animation.PropertyValuesHolder 23 import android.animation.ValueAnimator 24 import android.util.IntProperty 25 import android.view.View 26 import android.view.ViewGroup 27 import android.view.animation.Interpolator 28 import com.android.app.animation.Interpolators 29 import kotlin.math.max 30 import kotlin.math.min 31 32 /** 33 * A class that allows changes in bounds within a view hierarchy to animate seamlessly between the 34 * start and end state. 35 */ 36 class ViewHierarchyAnimator { 37 companion object { 38 /** Default values for the animation. These can all be overridden at call time. */ 39 private const val DEFAULT_DURATION = 500L 40 private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD 41 private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE 42 private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE 43 private val DEFAULT_FADE_IN_INTERPOLATOR = Interpolators.ALPHA_IN 44 45 /** The properties used to animate the view bounds. */ 46 private val PROPERTIES = 47 mapOf( 48 Bound.LEFT to createViewProperty(Bound.LEFT), 49 Bound.TOP to createViewProperty(Bound.TOP), 50 Bound.RIGHT to createViewProperty(Bound.RIGHT), 51 Bound.BOTTOM to createViewProperty(Bound.BOTTOM), 52 ) 53 54 private fun createViewProperty(bound: Bound): IntProperty<View> { 55 return object : IntProperty<View>(bound.label) { 56 override fun setValue(view: View, value: Int) { 57 setBound(view, bound, value) 58 } 59 60 override fun get(view: View): Int { 61 return getBound(view, bound) ?: bound.getValue(view) 62 } 63 } 64 } 65 66 /** 67 * Instruct the animator to watch for changes to the layout of [rootView] and its children 68 * and animate them. It uses the given [interpolator] and [duration]. 69 * 70 * If a new layout change happens while an animation is already in progress, the animation 71 * is updated to continue from the current values to the new end state. 72 * 73 * By default, child views whole layout changes are animated as well. However, this can be 74 * controlled by [animateChildren]. If children are included, a set of [excludedViews] can 75 * be passed. If any dependent view from [rootView] matches an entry in this set, changes to 76 * that view will not be animated. 77 * 78 * The animator continues to respond to layout changes until [stopAnimating] is called. 79 * 80 * Successive calls to this method override the previous settings ([interpolator] and 81 * [duration]). The changes take effect on the next animation. 82 * 83 * Returns true if the [rootView] is already visible and will be animated, false otherwise. 84 * To animate the addition of a view, see [animateAddition]. 85 */ 86 @JvmOverloads 87 fun animate( 88 rootView: View, 89 interpolator: Interpolator = DEFAULT_INTERPOLATOR, 90 duration: Long = DEFAULT_DURATION, 91 animateChildren: Boolean = true, 92 excludedViews: Set<View> = emptySet(), 93 ): Boolean { 94 return animate( 95 rootView, 96 interpolator, 97 duration, 98 ephemeral = false, 99 animateChildren = animateChildren, 100 excludedViews = excludedViews, 101 ) 102 } 103 104 /** 105 * Like [animate], but only takes effect on the next layout update, then unregisters itself 106 * once the first animation is complete. 107 */ 108 @JvmOverloads 109 fun animateNextUpdate( 110 rootView: View, 111 interpolator: Interpolator = DEFAULT_INTERPOLATOR, 112 duration: Long = DEFAULT_DURATION, 113 animateChildren: Boolean = true, 114 excludedViews: Set<View> = emptySet(), 115 ): Boolean { 116 return animate( 117 rootView, 118 interpolator, 119 duration, 120 ephemeral = true, 121 animateChildren = animateChildren, 122 excludedViews = excludedViews, 123 ) 124 } 125 126 private fun animate( 127 rootView: View, 128 interpolator: Interpolator, 129 duration: Long, 130 ephemeral: Boolean, 131 animateChildren: Boolean, 132 excludedViews: Set<View> = emptySet(), 133 ): Boolean { 134 if ( 135 !occupiesSpace( 136 rootView.visibility, 137 rootView.left, 138 rootView.top, 139 rootView.right, 140 rootView.bottom, 141 ) 142 ) { 143 return false 144 } 145 146 val listener = createUpdateListener(interpolator, duration, ephemeral) 147 addListener( 148 rootView, 149 listener, 150 recursive = true, 151 animateChildren = animateChildren, 152 excludedViews = excludedViews, 153 ) 154 return true 155 } 156 157 /** 158 * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation 159 * using [interpolator] and [duration]. 160 * 161 * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise 162 * it keeps listening for further updates. 163 */ 164 private fun createUpdateListener( 165 interpolator: Interpolator, 166 duration: Long, 167 ephemeral: Boolean, 168 ): View.OnLayoutChangeListener { 169 return createListener(interpolator, duration, ephemeral) 170 } 171 172 /** 173 * Instruct the animator to stop watching for changes to the layout of [rootView] and its 174 * children. 175 * 176 * Any animations already in progress continue until their natural conclusion. 177 */ 178 fun stopAnimating(rootView: View) { 179 recursivelyRemoveListener(rootView) 180 } 181 182 /** 183 * Instruct the animator to watch for changes to the layout of [rootView] and its children, 184 * and animate the next time the hierarchy appears after not being visible. It uses the 185 * given [interpolator] and [duration]. 186 * 187 * The start state of the animation is controlled by [origin]. This value can be any of the 188 * four corners, any of the four edges, or the center of the view. If any margins are added 189 * on the side(s) of the origin, the translation of those margins can be included by 190 * specifying [includeMargins]. 191 * 192 * Returns true if the [rootView] is invisible and will be animated, false otherwise. To 193 * animate an already visible view, see [animate] and [animateNextUpdate]. 194 * 195 * Then animator unregisters itself once the first addition animation is complete. 196 * 197 * @param includeFadeIn true if the animator should also fade in the view and child views. 198 * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if 199 * [includeFadeIn] is false. 200 * @param onAnimationEnd an optional runnable that will be run once the animation finishes, 201 * regardless of whether the animation is cancelled or finishes successfully. 202 */ 203 @JvmOverloads 204 fun animateAddition( 205 rootView: View, 206 origin: Hotspot = Hotspot.CENTER, 207 interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR, 208 duration: Long = DEFAULT_DURATION, 209 includeMargins: Boolean = false, 210 includeFadeIn: Boolean = false, 211 fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR, 212 onAnimationEnd: Runnable? = null, 213 ): Boolean { 214 if ( 215 occupiesSpace( 216 rootView.visibility, 217 rootView.left, 218 rootView.top, 219 rootView.right, 220 rootView.bottom, 221 ) 222 ) { 223 return false 224 } 225 226 val listener = 227 createAdditionListener( 228 origin, 229 interpolator, 230 duration, 231 ignorePreviousValues = !includeMargins, 232 onAnimationEnd, 233 ) 234 addListener(rootView, listener, recursive = true) 235 236 if (!includeFadeIn) { 237 return true 238 } 239 240 if (rootView is ViewGroup) { 241 // First, fade in the container view 242 val containerDuration = duration / 6 243 createAndStartFadeInAnimator( 244 rootView, 245 containerDuration, 246 startDelay = 0, 247 interpolator = fadeInInterpolator, 248 ) 249 250 // Then, fade in the child views 251 val childDuration = duration / 3 252 for (i in 0 until rootView.childCount) { 253 val view = rootView.getChildAt(i) 254 createAndStartFadeInAnimator( 255 view, 256 childDuration, 257 // Wait until the container fades in before fading in the children 258 startDelay = containerDuration, 259 interpolator = fadeInInterpolator, 260 ) 261 } 262 // For now, we don't recursively fade in additional sub views (e.g. grandchild 263 // views) since it hasn't been necessary, but we could add that functionality. 264 } else { 265 // Fade in the view during the first half of the addition 266 createAndStartFadeInAnimator( 267 rootView, 268 duration / 2, 269 startDelay = 0, 270 interpolator = fadeInInterpolator, 271 ) 272 } 273 274 return true 275 } 276 277 /** 278 * Returns a new [View.OnLayoutChangeListener] that on the next call triggers a layout 279 * addition animation from the given [origin], using [interpolator] and [duration]. 280 * 281 * If [ignorePreviousValues] is true, the animation will only span the area covered by the 282 * new bounds. Otherwise it will include the margins between the previous and new bounds. 283 */ 284 private fun createAdditionListener( 285 origin: Hotspot, 286 interpolator: Interpolator, 287 duration: Long, 288 ignorePreviousValues: Boolean, 289 onAnimationEnd: Runnable? = null, 290 ): View.OnLayoutChangeListener { 291 return createListener( 292 interpolator, 293 duration, 294 ephemeral = true, 295 origin = origin, 296 ignorePreviousValues = ignorePreviousValues, 297 onAnimationEnd, 298 ) 299 } 300 301 /** 302 * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation 303 * using [interpolator] and [duration]. 304 * 305 * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise 306 * it keeps listening for further updates. 307 * 308 * [origin] specifies whether the start values should be determined by a hotspot, and 309 * [ignorePreviousValues] controls whether the previous values should be taken into account. 310 */ 311 private fun createListener( 312 interpolator: Interpolator, 313 duration: Long, 314 ephemeral: Boolean, 315 origin: Hotspot? = null, 316 ignorePreviousValues: Boolean = false, 317 onAnimationEnd: Runnable? = null, 318 ): View.OnLayoutChangeListener { 319 return object : View.OnLayoutChangeListener { 320 override fun onLayoutChange( 321 view: View?, 322 left: Int, 323 top: Int, 324 right: Int, 325 bottom: Int, 326 previousLeft: Int, 327 previousTop: Int, 328 previousRight: Int, 329 previousBottom: Int, 330 ) { 331 if (view == null) return 332 333 val startLeft = getBound(view, Bound.LEFT) ?: previousLeft 334 val startTop = getBound(view, Bound.TOP) ?: previousTop 335 val startRight = getBound(view, Bound.RIGHT) ?: previousRight 336 val startBottom = getBound(view, Bound.BOTTOM) ?: previousBottom 337 338 (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() 339 340 if (!occupiesSpace(view.visibility, left, top, right, bottom)) { 341 setBound(view, Bound.LEFT, left) 342 setBound(view, Bound.TOP, top) 343 setBound(view, Bound.RIGHT, right) 344 setBound(view, Bound.BOTTOM, bottom) 345 return 346 } 347 348 val startValues = 349 processStartValues( 350 origin, 351 left, 352 top, 353 right, 354 bottom, 355 startLeft, 356 startTop, 357 startRight, 358 startBottom, 359 ignorePreviousValues, 360 ) 361 val endValues = 362 mapOf( 363 Bound.LEFT to left, 364 Bound.TOP to top, 365 Bound.RIGHT to right, 366 Bound.BOTTOM to bottom, 367 ) 368 369 val boundsToAnimate = mutableSetOf<Bound>() 370 if (startValues.getValue(Bound.LEFT) != left) boundsToAnimate.add(Bound.LEFT) 371 if (startValues.getValue(Bound.TOP) != top) boundsToAnimate.add(Bound.TOP) 372 if (startValues.getValue(Bound.RIGHT) != right) boundsToAnimate.add(Bound.RIGHT) 373 if (startValues.getValue(Bound.BOTTOM) != bottom) { 374 boundsToAnimate.add(Bound.BOTTOM) 375 } 376 377 if (boundsToAnimate.isNotEmpty()) { 378 startAnimation( 379 view, 380 boundsToAnimate, 381 startValues, 382 endValues, 383 interpolator, 384 duration, 385 ephemeral, 386 onAnimationEnd, 387 ) 388 } 389 } 390 } 391 } 392 393 /** 394 * Animates the removal of [rootView] and its children from the hierarchy. It uses the given 395 * [interpolator] and [duration]. 396 * 397 * The end state of the animation is controlled by [destination]. This value can be any of 398 * the four corners, any of the four edges, or the center of the view. If any margins are 399 * added on the side(s) of the [destination], the translation of those margins can be 400 * included by specifying [includeMargins]. 401 * 402 * @param onAnimationEnd an optional runnable that will be run once the animation finishes, 403 * regardless of whether the animation is cancelled or finishes successfully. 404 */ 405 @JvmOverloads 406 fun animateRemoval( 407 rootView: View, 408 destination: Hotspot = Hotspot.CENTER, 409 interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR, 410 duration: Long = DEFAULT_DURATION, 411 includeMargins: Boolean = false, 412 onAnimationEnd: Runnable? = null, 413 ): Boolean { 414 if ( 415 !occupiesSpace( 416 rootView.visibility, 417 rootView.left, 418 rootView.top, 419 rootView.right, 420 rootView.bottom, 421 ) 422 ) { 423 return false 424 } 425 426 val parent = rootView.parent as ViewGroup 427 428 // Ensure that rootView's siblings animate nicely around the removal. 429 val listener = createUpdateListener(interpolator, duration, ephemeral = true) 430 for (i in 0 until parent.childCount) { 431 val child = parent.getChildAt(i) 432 if (child == rootView) continue 433 addListener(child, listener, recursive = false) 434 } 435 436 val viewHasSiblings = parent.childCount > 1 437 if (viewHasSiblings) { 438 // Remove the view so that a layout update is triggered for the siblings and they 439 // animate to their next position while the view's removal is also animating. 440 parent.removeView(rootView) 441 // By adding the view to the overlay, we can animate it while it isn't part of the 442 // view hierarchy. It is correctly positioned because we have its previous bounds, 443 // and we set them manually during the animation. 444 parent.overlay.add(rootView) 445 } 446 // If this view has no siblings, the parent view may shrink to (0,0) size and mess 447 // up the animation if we immediately remove the view. So instead, we just leave the 448 // view in the real hierarchy until the animation finishes. 449 450 val endRunnable = Runnable { 451 if (viewHasSiblings) { 452 parent.overlay.remove(rootView) 453 } else { 454 parent.removeView(rootView) 455 } 456 onAnimationEnd?.run() 457 } 458 459 val startValues = 460 mapOf( 461 Bound.LEFT to rootView.left, 462 Bound.TOP to rootView.top, 463 Bound.RIGHT to rootView.right, 464 Bound.BOTTOM to rootView.bottom, 465 ) 466 val endValues = 467 processEndValuesForRemoval( 468 destination, 469 rootView, 470 rootView.left, 471 rootView.top, 472 rootView.right, 473 rootView.bottom, 474 includeMargins, 475 ) 476 477 val boundsToAnimate = mutableSetOf<Bound>() 478 if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) 479 if (rootView.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) 480 if (rootView.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) 481 if (rootView.bottom != endValues.getValue(Bound.BOTTOM)) { 482 boundsToAnimate.add(Bound.BOTTOM) 483 } 484 485 startAnimation( 486 rootView, 487 boundsToAnimate, 488 startValues, 489 endValues, 490 interpolator, 491 duration, 492 ephemeral = true, 493 endRunnable, 494 ) 495 496 if (rootView is ViewGroup) { 497 // Shift the children so they maintain a consistent position within the shrinking 498 // view. 499 shiftChildrenForRemoval(rootView, destination, endValues, interpolator, duration) 500 501 // Fade out the children during the first half of the removal, so they don't clutter 502 // too much once the view becomes very small. Then we fade out the view itself, in 503 // case it has its own content and/or background. 504 val startAlphas = FloatArray(rootView.childCount) 505 for (i in 0 until rootView.childCount) { 506 startAlphas[i] = rootView.getChildAt(i).alpha 507 } 508 509 val animator = ValueAnimator.ofFloat(1f, 0f) 510 animator.interpolator = Interpolators.ALPHA_OUT 511 animator.duration = duration / 2 512 animator.addUpdateListener { animation -> 513 for (i in 0 until rootView.childCount) { 514 rootView.getChildAt(i).alpha = 515 (animation.animatedValue as Float) * startAlphas[i] 516 } 517 } 518 animator.addListener( 519 object : AnimatorListenerAdapter() { 520 override fun onAnimationEnd(animation: Animator) { 521 rootView 522 .animate() 523 .alpha(0f) 524 .setInterpolator(Interpolators.ALPHA_OUT) 525 .setDuration(duration / 2) 526 .start() 527 } 528 } 529 ) 530 animator.start() 531 } else { 532 // Fade out the view during the second half of the removal. 533 rootView 534 .animate() 535 .alpha(0f) 536 .setInterpolator(Interpolators.ALPHA_OUT) 537 .setDuration(duration / 2) 538 .setStartDelay(duration / 2) 539 .start() 540 } 541 542 return true 543 } 544 545 /** 546 * Animates the children of [rootView] so that its layout remains internally consistent as 547 * it shrinks towards [destination] and changes its bounds to [endValues]. 548 * 549 * Uses [interpolator] and [duration], which should match those of the removal animation. 550 */ 551 private fun shiftChildrenForRemoval( 552 rootView: ViewGroup, 553 destination: Hotspot, 554 endValues: Map<Bound, Int>, 555 interpolator: Interpolator, 556 duration: Long, 557 ) { 558 for (i in 0 until rootView.childCount) { 559 val child = rootView.getChildAt(i) 560 val childStartValues = 561 mapOf( 562 Bound.LEFT to child.left, 563 Bound.TOP to child.top, 564 Bound.RIGHT to child.right, 565 Bound.BOTTOM to child.bottom, 566 ) 567 val childEndValues = 568 processChildEndValuesForRemoval( 569 destination, 570 child.left, 571 child.top, 572 child.right, 573 child.bottom, 574 endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT), 575 endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP), 576 ) 577 578 val boundsToAnimate = mutableSetOf<Bound>() 579 if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) 580 if (child.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) 581 if (child.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) 582 if (child.bottom != endValues.getValue(Bound.BOTTOM)) { 583 boundsToAnimate.add(Bound.BOTTOM) 584 } 585 586 startAnimation( 587 child, 588 boundsToAnimate, 589 childStartValues, 590 childEndValues, 591 interpolator, 592 duration, 593 ephemeral = true, 594 ) 595 } 596 } 597 598 /** 599 * Returns whether the given [visibility] and bounds are consistent with a view being a 600 * contributing part of the hierarchy. 601 */ 602 private fun occupiesSpace( 603 visibility: Int, 604 left: Int, 605 top: Int, 606 right: Int, 607 bottom: Int, 608 ): Boolean { 609 return visibility != View.GONE && left != right && top != bottom 610 } 611 612 /** 613 * Computes the actual starting values based on the requested [origin] and on 614 * [ignorePreviousValues]. 615 * 616 * If [origin] is null, the resolved start values will be the same as those passed in, or 617 * the same as the new values if [ignorePreviousValues] is true. If [origin] is not null, 618 * the start values are resolved based on it, and [ignorePreviousValues] controls whether or 619 * not newly introduced margins are included. 620 * 621 * Base case 622 * 623 * ``` 624 * 1) origin=TOP 625 * x---------x x---------x x---------x x---------x x---------x 626 * x---------x | | | | | | 627 * -> -> x---------x -> | | -> | | 628 * x---------x | | 629 * x---------x 630 * 2) origin=BOTTOM_LEFT 631 * x---------x 632 * x-------x | | 633 * -> -> x----x -> | | -> | | 634 * x--x | | | | | | 635 * x x--x x----x x-------x x---------x 636 * 3) origin=CENTER 637 * x---------x 638 * x-----x x-------x | | 639 * x -> x---x -> | | -> | | -> | | 640 * x-----x x-------x | | 641 * x---------x 642 * ``` 643 * 644 * In case the start and end values differ in the direction of the origin, and 645 * [ignorePreviousValues] is false, the previous values are used and a translation is 646 * included in addition to the view expansion. 647 * 648 * ``` 649 * origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70) 650 * x 651 * x--x 652 * x--x x----x 653 * -> -> | | -> x------x 654 * x----x | | 655 * | | 656 * x------x 657 * ``` 658 */ 659 private fun processStartValues( 660 origin: Hotspot?, 661 newLeft: Int, 662 newTop: Int, 663 newRight: Int, 664 newBottom: Int, 665 previousLeft: Int, 666 previousTop: Int, 667 previousRight: Int, 668 previousBottom: Int, 669 ignorePreviousValues: Boolean, 670 ): Map<Bound, Int> { 671 val startLeft = if (ignorePreviousValues) newLeft else previousLeft 672 val startTop = if (ignorePreviousValues) newTop else previousTop 673 val startRight = if (ignorePreviousValues) newRight else previousRight 674 val startBottom = if (ignorePreviousValues) newBottom else previousBottom 675 676 var left = startLeft 677 var top = startTop 678 var right = startRight 679 var bottom = startBottom 680 681 if (origin != null) { 682 left = 683 when (origin) { 684 Hotspot.CENTER -> (newLeft + newRight) / 2 685 Hotspot.BOTTOM_LEFT, 686 Hotspot.LEFT, 687 Hotspot.TOP_LEFT -> min(startLeft, newLeft) 688 Hotspot.TOP, 689 Hotspot.BOTTOM -> newLeft 690 Hotspot.TOP_RIGHT, 691 Hotspot.RIGHT, 692 Hotspot.BOTTOM_RIGHT -> max(startRight, newRight) 693 } 694 top = 695 when (origin) { 696 Hotspot.CENTER -> (newTop + newBottom) / 2 697 Hotspot.TOP_LEFT, 698 Hotspot.TOP, 699 Hotspot.TOP_RIGHT -> min(startTop, newTop) 700 Hotspot.LEFT, 701 Hotspot.RIGHT -> newTop 702 Hotspot.BOTTOM_RIGHT, 703 Hotspot.BOTTOM, 704 Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom) 705 } 706 right = 707 when (origin) { 708 Hotspot.CENTER -> (newLeft + newRight) / 2 709 Hotspot.TOP_RIGHT, 710 Hotspot.RIGHT, 711 Hotspot.BOTTOM_RIGHT -> max(startRight, newRight) 712 Hotspot.TOP, 713 Hotspot.BOTTOM -> newRight 714 Hotspot.BOTTOM_LEFT, 715 Hotspot.LEFT, 716 Hotspot.TOP_LEFT -> min(startLeft, newLeft) 717 } 718 bottom = 719 when (origin) { 720 Hotspot.CENTER -> (newTop + newBottom) / 2 721 Hotspot.BOTTOM_RIGHT, 722 Hotspot.BOTTOM, 723 Hotspot.BOTTOM_LEFT -> max(startBottom, newBottom) 724 Hotspot.LEFT, 725 Hotspot.RIGHT -> newBottom 726 Hotspot.TOP_LEFT, 727 Hotspot.TOP, 728 Hotspot.TOP_RIGHT -> min(startTop, newTop) 729 } 730 } 731 732 return mapOf( 733 Bound.LEFT to left, 734 Bound.TOP to top, 735 Bound.RIGHT to right, 736 Bound.BOTTOM to bottom, 737 ) 738 } 739 740 /** 741 * Computes a removal animation's end values based on the requested [destination] and the 742 * view's starting bounds. 743 * 744 * Examples: 745 * ``` 746 * 1) destination=TOP 747 * x---------x x---------x x---------x x---------x x---------x 748 * | | | | | | x---------x 749 * | | -> | | -> x---------x -> -> 750 * | | x---------x 751 * x---------x 752 * 2) destination=BOTTOM_LEFT 753 * x---------x 754 * | | x-------x 755 * | | -> | | -> x----x -> -> 756 * | | | | | | x--x 757 * x---------x x-------x x----x x--x x 758 * 3) destination=CENTER 759 * x---------x 760 * | | x-------x x-----x 761 * | | -> | | -> | | -> x---x -> x 762 * | | x-------x x-----x 763 * x---------x 764 * 4) destination=TOP, includeMargins=true (and view has large top margin) 765 * x---------x 766 * x---------x 767 * x---------x x---------x 768 * x---------x | | 769 * x---------x | | x---------x 770 * | | | | 771 * | | -> x---------x -> -> -> 772 * | | 773 * x---------x 774 * ``` 775 */ 776 private fun processEndValuesForRemoval( 777 destination: Hotspot, 778 rootView: View, 779 left: Int, 780 top: Int, 781 right: Int, 782 bottom: Int, 783 includeMargins: Boolean = false, 784 ): Map<Bound, Int> { 785 val marginAdjustment = 786 if (includeMargins && (rootView.layoutParams is ViewGroup.MarginLayoutParams)) { 787 val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams 788 DimenHolder( 789 left = marginLp.leftMargin, 790 top = marginLp.topMargin, 791 right = marginLp.rightMargin, 792 bottom = marginLp.bottomMargin, 793 ) 794 } else { 795 DimenHolder(0, 0, 0, 0) 796 } 797 798 // These are the end values to use *if* this bound is part of the destination. 799 val endLeft = left - marginAdjustment.left 800 val endTop = top - marginAdjustment.top 801 val endRight = right + marginAdjustment.right 802 val endBottom = bottom + marginAdjustment.bottom 803 804 // For the below calculations: We need to ensure that the destination bound and the 805 // bound *opposite* to the destination bound end at the same value, to ensure that the 806 // view has size 0 for that dimension. 807 // For example, 808 // - If destination=TOP, then endTop == endBottom. Left and right stay the same. 809 // - If destination=RIGHT, then endRight == endLeft. Top and bottom stay the same. 810 // - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight. 811 812 return when (destination) { 813 Hotspot.TOP -> 814 mapOf( 815 Bound.TOP to endTop, 816 Bound.BOTTOM to endTop, 817 Bound.LEFT to left, 818 Bound.RIGHT to right, 819 ) 820 Hotspot.TOP_RIGHT -> 821 mapOf( 822 Bound.TOP to endTop, 823 Bound.BOTTOM to endTop, 824 Bound.RIGHT to endRight, 825 Bound.LEFT to endRight, 826 ) 827 Hotspot.RIGHT -> 828 mapOf( 829 Bound.RIGHT to endRight, 830 Bound.LEFT to endRight, 831 Bound.TOP to top, 832 Bound.BOTTOM to bottom, 833 ) 834 Hotspot.BOTTOM_RIGHT -> 835 mapOf( 836 Bound.BOTTOM to endBottom, 837 Bound.TOP to endBottom, 838 Bound.RIGHT to endRight, 839 Bound.LEFT to endRight, 840 ) 841 Hotspot.BOTTOM -> 842 mapOf( 843 Bound.BOTTOM to endBottom, 844 Bound.TOP to endBottom, 845 Bound.LEFT to left, 846 Bound.RIGHT to right, 847 ) 848 Hotspot.BOTTOM_LEFT -> 849 mapOf( 850 Bound.BOTTOM to endBottom, 851 Bound.TOP to endBottom, 852 Bound.LEFT to endLeft, 853 Bound.RIGHT to endLeft, 854 ) 855 Hotspot.LEFT -> 856 mapOf( 857 Bound.LEFT to endLeft, 858 Bound.RIGHT to endLeft, 859 Bound.TOP to top, 860 Bound.BOTTOM to bottom, 861 ) 862 Hotspot.TOP_LEFT -> 863 mapOf( 864 Bound.TOP to endTop, 865 Bound.BOTTOM to endTop, 866 Bound.LEFT to endLeft, 867 Bound.RIGHT to endLeft, 868 ) 869 Hotspot.CENTER -> 870 mapOf( 871 Bound.LEFT to (endLeft + endRight) / 2, 872 Bound.RIGHT to (endLeft + endRight) / 2, 873 Bound.TOP to (endTop + endBottom) / 2, 874 Bound.BOTTOM to (endTop + endBottom) / 2, 875 ) 876 } 877 } 878 879 /** 880 * Computes the end values for the child of a view being removed, based on the child's 881 * starting bounds, the removal's [destination], and the [parentWidth] and [parentHeight]. 882 * 883 * The end values always represent the child's position after it has been translated so that 884 * its center is at the [destination]. 885 * 886 * Examples: 887 * ``` 888 * 1) destination=TOP 889 * The child maintains its left and right positions, but is shifted up so that its 890 * center is on the parent's end top edge. 891 * 2) destination=BOTTOM_LEFT 892 * The child shifts so that its center is on the parent's end bottom left corner. 893 * 3) destination=CENTER 894 * The child shifts so that its own center is on the parent's end center. 895 * ``` 896 */ 897 private fun processChildEndValuesForRemoval( 898 destination: Hotspot, 899 left: Int, 900 top: Int, 901 right: Int, 902 bottom: Int, 903 parentWidth: Int, 904 parentHeight: Int, 905 ): Map<Bound, Int> { 906 val halfWidth = (right - left) / 2 907 val halfHeight = (bottom - top) / 2 908 909 val endLeft = 910 when (destination) { 911 Hotspot.CENTER -> (parentWidth / 2) - halfWidth 912 Hotspot.BOTTOM_LEFT, 913 Hotspot.LEFT, 914 Hotspot.TOP_LEFT -> -halfWidth 915 Hotspot.TOP_RIGHT, 916 Hotspot.RIGHT, 917 Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth 918 Hotspot.TOP, 919 Hotspot.BOTTOM -> left 920 } 921 val endTop = 922 when (destination) { 923 Hotspot.CENTER -> (parentHeight / 2) - halfHeight 924 Hotspot.TOP_LEFT, 925 Hotspot.TOP, 926 Hotspot.TOP_RIGHT -> -halfHeight 927 Hotspot.BOTTOM_RIGHT, 928 Hotspot.BOTTOM, 929 Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight 930 Hotspot.LEFT, 931 Hotspot.RIGHT -> top 932 } 933 val endRight = 934 when (destination) { 935 Hotspot.CENTER -> (parentWidth / 2) + halfWidth 936 Hotspot.TOP_RIGHT, 937 Hotspot.RIGHT, 938 Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth 939 Hotspot.BOTTOM_LEFT, 940 Hotspot.LEFT, 941 Hotspot.TOP_LEFT -> halfWidth 942 Hotspot.TOP, 943 Hotspot.BOTTOM -> right 944 } 945 val endBottom = 946 when (destination) { 947 Hotspot.CENTER -> (parentHeight / 2) + halfHeight 948 Hotspot.BOTTOM_RIGHT, 949 Hotspot.BOTTOM, 950 Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight 951 Hotspot.TOP_LEFT, 952 Hotspot.TOP, 953 Hotspot.TOP_RIGHT -> halfHeight 954 Hotspot.LEFT, 955 Hotspot.RIGHT -> bottom 956 } 957 958 return mapOf( 959 Bound.LEFT to endLeft, 960 Bound.TOP to endTop, 961 Bound.RIGHT to endRight, 962 Bound.BOTTOM to endBottom, 963 ) 964 } 965 966 private fun addListener( 967 view: View, 968 listener: View.OnLayoutChangeListener, 969 recursive: Boolean = false, 970 animateChildren: Boolean = true, 971 excludedViews: Set<View> = emptySet(), 972 ) { 973 if (excludedViews.contains(view)) return 974 975 // Make sure that only one listener is active at a time. 976 val previousListener = view.getTag(R.id.tag_layout_listener) 977 if (previousListener != null && previousListener is View.OnLayoutChangeListener) { 978 view.removeOnLayoutChangeListener(previousListener) 979 } 980 981 view.addOnLayoutChangeListener(listener) 982 view.setTag(R.id.tag_layout_listener, listener) 983 if (animateChildren && view is ViewGroup && recursive) { 984 for (i in 0 until view.childCount) { 985 addListener( 986 view.getChildAt(i), 987 listener, 988 recursive = true, 989 animateChildren = animateChildren, 990 excludedViews = excludedViews, 991 ) 992 } 993 } 994 } 995 996 private fun recursivelyRemoveListener(view: View) { 997 val listener = view.getTag(R.id.tag_layout_listener) 998 if (listener != null && listener is View.OnLayoutChangeListener) { 999 view.setTag(R.id.tag_layout_listener, null /* tag */) 1000 view.removeOnLayoutChangeListener(listener) 1001 } 1002 1003 if (view is ViewGroup) { 1004 for (i in 0 until view.childCount) { 1005 recursivelyRemoveListener(view.getChildAt(i)) 1006 } 1007 } 1008 } 1009 1010 private fun getBound(view: View, bound: Bound): Int? { 1011 return view.getTag(bound.overrideTag) as? Int 1012 } 1013 1014 private fun setBound(view: View, bound: Bound, value: Int) { 1015 view.setTag(bound.overrideTag, value) 1016 bound.setValue(view, value) 1017 } 1018 1019 /** 1020 * Initiates the animation of the requested [bounds] between [startValues] and [endValues] 1021 * by creating the animator, registering it with the [view], and starting it using 1022 * [interpolator] and [duration]. 1023 * 1024 * If [ephemeral] is true, the layout change listener is unregistered at the end of the 1025 * animation, so no more animations happen. 1026 */ 1027 private fun startAnimation( 1028 view: View, 1029 bounds: Set<Bound>, 1030 startValues: Map<Bound, Int>, 1031 endValues: Map<Bound, Int>, 1032 interpolator: Interpolator, 1033 duration: Long, 1034 ephemeral: Boolean, 1035 onAnimationEnd: Runnable? = null, 1036 ) { 1037 val propertyValuesHolders = 1038 buildList { 1039 bounds.forEach { bound -> 1040 add( 1041 PropertyValuesHolder.ofInt( 1042 PROPERTIES[bound], 1043 startValues.getValue(bound), 1044 endValues.getValue(bound), 1045 ) 1046 ) 1047 } 1048 } 1049 .toTypedArray() 1050 1051 (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() 1052 1053 val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders) 1054 animator.interpolator = interpolator 1055 animator.duration = duration 1056 animator.addListener( 1057 object : AnimatorListenerAdapter() { 1058 var cancelled = false 1059 1060 override fun onAnimationEnd(animation: Animator) { 1061 view.setTag(R.id.tag_animator, null /* tag */) 1062 bounds.forEach { view.setTag(it.overrideTag, null /* tag */) } 1063 1064 // When an animation is cancelled, a new one might be taking over. We 1065 // shouldn't unregister the listener yet. 1066 if (ephemeral && !cancelled) { 1067 // The duration is the same for the whole hierarchy, so it's safe to 1068 // remove the listener recursively. We do this because some descendant 1069 // views might not change bounds, and therefore not animate and leak the 1070 // listener. 1071 recursivelyRemoveListener(view) 1072 } 1073 // Run the end runnable regardless of whether the animation was cancelled or 1074 // not - this ensures critical actions (like removing a window) always occur 1075 // (see b/344049884). 1076 onAnimationEnd?.run() 1077 } 1078 1079 override fun onAnimationCancel(animation: Animator) { 1080 cancelled = true 1081 } 1082 } 1083 ) 1084 1085 bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) } 1086 1087 view.setTag(R.id.tag_animator, animator) 1088 animator.start() 1089 } 1090 1091 private fun createAndStartFadeInAnimator( 1092 view: View, 1093 duration: Long, 1094 startDelay: Long, 1095 interpolator: Interpolator, 1096 ) { 1097 val animator = ObjectAnimator.ofFloat(view, "alpha", 1f) 1098 animator.startDelay = startDelay 1099 animator.duration = duration 1100 animator.interpolator = interpolator 1101 animator.addListener( 1102 object : AnimatorListenerAdapter() { 1103 override fun onAnimationEnd(animation: Animator) { 1104 view.setTag(R.id.tag_alpha_animator, null /* tag */) 1105 } 1106 } 1107 ) 1108 1109 (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel() 1110 view.setTag(R.id.tag_alpha_animator, animator) 1111 animator.start() 1112 } 1113 } 1114 1115 /** An enum used to determine the origin of addition animations. */ 1116 enum class Hotspot { 1117 CENTER, 1118 LEFT, 1119 TOP_LEFT, 1120 TOP, 1121 TOP_RIGHT, 1122 RIGHT, 1123 BOTTOM_RIGHT, 1124 BOTTOM, 1125 BOTTOM_LEFT, 1126 } 1127 1128 private enum class Bound(val label: String, val overrideTag: Int) { 1129 LEFT("left", R.id.tag_override_left) { 1130 override fun setValue(view: View, value: Int) { 1131 view.left = value 1132 } 1133 1134 override fun getValue(view: View): Int { 1135 return view.left 1136 } 1137 }, 1138 TOP("top", R.id.tag_override_top) { 1139 override fun setValue(view: View, value: Int) { 1140 view.top = value 1141 } 1142 1143 override fun getValue(view: View): Int { 1144 return view.top 1145 } 1146 }, 1147 RIGHT("right", R.id.tag_override_right) { 1148 override fun setValue(view: View, value: Int) { 1149 view.right = value 1150 } 1151 1152 override fun getValue(view: View): Int { 1153 return view.right 1154 } 1155 }, 1156 BOTTOM("bottom", R.id.tag_override_bottom) { 1157 override fun setValue(view: View, value: Int) { 1158 view.bottom = value 1159 } 1160 1161 override fun getValue(view: View): Int { 1162 return view.bottom 1163 } 1164 }; 1165 1166 abstract fun setValue(view: View, value: Int) 1167 1168 abstract fun getValue(view: View): Int 1169 } 1170 1171 /** Simple data class to hold a set of dimens for left, top, right, bottom. */ 1172 private data class DimenHolder(val left: Int, val top: Int, val right: Int, val bottom: Int) 1173 } 1174