Loading packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +320 −6 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.animation.PropertyValuesHolder import android.animation.ValueAnimator import android.util.IntProperty import android.view.View import android.view.ViewGroup Loading @@ -37,6 +38,7 @@ class ViewHierarchyAnimator { private const val DEFAULT_DURATION = 500L private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE /** The properties used to animate the view bounds. */ private val PROPERTIES = mapOf( Loading Loading @@ -113,7 +115,7 @@ class ViewHierarchyAnimator { } val listener = createUpdateListener(interpolator, duration, ephemeral) recursivelyAddListener(rootView, listener) addListener(rootView, listener, recursive = true) return true } Loading Loading @@ -183,7 +185,7 @@ class ViewHierarchyAnimator { val listener = createAdditionListener( origin, interpolator, duration, ignorePreviousValues = !includeMargins ) recursivelyAddListener(rootView, listener) addListener(rootView, listener, recursive = true) return true } Loading Loading @@ -297,6 +299,183 @@ class ViewHierarchyAnimator { } } /** * Animates the removal of [rootView] and its children from the hierarchy. It uses the given * [interpolator] and [duration]. * * The end state of the animation is controlled by [destination]. This value can be any of * the four corners, any of the four edges, or the center of the view. */ @JvmOverloads fun animateRemoval( rootView: View, destination: Hotspot = Hotspot.CENTER, interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR, duration: Long = DEFAULT_DURATION ): Boolean { if (!isVisible( rootView.visibility, rootView.left, rootView.top, rootView.right, rootView.bottom ) ) { return false } val parent = rootView.parent as ViewGroup // Ensure that rootView's siblings animate nicely around the removal. val listener = createUpdateListener( interpolator, duration, ephemeral = true ) for (i in 0 until parent.childCount) { val child = parent.getChildAt(i) if (child == rootView) continue addListener(child, listener, recursive = false) } // Remove the view so that a layout update is triggered for the siblings and they // animate to their next position while the view's removal is also animating. parent.removeView(rootView) // By adding the view to the overlay, we can animate it while it isn't part of the view // hierarchy. It is correctly positioned because we have its previous bounds, and we set // them manually during the animation. parent.overlay.add(rootView) val startValues = mapOf( Bound.LEFT to rootView.left, Bound.TOP to rootView.top, Bound.RIGHT to rootView.right, Bound.BOTTOM to rootView.bottom ) val endValues = processEndValuesForRemoval( destination, rootView.left, rootView.top, rootView.right, rootView.bottom ) val boundsToAnimate = mutableSetOf<Bound>() if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) if (rootView.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) if (rootView.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) if (rootView.bottom != endValues.getValue(Bound.BOTTOM)) { boundsToAnimate.add(Bound.BOTTOM) } startAnimation( rootView, boundsToAnimate, startValues, endValues, interpolator, duration, ephemeral = true ) if (rootView is ViewGroup) { // Shift the children so they maintain a consistent position within the shrinking // view. shiftChildrenForRemoval(rootView, destination, endValues, interpolator, duration) // Fade out the children during the first half of the removal, so they don't clutter // too much once the view becomes very small. Then we fade out the view itself, in // case it has its own content and/or background. val startAlphas = FloatArray(rootView.childCount) for (i in 0 until rootView.childCount) { startAlphas[i] = rootView.getChildAt(i).alpha } val animator = ValueAnimator.ofFloat(1f, 0f) animator.interpolator = Interpolators.ALPHA_OUT animator.duration = duration / 2 animator.addUpdateListener { animation -> for (i in 0 until rootView.childCount) { rootView.getChildAt(i).alpha = (animation.animatedValue as Float) * startAlphas[i] } } animator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { rootView.animate() .alpha(0f) .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) .withEndAction { parent.overlay.remove(rootView) } .start() } }) animator.start() } else { // Fade out the view during the second half of the removal. rootView.animate() .alpha(0f) .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) .setStartDelay(duration / 2) .withEndAction { parent.overlay.remove(rootView) } .start() } return true } /** * Animates the children of [rootView] so that its layout remains internally consistent as * it shrinks towards [destination] and changes its bounds to [endValues]. * * Uses [interpolator] and [duration], which should match those of the removal animation. */ private fun shiftChildrenForRemoval( rootView: ViewGroup, destination: Hotspot, endValues: Map<Bound, Int>, interpolator: Interpolator, duration: Long ) { for (i in 0 until rootView.childCount) { val child = rootView.getChildAt(i) val childStartValues = mapOf( Bound.LEFT to child.left, Bound.TOP to child.top, Bound.RIGHT to child.right, Bound.BOTTOM to child.bottom ) val childEndValues = processChildEndValuesForRemoval( destination, child.left, child.top, child.right, child.bottom, endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT), endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP) ) val boundsToAnimate = mutableSetOf<Bound>() if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) if (child.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) if (child.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) if (child.bottom != endValues.getValue(Bound.BOTTOM)) { boundsToAnimate.add(Bound.BOTTOM) } startAnimation( child, boundsToAnimate, childStartValues, childEndValues, interpolator, duration, ephemeral = true ) } } /** * Returns whether the given [visibility] and bounds are consistent with a view being * currently visible on screen. Loading @@ -312,7 +491,7 @@ class ViewHierarchyAnimator { } /** * Compute the actual starting values based on the requested [origin] and on * Computes the actual starting values based on the requested [origin] and on * [ignorePreviousValues]. * * If [origin] is null, the resolved start values will be the same as those passed in, or Loading Loading @@ -422,7 +601,140 @@ class ViewHierarchyAnimator { ) } private fun recursivelyAddListener(view: View, listener: View.OnLayoutChangeListener) { /** * Computes a removal animation's end values based on the requested [destination] and the * view's starting bounds. * * Examples: * 1) destination=TOP * x---------x x---------x x---------x x---------x x---------x * | | | | | | x---------x * | | -> | | -> x---------x -> -> * | | x---------x * x---------x * 2) destination=BOTTOM_LEFT * x---------x * | | x-------x * | | -> | | -> x----x -> -> * | | | | | | x--x * x---------x x-------x x----x x--x x * 3) destination=CENTER * x---------x * | | x-------x x-----x * | | -> | | -> | | -> x---x -> x * | | x-------x x-----x * x---------x */ private fun processEndValuesForRemoval( destination: Hotspot, left: Int, top: Int, right: Int, bottom: Int ): Map<Bound, Int> { val endLeft = when (destination) { Hotspot.CENTER -> (left + right) / 2 Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP -> left Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> right } val endTop = when (destination) { Hotspot.CENTER -> (top + bottom) / 2 Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT -> top Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> bottom } val endRight = when (destination) { Hotspot.CENTER -> (left + right) / 2 Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM -> right Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> left } val endBottom = when (destination) { Hotspot.CENTER -> (top + bottom) / 2 Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT -> bottom Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> top } return mapOf( Bound.LEFT to endLeft, Bound.TOP to endTop, Bound.RIGHT to endRight, Bound.BOTTOM to endBottom ) } /** * Computes the end values for the child of a view being removed, based on the child's * starting bounds, the removal's [destination], and the [parentWidth] and [parentHeight]. * * The end values always represent the child's position after it has been translated so that * its center is at the [destination]. * * Examples: * 1) destination=TOP * The child maintains its left and right positions, but is shifted up so that its * center is on the parent's end top edge. * 2) destination=BOTTOM_LEFT * The child shifts so that its center is on the parent's end bottom left corner. * 3) destination=CENTER * The child shifts so that its own center is on the parent's end center. */ private fun processChildEndValuesForRemoval( destination: Hotspot, left: Int, top: Int, right: Int, bottom: Int, parentWidth: Int, parentHeight: Int ): Map<Bound, Int> { val halfWidth = (right - left) / 2 val halfHeight = (bottom - top) / 2 val endLeft = when (destination) { Hotspot.CENTER -> (parentWidth / 2) - halfWidth Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> -halfWidth Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth Hotspot.TOP, Hotspot.BOTTOM -> left } val endTop = when (destination) { Hotspot.CENTER -> (parentHeight / 2) - halfHeight Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> -halfHeight Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight Hotspot.LEFT, Hotspot.RIGHT -> top } val endRight = when (destination) { Hotspot.CENTER -> (parentWidth / 2) + halfWidth Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> halfWidth Hotspot.TOP, Hotspot.BOTTOM -> right } val endBottom = when (destination) { Hotspot.CENTER -> (parentHeight / 2) + halfHeight Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> halfHeight Hotspot.LEFT, Hotspot.RIGHT -> bottom } return mapOf( Bound.LEFT to endLeft, Bound.TOP to endTop, Bound.RIGHT to endRight, Bound.BOTTOM to endBottom ) } private fun addListener( view: View, listener: View.OnLayoutChangeListener, recursive: Boolean = false ) { // Make sure that only one listener is active at a time. val previousListener = view.getTag(R.id.tag_layout_listener) if (previousListener != null && previousListener is View.OnLayoutChangeListener) { Loading @@ -431,9 +743,9 @@ class ViewHierarchyAnimator { view.addOnLayoutChangeListener(listener) view.setTag(R.id.tag_layout_listener, listener) if (view is ViewGroup) { if (view is ViewGroup && recursive) { for (i in 0 until view.childCount) { recursivelyAddListener(view.getChildAt(i), listener) addListener(view.getChildAt(i), listener, recursive = true) } } } Loading Loading @@ -490,6 +802,8 @@ class ViewHierarchyAnimator { } }.toTypedArray() (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders) animator.interpolator = interpolator animator.duration = duration Loading packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +314 −28 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +320 −6 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.animation.PropertyValuesHolder import android.animation.ValueAnimator import android.util.IntProperty import android.view.View import android.view.ViewGroup Loading @@ -37,6 +38,7 @@ class ViewHierarchyAnimator { private const val DEFAULT_DURATION = 500L private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE /** The properties used to animate the view bounds. */ private val PROPERTIES = mapOf( Loading Loading @@ -113,7 +115,7 @@ class ViewHierarchyAnimator { } val listener = createUpdateListener(interpolator, duration, ephemeral) recursivelyAddListener(rootView, listener) addListener(rootView, listener, recursive = true) return true } Loading Loading @@ -183,7 +185,7 @@ class ViewHierarchyAnimator { val listener = createAdditionListener( origin, interpolator, duration, ignorePreviousValues = !includeMargins ) recursivelyAddListener(rootView, listener) addListener(rootView, listener, recursive = true) return true } Loading Loading @@ -297,6 +299,183 @@ class ViewHierarchyAnimator { } } /** * Animates the removal of [rootView] and its children from the hierarchy. It uses the given * [interpolator] and [duration]. * * The end state of the animation is controlled by [destination]. This value can be any of * the four corners, any of the four edges, or the center of the view. */ @JvmOverloads fun animateRemoval( rootView: View, destination: Hotspot = Hotspot.CENTER, interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR, duration: Long = DEFAULT_DURATION ): Boolean { if (!isVisible( rootView.visibility, rootView.left, rootView.top, rootView.right, rootView.bottom ) ) { return false } val parent = rootView.parent as ViewGroup // Ensure that rootView's siblings animate nicely around the removal. val listener = createUpdateListener( interpolator, duration, ephemeral = true ) for (i in 0 until parent.childCount) { val child = parent.getChildAt(i) if (child == rootView) continue addListener(child, listener, recursive = false) } // Remove the view so that a layout update is triggered for the siblings and they // animate to their next position while the view's removal is also animating. parent.removeView(rootView) // By adding the view to the overlay, we can animate it while it isn't part of the view // hierarchy. It is correctly positioned because we have its previous bounds, and we set // them manually during the animation. parent.overlay.add(rootView) val startValues = mapOf( Bound.LEFT to rootView.left, Bound.TOP to rootView.top, Bound.RIGHT to rootView.right, Bound.BOTTOM to rootView.bottom ) val endValues = processEndValuesForRemoval( destination, rootView.left, rootView.top, rootView.right, rootView.bottom ) val boundsToAnimate = mutableSetOf<Bound>() if (rootView.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) if (rootView.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) if (rootView.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) if (rootView.bottom != endValues.getValue(Bound.BOTTOM)) { boundsToAnimate.add(Bound.BOTTOM) } startAnimation( rootView, boundsToAnimate, startValues, endValues, interpolator, duration, ephemeral = true ) if (rootView is ViewGroup) { // Shift the children so they maintain a consistent position within the shrinking // view. shiftChildrenForRemoval(rootView, destination, endValues, interpolator, duration) // Fade out the children during the first half of the removal, so they don't clutter // too much once the view becomes very small. Then we fade out the view itself, in // case it has its own content and/or background. val startAlphas = FloatArray(rootView.childCount) for (i in 0 until rootView.childCount) { startAlphas[i] = rootView.getChildAt(i).alpha } val animator = ValueAnimator.ofFloat(1f, 0f) animator.interpolator = Interpolators.ALPHA_OUT animator.duration = duration / 2 animator.addUpdateListener { animation -> for (i in 0 until rootView.childCount) { rootView.getChildAt(i).alpha = (animation.animatedValue as Float) * startAlphas[i] } } animator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { rootView.animate() .alpha(0f) .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) .withEndAction { parent.overlay.remove(rootView) } .start() } }) animator.start() } else { // Fade out the view during the second half of the removal. rootView.animate() .alpha(0f) .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) .setStartDelay(duration / 2) .withEndAction { parent.overlay.remove(rootView) } .start() } return true } /** * Animates the children of [rootView] so that its layout remains internally consistent as * it shrinks towards [destination] and changes its bounds to [endValues]. * * Uses [interpolator] and [duration], which should match those of the removal animation. */ private fun shiftChildrenForRemoval( rootView: ViewGroup, destination: Hotspot, endValues: Map<Bound, Int>, interpolator: Interpolator, duration: Long ) { for (i in 0 until rootView.childCount) { val child = rootView.getChildAt(i) val childStartValues = mapOf( Bound.LEFT to child.left, Bound.TOP to child.top, Bound.RIGHT to child.right, Bound.BOTTOM to child.bottom ) val childEndValues = processChildEndValuesForRemoval( destination, child.left, child.top, child.right, child.bottom, endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT), endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP) ) val boundsToAnimate = mutableSetOf<Bound>() if (child.left != endValues.getValue(Bound.LEFT)) boundsToAnimate.add(Bound.LEFT) if (child.top != endValues.getValue(Bound.TOP)) boundsToAnimate.add(Bound.TOP) if (child.right != endValues.getValue(Bound.RIGHT)) boundsToAnimate.add(Bound.RIGHT) if (child.bottom != endValues.getValue(Bound.BOTTOM)) { boundsToAnimate.add(Bound.BOTTOM) } startAnimation( child, boundsToAnimate, childStartValues, childEndValues, interpolator, duration, ephemeral = true ) } } /** * Returns whether the given [visibility] and bounds are consistent with a view being * currently visible on screen. Loading @@ -312,7 +491,7 @@ class ViewHierarchyAnimator { } /** * Compute the actual starting values based on the requested [origin] and on * Computes the actual starting values based on the requested [origin] and on * [ignorePreviousValues]. * * If [origin] is null, the resolved start values will be the same as those passed in, or Loading Loading @@ -422,7 +601,140 @@ class ViewHierarchyAnimator { ) } private fun recursivelyAddListener(view: View, listener: View.OnLayoutChangeListener) { /** * Computes a removal animation's end values based on the requested [destination] and the * view's starting bounds. * * Examples: * 1) destination=TOP * x---------x x---------x x---------x x---------x x---------x * | | | | | | x---------x * | | -> | | -> x---------x -> -> * | | x---------x * x---------x * 2) destination=BOTTOM_LEFT * x---------x * | | x-------x * | | -> | | -> x----x -> -> * | | | | | | x--x * x---------x x-------x x----x x--x x * 3) destination=CENTER * x---------x * | | x-------x x-----x * | | -> | | -> | | -> x---x -> x * | | x-------x x-----x * x---------x */ private fun processEndValuesForRemoval( destination: Hotspot, left: Int, top: Int, right: Int, bottom: Int ): Map<Bound, Int> { val endLeft = when (destination) { Hotspot.CENTER -> (left + right) / 2 Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP -> left Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> right } val endTop = when (destination) { Hotspot.CENTER -> (top + bottom) / 2 Hotspot.LEFT, Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT -> top Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> bottom } val endRight = when (destination) { Hotspot.CENTER -> (left + right) / 2 Hotspot.TOP, Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM -> right Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> left } val endBottom = when (destination) { Hotspot.CENTER -> (top + bottom) / 2 Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT, Hotspot.LEFT -> bottom Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> top } return mapOf( Bound.LEFT to endLeft, Bound.TOP to endTop, Bound.RIGHT to endRight, Bound.BOTTOM to endBottom ) } /** * Computes the end values for the child of a view being removed, based on the child's * starting bounds, the removal's [destination], and the [parentWidth] and [parentHeight]. * * The end values always represent the child's position after it has been translated so that * its center is at the [destination]. * * Examples: * 1) destination=TOP * The child maintains its left and right positions, but is shifted up so that its * center is on the parent's end top edge. * 2) destination=BOTTOM_LEFT * The child shifts so that its center is on the parent's end bottom left corner. * 3) destination=CENTER * The child shifts so that its own center is on the parent's end center. */ private fun processChildEndValuesForRemoval( destination: Hotspot, left: Int, top: Int, right: Int, bottom: Int, parentWidth: Int, parentHeight: Int ): Map<Bound, Int> { val halfWidth = (right - left) / 2 val halfHeight = (bottom - top) / 2 val endLeft = when (destination) { Hotspot.CENTER -> (parentWidth / 2) - halfWidth Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> -halfWidth Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth - halfWidth Hotspot.TOP, Hotspot.BOTTOM -> left } val endTop = when (destination) { Hotspot.CENTER -> (parentHeight / 2) - halfHeight Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> -halfHeight Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> parentHeight - halfHeight Hotspot.LEFT, Hotspot.RIGHT -> top } val endRight = when (destination) { Hotspot.CENTER -> (parentWidth / 2) + halfWidth Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> parentWidth + halfWidth Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> halfWidth Hotspot.TOP, Hotspot.BOTTOM -> right } val endBottom = when (destination) { Hotspot.CENTER -> (parentHeight / 2) + halfHeight Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> parentHeight + halfHeight Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> halfHeight Hotspot.LEFT, Hotspot.RIGHT -> bottom } return mapOf( Bound.LEFT to endLeft, Bound.TOP to endTop, Bound.RIGHT to endRight, Bound.BOTTOM to endBottom ) } private fun addListener( view: View, listener: View.OnLayoutChangeListener, recursive: Boolean = false ) { // Make sure that only one listener is active at a time. val previousListener = view.getTag(R.id.tag_layout_listener) if (previousListener != null && previousListener is View.OnLayoutChangeListener) { Loading @@ -431,9 +743,9 @@ class ViewHierarchyAnimator { view.addOnLayoutChangeListener(listener) view.setTag(R.id.tag_layout_listener, listener) if (view is ViewGroup) { if (view is ViewGroup && recursive) { for (i in 0 until view.childCount) { recursivelyAddListener(view.getChildAt(i), listener) addListener(view.getChildAt(i), listener, recursive = true) } } } Loading Loading @@ -490,6 +802,8 @@ class ViewHierarchyAnimator { } }.toTypedArray() (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders) animator.interpolator = interpolator animator.duration = duration Loading
packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +314 −28 File changed.Preview size limit exceeded, changes collapsed. Show changes