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