Allow ViewHierarchyAnimator to exclude child views from animations.

This has been a long requested feature and b/283456173 provided the
right opportunity to implement it. I have tested it within the privacy
dialog and the behavior is as expected.

Bug: 283456173
Test: unit tests included
Change-Id: I9b97e2e7aaff603174cabd4328f17ec1884816ee
This commit is contained in:
Luca Zuccarini
2023-07-13 10:44:53 +00:00
parent 0b73a074b2
commit b072e53bdd
2 changed files with 70 additions and 10 deletions

View File

@@ -70,6 +70,9 @@ class ViewHierarchyAnimator {
* 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.
*
* A set of [excludedViews] can be passed. If any dependent view from [rootView] matches an
* entry in this set, changes to that view will not be animated.
*
* The animator continues to respond to layout changes until [stopAnimating] is called.
*
* Successive calls to this method override the previous settings ([interpolator] and
@@ -82,9 +85,16 @@ class ViewHierarchyAnimator {
fun animate(
rootView: View,
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
duration: Long = DEFAULT_DURATION
duration: Long = DEFAULT_DURATION,
excludedViews: Set<View> = emptySet()
): Boolean {
return animate(rootView, interpolator, duration, ephemeral = false)
return animate(
rootView,
interpolator,
duration,
ephemeral = false,
excludedViews = excludedViews
)
}
/**
@@ -95,16 +105,24 @@ class ViewHierarchyAnimator {
fun animateNextUpdate(
rootView: View,
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
duration: Long = DEFAULT_DURATION
duration: Long = DEFAULT_DURATION,
excludedViews: Set<View> = emptySet()
): Boolean {
return animate(rootView, interpolator, duration, ephemeral = true)
return animate(
rootView,
interpolator,
duration,
ephemeral = true,
excludedViews = excludedViews
)
}
private fun animate(
rootView: View,
interpolator: Interpolator,
duration: Long,
ephemeral: Boolean
ephemeral: Boolean,
excludedViews: Set<View> = emptySet()
): Boolean {
if (
!occupiesSpace(
@@ -119,7 +137,7 @@ class ViewHierarchyAnimator {
}
val listener = createUpdateListener(interpolator, duration, ephemeral)
addListener(rootView, listener, recursive = true)
addListener(rootView, listener, recursive = true, excludedViews = excludedViews)
return true
}
@@ -921,8 +939,11 @@ class ViewHierarchyAnimator {
private fun addListener(
view: View,
listener: View.OnLayoutChangeListener,
recursive: Boolean = false
recursive: Boolean = false,
excludedViews: Set<View> = emptySet()
) {
if (excludedViews.contains(view)) return
// 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) {
@@ -933,7 +954,12 @@ class ViewHierarchyAnimator {
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)
addListener(
view.getChildAt(i),
listener,
recursive = true,
excludedViews = excludedViews
)
}
}
}

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.test.filters.SmallTest
import com.android.app.animation.Interpolators
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.children
import junit.framework.Assert.assertEquals
@@ -19,7 +20,6 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import com.android.app.animation.Interpolators
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -178,7 +178,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
}
@Test
fun animatesRootAndChildren() {
fun animatesRootAndChildren_withoutExcludedViews() {
setUpRootWithChildren()
val success = ViewHierarchyAnimator.animate(rootView)
@@ -207,6 +207,40 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
checkBounds(rootView.getChildAt(1), l = 95, t = 0, r = 190, b = 100)
}
@Test
fun animatesRootAndChildren_withExcludedViews() {
setUpRootWithChildren()
val success = ViewHierarchyAnimator.animate(
rootView,
excludedViews = setOf(rootView.getChildAt(0))
)
// Change all bounds.
rootView.measure(
View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
)
rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
assertTrue(success)
assertNotNull(rootView.getTag(R.id.tag_animator))
assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
// The initial values for the affected views should be those of the previous layout, while
// the excluded view should be at the final values from the beginning.
checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
endAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_animator))
assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
// The end values should be those of the latest layout.
checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
checkBounds(rootView.getChildAt(1), l = 90, t = 0, r = 180, b = 100)
}
@Test
fun animatesInvisibleViews() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)