Loading packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +331 −76 Original line number Diff line number Diff line Loading @@ -24,59 +24,19 @@ import android.util.IntProperty import android.view.View import android.view.ViewGroup import android.view.animation.Interpolator import kotlin.math.max import kotlin.math.min /** * A class that allows changes in bounds within a view hierarchy to animate seamlessly between the * start and end state. */ class ViewHierarchyAnimator { // TODO(b/221418522): make this private once it can't be passed as an arg anymore. enum class Bound(val label: String, val overrideTag: Int) { LEFT("left", R.id.tag_override_left) { override fun setValue(view: View, value: Int) { view.left = value } override fun getValue(view: View): Int { return view.left } }, TOP("top", R.id.tag_override_top) { override fun setValue(view: View, value: Int) { view.top = value } override fun getValue(view: View): Int { return view.top } }, RIGHT("right", R.id.tag_override_right) { override fun setValue(view: View, value: Int) { view.right = value } override fun getValue(view: View): Int { return view.right } }, BOTTOM("bottom", R.id.tag_override_bottom) { override fun setValue(view: View, value: Int) { view.bottom = value } override fun getValue(view: View): Int { return view.bottom } }; abstract fun setValue(view: View, value: Int) abstract fun getValue(view: View): Int } companion object { /** Default values for the animation. These can all be overridden at call time. */ private const val DEFAULT_DURATION = 500L private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE private val DEFAULT_BOUNDS = setOf(Bound.LEFT, Bound.TOP, Bound.RIGHT, Bound.BOTTOM) /** The properties used to animate the view bounds. */ Loading Loading @@ -112,6 +72,9 @@ class ViewHierarchyAnimator { * Successive calls to this method override the previous settings ([interpolator] and * [duration]). The changes take effect on the next animation. * * Returns true if the [rootView] is already visible and will be animated, false otherwise. * To animate the addition of a view, see [animateAddition]. * * TODO(b/221418522): remove the ability to select which bounds to animate and always * animate all of them. */ Loading @@ -121,8 +84,8 @@ class ViewHierarchyAnimator { bounds: Set<Bound> = DEFAULT_BOUNDS, interpolator: Interpolator = DEFAULT_INTERPOLATOR, duration: Long = DEFAULT_DURATION ) { animate(rootView, bounds, interpolator, duration, false /* ephemeral */) ): Boolean { return animate(rootView, bounds, interpolator, duration, ephemeral = false) } /** Loading @@ -138,8 +101,8 @@ class ViewHierarchyAnimator { bounds: Set<Bound> = DEFAULT_BOUNDS, interpolator: Interpolator = DEFAULT_INTERPOLATOR, duration: Long = DEFAULT_DURATION ) { animate(rootView, bounds, interpolator, duration, true /* ephemeral */) ): Boolean { return animate(rootView, bounds, interpolator, duration, ephemeral = true) } private fun animate( Loading @@ -148,9 +111,42 @@ class ViewHierarchyAnimator { interpolator: Interpolator, duration: Long, ephemeral: Boolean ): Boolean { if (!isVisible( rootView.visibility, rootView.left, rootView.top, rootView.right, rootView.bottom ) ) { val listener = createListener(bounds, interpolator, duration, ephemeral) return false } val listener = createUpdateListener(bounds, interpolator, duration, ephemeral) recursivelyAddListener(rootView, listener) return true } /** * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation * for the specified [bounds], using [interpolator] and [duration]. * * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise * it keeps listening for further updates. */ private fun createUpdateListener( bounds: Set<Bound>, interpolator: Interpolator, duration: Long, ephemeral: Boolean ): View.OnLayoutChangeListener { return createListener( bounds, interpolator, duration, ephemeral ) } /** Loading @@ -173,11 +169,87 @@ class ViewHierarchyAnimator { } } /** * Instruct the animator to watch for changes to the layout of [rootView] and its children, * and animate the next time the hierarchy appears after not being visible. It uses the * given [interpolator] and [duration]. * * The start state of the animation is controlled by [origin]. This value can be any of the * four corners, any of the four edges, or the center of the view. If any margins are added * on the side(s) of the origin, the translation of those margins can be included by * specifying [includeMargins]. * * Returns true if the [rootView] is invisible and will be animated, false otherwise. To * animate an already visible view, see [animate] and [animateNextUpdate]. * * Then animator unregisters itself once the first addition animation is complete. */ @JvmOverloads fun animateAddition( rootView: View, origin: Hotspot = Hotspot.CENTER, interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR, duration: Long = DEFAULT_DURATION, includeMargins: Boolean = false ): Boolean { if (isVisible( rootView.visibility, rootView.left, rootView.top, rootView.right, rootView.bottom ) ) { return false } val listener = createAdditionListener( origin, interpolator, duration, ignorePreviousValues = !includeMargins ) recursivelyAddListener(rootView, listener) return true } /** * Returns a new [View.OnLayoutChangeListener] that on the next call triggers a layout * addition animation from the given [origin], using [interpolator] and [duration]. * * If [ignorePreviousValues] is true, the animation will only span the area covered by the * new bounds. Otherwise it will include the margins between the previous and new bounds. */ private fun createAdditionListener( origin: Hotspot, interpolator: Interpolator, duration: Long, ignorePreviousValues: Boolean ): View.OnLayoutChangeListener { return createListener( DEFAULT_BOUNDS, interpolator, duration, ephemeral = true, origin = origin, ignorePreviousValues = ignorePreviousValues ) } /** * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation * for the specified [bounds], using [interpolator] and [duration]. * * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise * it keeps listening for further updates. * * [origin] specifies whether the start values should be determined by a hotspot, and * [ignorePreviousValues] controls whether the previous values should be taken into account. */ private fun createListener( bounds: Set<Bound>, interpolator: Interpolator, duration: Long, ephemeral: Boolean ephemeral: Boolean, origin: Hotspot? = null, ignorePreviousValues: Boolean = false ): View.OnLayoutChangeListener { return object : View.OnLayoutChangeListener { override fun onLayoutChange( Loading @@ -186,21 +258,21 @@ class ViewHierarchyAnimator { top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int previousLeft: Int, previousTop: Int, previousRight: Int, previousBottom: Int ) { if (view == null) return val startLeft = getBound(view, Bound.LEFT) ?: oldLeft val startTop = getBound(view, Bound.TOP) ?: oldTop val startRight = getBound(view, Bound.RIGHT) ?: oldRight val startBottom = getBound(view, Bound.BOTTOM) ?: oldBottom val startLeft = getBound(view, Bound.LEFT) ?: previousLeft val startTop = getBound(view, Bound.TOP) ?: previousTop val startRight = getBound(view, Bound.RIGHT) ?: previousRight val startBottom = getBound(view, Bound.BOTTOM) ?: previousBottom (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() if (view.visibility == View.GONE || view.visibility == View.INVISIBLE) { if (!isVisible(view.visibility, left, top, right, bottom)) { setBound(view, Bound.LEFT, left) setBound(view, Bound.TOP, top) setBound(view, Bound.RIGHT, right) Loading @@ -208,11 +280,17 @@ class ViewHierarchyAnimator { return } val startValues = mapOf( Bound.LEFT to startLeft, Bound.TOP to startTop, Bound.RIGHT to startRight, Bound.BOTTOM to startBottom val startValues = processStartValues( origin, left, top, right, bottom, startLeft, startTop, startRight, startBottom, ignorePreviousValues ) val endValues = mapOf( Bound.LEFT to left, Loading @@ -221,11 +299,12 @@ class ViewHierarchyAnimator { Bound.BOTTOM to bottom ) val boundsToAnimate = bounds.toMutableSet() if (left == startLeft) boundsToAnimate.remove(Bound.LEFT) if (top == startTop) boundsToAnimate.remove(Bound.TOP) if (right == startRight) boundsToAnimate.remove(Bound.RIGHT) if (bottom == startBottom) boundsToAnimate.remove(Bound.BOTTOM) val boundsToAnimate = mutableSetOf<Bound>() bounds.forEach { bound -> if (endValues.getValue(bound) != startValues.getValue(bound)) { boundsToAnimate.add(bound) } } if (boundsToAnimate.isNotEmpty()) { startAnimation( Loading @@ -242,11 +321,136 @@ class ViewHierarchyAnimator { } } /** * Returns whether the given [visibility] and bounds are consistent with a view being * currently visible on screen. */ private fun isVisible( visibility: Int, left: Int, top: Int, right: Int, bottom: Int ): Boolean { return visibility == View.VISIBLE && left != right && top != bottom } /** * Compute 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 * the same as the new values if [ignorePreviousValues] is true. If [origin] is not null, * the start values are resolved based on it, and [ignorePreviousValues] controls whether or * not newly introduced margins are included. * * Base case * 1) origin=TOP * x---------x x---------x x---------x x---------x x---------x * x---------x | | | | | | * -> -> x---------x -> | | -> | | * x---------x | | * x---------x * 2) origin=BOTTOM_LEFT * x---------x * x-------x | | * -> -> x----x -> | | -> | | * x--x | | | | | | * x x--x x----x x-------x x---------x * 3) origin=CENTER * x---------x * x-----x x-------x | | * x -> x---x -> | | -> | | -> | | * x-----x x-------x | | * x---------x * * In case the start and end values differ in the direction of the origin, and * [ignorePreviousValues] is false, the previous values are used and a translation is * included in addition to the view expansion. * * origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70) * x * x--x * x--x x----x * -> -> | | -> x------x * x----x | | * | | * x------x */ private fun processStartValues( origin: Hotspot?, newLeft: Int, newTop: Int, newRight: Int, newBottom: Int, previousLeft: Int, previousTop: Int, previousRight: Int, previousBottom: Int, ignorePreviousValues: Boolean ): Map<Bound, Int> { val startLeft = if (ignorePreviousValues) newLeft else previousLeft val startTop = if (ignorePreviousValues) newTop else previousTop val startRight = if (ignorePreviousValues) newRight else previousRight val startBottom = if (ignorePreviousValues) newBottom else previousBottom var left = startLeft var top = startTop var right = startRight var bottom = startBottom if (origin != null) { left = when (origin) { Hotspot.CENTER -> (newLeft + newRight) / 2 Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) Hotspot.TOP, Hotspot.BOTTOM -> newLeft Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( startRight, newRight ) } top = when (origin) { Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) Hotspot.LEFT, Hotspot.RIGHT -> newTop Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( startBottom, newBottom ) } right = when (origin) { Hotspot.CENTER -> (newLeft + newRight) / 2 Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( startRight, newRight ) Hotspot.TOP, Hotspot.BOTTOM -> newRight Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) } bottom = when (origin) { Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( startBottom, newBottom ) Hotspot.LEFT, Hotspot.RIGHT -> newBottom Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) } } return mapOf( Bound.LEFT to left, Bound.TOP to top, Bound.RIGHT to right, Bound.BOTTOM to bottom ) } private fun recursivelyAddListener(view: View, listener: View.OnLayoutChangeListener) { // Make sure that only one listener is active at a time. val oldListener = view.getTag(R.id.tag_layout_listener) if (oldListener != null && oldListener is View.OnLayoutChangeListener) { view.removeOnLayoutChangeListener(oldListener) val previousListener = view.getTag(R.id.tag_layout_listener) if (previousListener != null && previousListener is View.OnLayoutChangeListener) { view.removeOnLayoutChangeListener(previousListener) } view.addOnLayoutChangeListener(listener) Loading @@ -268,9 +472,12 @@ class ViewHierarchyAnimator { } /** * Initiates the animation of a single bound by creating the animator, registering it with * the [view], and starting it. If [ephemeral], the layout change listener is unregistered * at the end of the animation, so no more animations happen. * Initiates the animation of the requested [bounds] between [startValues] and [endValues] * by creating the animator, registering it with the [view], and starting it using * [interpolator] and [duration]. * * If [ephemeral] is true, the layout change listener is unregistered at the end of the * animation, so no more animations happen. */ private fun startAnimation( view: View, Loading Loading @@ -325,4 +532,52 @@ class ViewHierarchyAnimator { animator.start() } } /** An enum used to determine the origin of addition animations. */ enum class Hotspot { CENTER, LEFT, TOP_LEFT, TOP, TOP_RIGHT, RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT } // TODO(b/221418522): make this private once it can't be passed as an arg anymore. enum class Bound(val label: String, val overrideTag: Int) { LEFT("left", R.id.tag_override_left) { override fun setValue(view: View, value: Int) { view.left = value } override fun getValue(view: View): Int { return view.left } }, TOP("top", R.id.tag_override_top) { override fun setValue(view: View, value: Int) { view.top = value } override fun getValue(view: View): Int { return view.top } }, RIGHT("right", R.id.tag_override_right) { override fun setValue(view: View, value: Int) { view.right = value } override fun getValue(view: View): Int { return view.right } }, BOTTOM("bottom", R.id.tag_override_bottom) { override fun setValue(view: View, value: Int) { view.bottom = value } override fun getValue(view: View): Int { return view.bottom } }; abstract fun setValue(view: View, value: Int) abstract fun getValue(view: View): Int } } packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +461 −21 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +331 −76 Original line number Diff line number Diff line Loading @@ -24,59 +24,19 @@ import android.util.IntProperty import android.view.View import android.view.ViewGroup import android.view.animation.Interpolator import kotlin.math.max import kotlin.math.min /** * A class that allows changes in bounds within a view hierarchy to animate seamlessly between the * start and end state. */ class ViewHierarchyAnimator { // TODO(b/221418522): make this private once it can't be passed as an arg anymore. enum class Bound(val label: String, val overrideTag: Int) { LEFT("left", R.id.tag_override_left) { override fun setValue(view: View, value: Int) { view.left = value } override fun getValue(view: View): Int { return view.left } }, TOP("top", R.id.tag_override_top) { override fun setValue(view: View, value: Int) { view.top = value } override fun getValue(view: View): Int { return view.top } }, RIGHT("right", R.id.tag_override_right) { override fun setValue(view: View, value: Int) { view.right = value } override fun getValue(view: View): Int { return view.right } }, BOTTOM("bottom", R.id.tag_override_bottom) { override fun setValue(view: View, value: Int) { view.bottom = value } override fun getValue(view: View): Int { return view.bottom } }; abstract fun setValue(view: View, value: Int) abstract fun getValue(view: View): Int } companion object { /** Default values for the animation. These can all be overridden at call time. */ private const val DEFAULT_DURATION = 500L private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE private val DEFAULT_BOUNDS = setOf(Bound.LEFT, Bound.TOP, Bound.RIGHT, Bound.BOTTOM) /** The properties used to animate the view bounds. */ Loading Loading @@ -112,6 +72,9 @@ class ViewHierarchyAnimator { * Successive calls to this method override the previous settings ([interpolator] and * [duration]). The changes take effect on the next animation. * * Returns true if the [rootView] is already visible and will be animated, false otherwise. * To animate the addition of a view, see [animateAddition]. * * TODO(b/221418522): remove the ability to select which bounds to animate and always * animate all of them. */ Loading @@ -121,8 +84,8 @@ class ViewHierarchyAnimator { bounds: Set<Bound> = DEFAULT_BOUNDS, interpolator: Interpolator = DEFAULT_INTERPOLATOR, duration: Long = DEFAULT_DURATION ) { animate(rootView, bounds, interpolator, duration, false /* ephemeral */) ): Boolean { return animate(rootView, bounds, interpolator, duration, ephemeral = false) } /** Loading @@ -138,8 +101,8 @@ class ViewHierarchyAnimator { bounds: Set<Bound> = DEFAULT_BOUNDS, interpolator: Interpolator = DEFAULT_INTERPOLATOR, duration: Long = DEFAULT_DURATION ) { animate(rootView, bounds, interpolator, duration, true /* ephemeral */) ): Boolean { return animate(rootView, bounds, interpolator, duration, ephemeral = true) } private fun animate( Loading @@ -148,9 +111,42 @@ class ViewHierarchyAnimator { interpolator: Interpolator, duration: Long, ephemeral: Boolean ): Boolean { if (!isVisible( rootView.visibility, rootView.left, rootView.top, rootView.right, rootView.bottom ) ) { val listener = createListener(bounds, interpolator, duration, ephemeral) return false } val listener = createUpdateListener(bounds, interpolator, duration, ephemeral) recursivelyAddListener(rootView, listener) return true } /** * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation * for the specified [bounds], using [interpolator] and [duration]. * * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise * it keeps listening for further updates. */ private fun createUpdateListener( bounds: Set<Bound>, interpolator: Interpolator, duration: Long, ephemeral: Boolean ): View.OnLayoutChangeListener { return createListener( bounds, interpolator, duration, ephemeral ) } /** Loading @@ -173,11 +169,87 @@ class ViewHierarchyAnimator { } } /** * Instruct the animator to watch for changes to the layout of [rootView] and its children, * and animate the next time the hierarchy appears after not being visible. It uses the * given [interpolator] and [duration]. * * The start state of the animation is controlled by [origin]. This value can be any of the * four corners, any of the four edges, or the center of the view. If any margins are added * on the side(s) of the origin, the translation of those margins can be included by * specifying [includeMargins]. * * Returns true if the [rootView] is invisible and will be animated, false otherwise. To * animate an already visible view, see [animate] and [animateNextUpdate]. * * Then animator unregisters itself once the first addition animation is complete. */ @JvmOverloads fun animateAddition( rootView: View, origin: Hotspot = Hotspot.CENTER, interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR, duration: Long = DEFAULT_DURATION, includeMargins: Boolean = false ): Boolean { if (isVisible( rootView.visibility, rootView.left, rootView.top, rootView.right, rootView.bottom ) ) { return false } val listener = createAdditionListener( origin, interpolator, duration, ignorePreviousValues = !includeMargins ) recursivelyAddListener(rootView, listener) return true } /** * Returns a new [View.OnLayoutChangeListener] that on the next call triggers a layout * addition animation from the given [origin], using [interpolator] and [duration]. * * If [ignorePreviousValues] is true, the animation will only span the area covered by the * new bounds. Otherwise it will include the margins between the previous and new bounds. */ private fun createAdditionListener( origin: Hotspot, interpolator: Interpolator, duration: Long, ignorePreviousValues: Boolean ): View.OnLayoutChangeListener { return createListener( DEFAULT_BOUNDS, interpolator, duration, ephemeral = true, origin = origin, ignorePreviousValues = ignorePreviousValues ) } /** * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation * for the specified [bounds], using [interpolator] and [duration]. * * If [ephemeral] is true, the listener is unregistered after the first animation. Otherwise * it keeps listening for further updates. * * [origin] specifies whether the start values should be determined by a hotspot, and * [ignorePreviousValues] controls whether the previous values should be taken into account. */ private fun createListener( bounds: Set<Bound>, interpolator: Interpolator, duration: Long, ephemeral: Boolean ephemeral: Boolean, origin: Hotspot? = null, ignorePreviousValues: Boolean = false ): View.OnLayoutChangeListener { return object : View.OnLayoutChangeListener { override fun onLayoutChange( Loading @@ -186,21 +258,21 @@ class ViewHierarchyAnimator { top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int previousLeft: Int, previousTop: Int, previousRight: Int, previousBottom: Int ) { if (view == null) return val startLeft = getBound(view, Bound.LEFT) ?: oldLeft val startTop = getBound(view, Bound.TOP) ?: oldTop val startRight = getBound(view, Bound.RIGHT) ?: oldRight val startBottom = getBound(view, Bound.BOTTOM) ?: oldBottom val startLeft = getBound(view, Bound.LEFT) ?: previousLeft val startTop = getBound(view, Bound.TOP) ?: previousTop val startRight = getBound(view, Bound.RIGHT) ?: previousRight val startBottom = getBound(view, Bound.BOTTOM) ?: previousBottom (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() if (view.visibility == View.GONE || view.visibility == View.INVISIBLE) { if (!isVisible(view.visibility, left, top, right, bottom)) { setBound(view, Bound.LEFT, left) setBound(view, Bound.TOP, top) setBound(view, Bound.RIGHT, right) Loading @@ -208,11 +280,17 @@ class ViewHierarchyAnimator { return } val startValues = mapOf( Bound.LEFT to startLeft, Bound.TOP to startTop, Bound.RIGHT to startRight, Bound.BOTTOM to startBottom val startValues = processStartValues( origin, left, top, right, bottom, startLeft, startTop, startRight, startBottom, ignorePreviousValues ) val endValues = mapOf( Bound.LEFT to left, Loading @@ -221,11 +299,12 @@ class ViewHierarchyAnimator { Bound.BOTTOM to bottom ) val boundsToAnimate = bounds.toMutableSet() if (left == startLeft) boundsToAnimate.remove(Bound.LEFT) if (top == startTop) boundsToAnimate.remove(Bound.TOP) if (right == startRight) boundsToAnimate.remove(Bound.RIGHT) if (bottom == startBottom) boundsToAnimate.remove(Bound.BOTTOM) val boundsToAnimate = mutableSetOf<Bound>() bounds.forEach { bound -> if (endValues.getValue(bound) != startValues.getValue(bound)) { boundsToAnimate.add(bound) } } if (boundsToAnimate.isNotEmpty()) { startAnimation( Loading @@ -242,11 +321,136 @@ class ViewHierarchyAnimator { } } /** * Returns whether the given [visibility] and bounds are consistent with a view being * currently visible on screen. */ private fun isVisible( visibility: Int, left: Int, top: Int, right: Int, bottom: Int ): Boolean { return visibility == View.VISIBLE && left != right && top != bottom } /** * Compute 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 * the same as the new values if [ignorePreviousValues] is true. If [origin] is not null, * the start values are resolved based on it, and [ignorePreviousValues] controls whether or * not newly introduced margins are included. * * Base case * 1) origin=TOP * x---------x x---------x x---------x x---------x x---------x * x---------x | | | | | | * -> -> x---------x -> | | -> | | * x---------x | | * x---------x * 2) origin=BOTTOM_LEFT * x---------x * x-------x | | * -> -> x----x -> | | -> | | * x--x | | | | | | * x x--x x----x x-------x x---------x * 3) origin=CENTER * x---------x * x-----x x-------x | | * x -> x---x -> | | -> | | -> | | * x-----x x-------x | | * x---------x * * In case the start and end values differ in the direction of the origin, and * [ignorePreviousValues] is false, the previous values are used and a translation is * included in addition to the view expansion. * * origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70) * x * x--x * x--x x----x * -> -> | | -> x------x * x----x | | * | | * x------x */ private fun processStartValues( origin: Hotspot?, newLeft: Int, newTop: Int, newRight: Int, newBottom: Int, previousLeft: Int, previousTop: Int, previousRight: Int, previousBottom: Int, ignorePreviousValues: Boolean ): Map<Bound, Int> { val startLeft = if (ignorePreviousValues) newLeft else previousLeft val startTop = if (ignorePreviousValues) newTop else previousTop val startRight = if (ignorePreviousValues) newRight else previousRight val startBottom = if (ignorePreviousValues) newBottom else previousBottom var left = startLeft var top = startTop var right = startRight var bottom = startBottom if (origin != null) { left = when (origin) { Hotspot.CENTER -> (newLeft + newRight) / 2 Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) Hotspot.TOP, Hotspot.BOTTOM -> newLeft Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( startRight, newRight ) } top = when (origin) { Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) Hotspot.LEFT, Hotspot.RIGHT -> newTop Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( startBottom, newBottom ) } right = when (origin) { Hotspot.CENTER -> (newLeft + newRight) / 2 Hotspot.TOP_RIGHT, Hotspot.RIGHT, Hotspot.BOTTOM_RIGHT -> max( startRight, newRight ) Hotspot.TOP, Hotspot.BOTTOM -> newRight Hotspot.BOTTOM_LEFT, Hotspot.LEFT, Hotspot.TOP_LEFT -> min(startLeft, newLeft) } bottom = when (origin) { Hotspot.CENTER -> (newTop + newBottom) / 2 Hotspot.BOTTOM_RIGHT, Hotspot.BOTTOM, Hotspot.BOTTOM_LEFT -> max( startBottom, newBottom ) Hotspot.LEFT, Hotspot.RIGHT -> newBottom Hotspot.TOP_LEFT, Hotspot.TOP, Hotspot.TOP_RIGHT -> min(startTop, newTop) } } return mapOf( Bound.LEFT to left, Bound.TOP to top, Bound.RIGHT to right, Bound.BOTTOM to bottom ) } private fun recursivelyAddListener(view: View, listener: View.OnLayoutChangeListener) { // Make sure that only one listener is active at a time. val oldListener = view.getTag(R.id.tag_layout_listener) if (oldListener != null && oldListener is View.OnLayoutChangeListener) { view.removeOnLayoutChangeListener(oldListener) val previousListener = view.getTag(R.id.tag_layout_listener) if (previousListener != null && previousListener is View.OnLayoutChangeListener) { view.removeOnLayoutChangeListener(previousListener) } view.addOnLayoutChangeListener(listener) Loading @@ -268,9 +472,12 @@ class ViewHierarchyAnimator { } /** * Initiates the animation of a single bound by creating the animator, registering it with * the [view], and starting it. If [ephemeral], the layout change listener is unregistered * at the end of the animation, so no more animations happen. * Initiates the animation of the requested [bounds] between [startValues] and [endValues] * by creating the animator, registering it with the [view], and starting it using * [interpolator] and [duration]. * * If [ephemeral] is true, the layout change listener is unregistered at the end of the * animation, so no more animations happen. */ private fun startAnimation( view: View, Loading Loading @@ -325,4 +532,52 @@ class ViewHierarchyAnimator { animator.start() } } /** An enum used to determine the origin of addition animations. */ enum class Hotspot { CENTER, LEFT, TOP_LEFT, TOP, TOP_RIGHT, RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT } // TODO(b/221418522): make this private once it can't be passed as an arg anymore. enum class Bound(val label: String, val overrideTag: Int) { LEFT("left", R.id.tag_override_left) { override fun setValue(view: View, value: Int) { view.left = value } override fun getValue(view: View): Int { return view.left } }, TOP("top", R.id.tag_override_top) { override fun setValue(view: View, value: Int) { view.top = value } override fun getValue(view: View): Int { return view.top } }, RIGHT("right", R.id.tag_override_right) { override fun setValue(view: View, value: Int) { view.right = value } override fun getValue(view: View): Int { return view.right } }, BOTTOM("bottom", R.id.tag_override_bottom) { override fun setValue(view: View, value: Int) { view.bottom = value } override fun getValue(view: View): Int { return view.bottom } }; abstract fun setValue(view: View, value: Int) abstract fun getValue(view: View): Int } }
packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +461 −21 File changed.Preview size limit exceeded, changes collapsed. Show changes