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