| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.animation |
| |
| 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 |
| 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 { |
| 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.STANDARD |
| private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE |
| private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE |
| private val DEFAULT_FADE_IN_INTERPOLATOR = Interpolators.ALPHA_IN |
| |
| /** The properties used to animate the view bounds. */ |
| private val PROPERTIES = |
| mapOf( |
| Bound.LEFT to createViewProperty(Bound.LEFT), |
| Bound.TOP to createViewProperty(Bound.TOP), |
| Bound.RIGHT to createViewProperty(Bound.RIGHT), |
| Bound.BOTTOM to createViewProperty(Bound.BOTTOM) |
| ) |
| |
| private fun createViewProperty(bound: Bound): IntProperty<View> { |
| return object : IntProperty<View>(bound.label) { |
| override fun setValue(view: View, value: Int) { |
| setBound(view, bound, value) |
| } |
| |
| override fun get(view: View): Int { |
| return getBound(view, bound) ?: bound.getValue(view) |
| } |
| } |
| } |
| |
| /** |
| * Instruct the animator to watch for changes to the layout of [rootView] and its children |
| * and animate them. It uses the given [interpolator] and [duration]. |
| * |
| * If a new layout change happens while an animation is already in progress, the animation |
| * is updated to continue from the current values to the new end state. |
| * |
| * The animator continues to respond to layout changes until [stopAnimating] is called. |
| * |
| * 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]. |
| */ |
| @JvmOverloads |
| fun animate( |
| rootView: View, |
| interpolator: Interpolator = DEFAULT_INTERPOLATOR, |
| duration: Long = DEFAULT_DURATION |
| ): Boolean { |
| return animate(rootView, interpolator, duration, ephemeral = false) |
| } |
| |
| /** |
| * Like [animate], but only takes effect on the next layout update, then unregisters itself |
| * once the first animation is complete. |
| */ |
| @JvmOverloads |
| fun animateNextUpdate( |
| rootView: View, |
| interpolator: Interpolator = DEFAULT_INTERPOLATOR, |
| duration: Long = DEFAULT_DURATION |
| ): Boolean { |
| return animate(rootView, interpolator, duration, ephemeral = true) |
| } |
| |
| private fun animate( |
| rootView: View, |
| interpolator: Interpolator, |
| duration: Long, |
| ephemeral: Boolean |
| ): Boolean { |
| if ( |
| !isVisible( |
| rootView.visibility, |
| rootView.left, |
| rootView.top, |
| rootView.right, |
| rootView.bottom |
| ) |
| ) { |
| return false |
| } |
| |
| val listener = createUpdateListener(interpolator, duration, ephemeral) |
| addListener(rootView, listener, recursive = true) |
| return true |
| } |
| |
| /** |
| * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation |
| * 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( |
| interpolator: Interpolator, |
| duration: Long, |
| ephemeral: Boolean |
| ): View.OnLayoutChangeListener { |
| return createListener(interpolator, duration, ephemeral) |
| } |
| |
| /** |
| * Instruct the animator to stop watching for changes to the layout of [rootView] and its |
| * children. |
| * |
| * Any animations already in progress continue until their natural conclusion. |
| */ |
| fun stopAnimating(rootView: View) { |
| recursivelyRemoveListener(rootView) |
| } |
| |
| /** |
| * 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. |
| * |
| * @param includeFadeIn true if the animator should also fade in the view and child views. |
| * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if |
| * [includeFadeIn] is false. |
| */ |
| @JvmOverloads |
| fun animateAddition( |
| rootView: View, |
| origin: Hotspot = Hotspot.CENTER, |
| interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR, |
| duration: Long = DEFAULT_DURATION, |
| includeMargins: Boolean = false, |
| includeFadeIn: Boolean = false, |
| fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR |
| ): Boolean { |
| if ( |
| isVisible( |
| rootView.visibility, |
| rootView.left, |
| rootView.top, |
| rootView.right, |
| rootView.bottom |
| ) |
| ) { |
| return false |
| } |
| |
| val listener = |
| createAdditionListener( |
| origin, |
| interpolator, |
| duration, |
| ignorePreviousValues = !includeMargins |
| ) |
| addListener(rootView, listener, recursive = true) |
| |
| if (!includeFadeIn) { |
| return true |
| } |
| |
| if (rootView is ViewGroup) { |
| // First, fade in the container view |
| val containerDuration = duration / 6 |
| createAndStartFadeInAnimator( |
| rootView, containerDuration, startDelay = 0, interpolator = fadeInInterpolator |
| ) |
| |
| // Then, fade in the child views |
| val childDuration = duration / 3 |
| for (i in 0 until rootView.childCount) { |
| val view = rootView.getChildAt(i) |
| createAndStartFadeInAnimator( |
| view, |
| childDuration, |
| // Wait until the container fades in before fading in the children |
| startDelay = containerDuration, |
| interpolator = fadeInInterpolator |
| ) |
| } |
| // For now, we don't recursively fade in additional sub views (e.g. grandchild |
| // views) since it hasn't been necessary, but we could add that functionality. |
| } else { |
| // Fade in the view during the first half of the addition |
| createAndStartFadeInAnimator( |
| rootView, |
| duration / 2, |
| startDelay = 0, |
| interpolator = fadeInInterpolator |
| ) |
| } |
| |
| 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( |
| interpolator, |
| duration, |
| ephemeral = true, |
| origin = origin, |
| ignorePreviousValues = ignorePreviousValues |
| ) |
| } |
| |
| /** |
| * Returns a new [View.OnLayoutChangeListener] that when called triggers a layout animation |
| * 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( |
| interpolator: Interpolator, |
| duration: Long, |
| ephemeral: Boolean, |
| origin: Hotspot? = null, |
| ignorePreviousValues: Boolean = false |
| ): View.OnLayoutChangeListener { |
| return object : View.OnLayoutChangeListener { |
| override fun onLayoutChange( |
| view: View?, |
| left: Int, |
| top: Int, |
| right: Int, |
| bottom: Int, |
| previousLeft: Int, |
| previousTop: Int, |
| previousRight: Int, |
| previousBottom: Int |
| ) { |
| if (view == null) return |
| |
| 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 (!isVisible(view.visibility, left, top, right, bottom)) { |
| setBound(view, Bound.LEFT, left) |
| setBound(view, Bound.TOP, top) |
| setBound(view, Bound.RIGHT, right) |
| setBound(view, Bound.BOTTOM, bottom) |
| return |
| } |
| |
| val startValues = |
| processStartValues( |
| origin, |
| left, |
| top, |
| right, |
| bottom, |
| startLeft, |
| startTop, |
| startRight, |
| startBottom, |
| ignorePreviousValues |
| ) |
| val endValues = |
| mapOf( |
| Bound.LEFT to left, |
| Bound.TOP to top, |
| Bound.RIGHT to right, |
| Bound.BOTTOM to bottom |
| ) |
| |
| val boundsToAnimate = mutableSetOf<Bound>() |
| if (startValues.getValue(Bound.LEFT) != left) boundsToAnimate.add(Bound.LEFT) |
| if (startValues.getValue(Bound.TOP) != top) boundsToAnimate.add(Bound.TOP) |
| if (startValues.getValue(Bound.RIGHT) != right) boundsToAnimate.add(Bound.RIGHT) |
| if (startValues.getValue(Bound.BOTTOM) != bottom) { |
| boundsToAnimate.add(Bound.BOTTOM) |
| } |
| |
| if (boundsToAnimate.isNotEmpty()) { |
| startAnimation( |
| view, |
| boundsToAnimate, |
| startValues, |
| endValues, |
| interpolator, |
| duration, |
| ephemeral |
| ) |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| private fun isVisible( |
| visibility: Int, |
| left: Int, |
| top: Int, |
| right: Int, |
| bottom: Int |
| ): Boolean { |
| return visibility == View.VISIBLE && left != right && top != bottom |
| } |
| |
| /** |
| * 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 |
| * 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 |
| ) |
| } |
| |
| /** |
| * 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) { |
| view.removeOnLayoutChangeListener(previousListener) |
| } |
| |
| view.addOnLayoutChangeListener(listener) |
| view.setTag(R.id.tag_layout_listener, listener) |
| if (view is ViewGroup && recursive) { |
| for (i in 0 until view.childCount) { |
| addListener(view.getChildAt(i), listener, recursive = true) |
| } |
| } |
| } |
| |
| private fun recursivelyRemoveListener(view: View) { |
| val listener = view.getTag(R.id.tag_layout_listener) |
| if (listener != null && listener is View.OnLayoutChangeListener) { |
| view.setTag(R.id.tag_layout_listener, null /* tag */) |
| view.removeOnLayoutChangeListener(listener) |
| } |
| |
| if (view is ViewGroup) { |
| for (i in 0 until view.childCount) { |
| recursivelyRemoveListener(view.getChildAt(i)) |
| } |
| } |
| } |
| |
| private fun getBound(view: View, bound: Bound): Int? { |
| return view.getTag(bound.overrideTag) as? Int |
| } |
| |
| private fun setBound(view: View, bound: Bound, value: Int) { |
| view.setTag(bound.overrideTag, value) |
| bound.setValue(view, value) |
| } |
| |
| /** |
| * 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, |
| bounds: Set<Bound>, |
| startValues: Map<Bound, Int>, |
| endValues: Map<Bound, Int>, |
| interpolator: Interpolator, |
| duration: Long, |
| ephemeral: Boolean |
| ) { |
| val propertyValuesHolders = |
| buildList { |
| bounds.forEach { bound -> |
| add( |
| PropertyValuesHolder.ofInt( |
| PROPERTIES[bound], |
| startValues.getValue(bound), |
| endValues.getValue(bound) |
| ) |
| ) |
| } |
| } |
| .toTypedArray() |
| |
| (view.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel() |
| |
| val animator = ObjectAnimator.ofPropertyValuesHolder(view, *propertyValuesHolders) |
| animator.interpolator = interpolator |
| animator.duration = duration |
| animator.addListener( |
| object : AnimatorListenerAdapter() { |
| var cancelled = false |
| |
| override fun onAnimationEnd(animation: Animator) { |
| view.setTag(R.id.tag_animator, null /* tag */) |
| bounds.forEach { view.setTag(it.overrideTag, null /* tag */) } |
| |
| // When an animation is cancelled, a new one might be taking over. We |
| // shouldn't unregister the listener yet. |
| if (ephemeral && !cancelled) { |
| // The duration is the same for the whole hierarchy, so it's safe to |
| // remove the listener recursively. We do this because some descendant |
| // views might not change bounds, and therefore not animate and leak the |
| // listener. |
| recursivelyRemoveListener(view) |
| } |
| } |
| |
| override fun onAnimationCancel(animation: Animator?) { |
| cancelled = true |
| } |
| } |
| ) |
| |
| bounds.forEach { bound -> setBound(view, bound, startValues.getValue(bound)) } |
| |
| view.setTag(R.id.tag_animator, animator) |
| animator.start() |
| } |
| |
| private fun createAndStartFadeInAnimator( |
| view: View, |
| duration: Long, |
| startDelay: Long, |
| interpolator: Interpolator |
| ) { |
| val animator = ObjectAnimator.ofFloat(view, "alpha", 1f) |
| animator.startDelay = startDelay |
| animator.duration = duration |
| animator.interpolator = interpolator |
| animator.addListener(object : AnimatorListenerAdapter() { |
| override fun onAnimationEnd(animation: Animator) { |
| view.setTag(R.id.tag_alpha_animator, null /* tag */) |
| } |
| }) |
| |
| (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel() |
| view.setTag(R.id.tag_alpha_animator, animator) |
| 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 |
| } |
| |
| private 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 |
| } |
| } |