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