diff options
| author | 2025-02-14 02:08:50 -0800 | |
|---|---|---|
| committer | 2025-02-14 02:08:50 -0800 | |
| commit | 0329a8ccb81397dd90fc4997f148ae60f6565fca (patch) | |
| tree | 79b7c7161accadccd951d5eee66555bfc86e7c52 | |
| parent | b52e5663e2f5aea3dd74ba5aa2434f5b87b29c9a (diff) | |
| parent | 333c1672d8884cbc606a086b868ef50ad34a1bfa (diff) | |
Merge "[AnimLib] Integrate registry into controller" into main
5 files changed, 200 insertions, 21 deletions
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt index 65cd3c79cd16..444389fb26ea 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -36,6 +36,7 @@ import android.view.ViewGroupOverlay import android.widget.FrameLayout import com.android.internal.jank.Cuj.CujType import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import java.util.LinkedList import kotlin.math.min import kotlin.math.roundToInt @@ -58,7 +59,7 @@ open class GhostedViewTransitionAnimatorController @JvmOverloads constructor( /** The view that will be ghosted and from which the background will be extracted. */ - private val ghostedView: View, + transitioningView: View, /** The [CujType] associated to this launch animation. */ private val launchCujType: Int? = null, @@ -75,11 +76,24 @@ constructor( private val isEphemeral: Boolean = false, private var interactionJankMonitor: InteractionJankMonitor = InteractionJankMonitor.getInstance(), + + /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */ + private val transitionRegistry: IViewTransitionRegistry? = + if (Flags.decoupleViewControllerInAnimlib()) { + ViewTransitionRegistry.instance + } else { + null + } ) : ActivityTransitionAnimator.Controller { override val isLaunching: Boolean = true /** The container to which we will add the ghost view and expanding background. */ - override var transitionContainer = ghostedView.rootView as ViewGroup + override var transitionContainer: ViewGroup + get() = ghostedView.rootView as ViewGroup + set(_) { + // empty, should never be set to avoid memory leak + } + private val transitionContainerOverlay: ViewGroupOverlay get() = transitionContainer.overlay @@ -138,9 +152,33 @@ constructor( } } + /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */ + private val transitionToken = + if (Flags.decoupleViewControllerInAnimlib()) { + ViewTransitionToken(transitioningView::class.java) + } else { + null + } + + /** The view that will be ghosted and from which the background will be extracted */ + private val ghostedView: View + get() = + if (Flags.decoupleViewControllerInAnimlib()) { + transitionRegistry?.getView(transitionToken!!) + } else { + _ghostedView + }!! + + private val _ghostedView = + if (Flags.decoupleViewControllerInAnimlib()) { + null + } else { + transitioningView + } + init { // Make sure the View we launch from implements LaunchableView to avoid visibility issues. - if (ghostedView !is LaunchableView) { + if (transitioningView !is LaunchableView) { throw IllegalArgumentException( "A GhostedViewLaunchAnimatorController was created from a View that does not " + "implement LaunchableView. This can lead to subtle bugs where the visibility " + @@ -148,6 +186,10 @@ constructor( ) } + if (Flags.decoupleViewControllerInAnimlib()) { + transitionRegistry?.register(transitionToken!!, transitioningView) + } + /** Find the first view with a background in [view] and its children. */ fun findBackground(view: View): Drawable? { if (view.background != null) { @@ -184,6 +226,7 @@ constructor( if (TransitionAnimator.returnAnimationsEnabled()) { ghostedView.removeOnAttachStateChangeListener(detachListener) } + transitionToken?.let { token -> transitionRegistry?.unregister(token) } } /** @@ -237,7 +280,7 @@ constructor( val insets = backgroundInsets val boundCorrections: Rect = if (ghostedView is LaunchableView) { - ghostedView.getPaddingForLaunchAnimation() + (ghostedView as LaunchableView).getPaddingForLaunchAnimation() } else { Rect() } @@ -387,8 +430,8 @@ constructor( if (ghostedView is LaunchableView) { // Restore the ghosted view visibility. - ghostedView.setShouldBlockVisibilityChanges(false) - ghostedView.onActivityLaunchAnimationEnd() + (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false) + (ghostedView as LaunchableView).onActivityLaunchAnimationEnd() } else { // Make the ghosted view visible. We ensure that the view is considered VISIBLE by // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 @@ -398,7 +441,7 @@ constructor( ghostedView.invalidate() } - if (isEphemeral) { + if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) { onDispose() } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt new file mode 100644 index 000000000000..af3ca87bf788 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 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.view.View + +/** Represents a Registry for holding a transitioning view mapped to a token */ +interface IViewTransitionRegistry { + + /** + * Registers the transitioning [view] mapped to a [token] + * + * @param token The token corresponding to the transitioning view + * @param view The view undergoing transition + */ + fun register(token: ViewTransitionToken, view: View) + + /** + * Unregisters the transitioned view from its corresponding [token] + * + * @param token The token corresponding to the transitioning view + */ + fun unregister(token: ViewTransitionToken) + + /** + * Extracts a transitioning view from registry using its corresponding [token] + * + * @param token The token corresponding to the transitioning view + */ + fun getView(token: ViewTransitionToken): View? + + /** + * Return token mapped to the [view], if it is present in the registry + * + * @param view the transitioning view whose token we are requesting + * @return token associated with the [view] if present, else null + */ + fun getViewToken(view: View): ViewTransitionToken? + + /** Event call to run on registry update (on both [register] and [unregister]) */ + fun onRegistryUpdate() +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt index 58c2a1c98ec4..86c7f76c6bee 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt @@ -24,14 +24,14 @@ import java.lang.ref.WeakReference * A registry to temporarily store the view being transitioned into a Dialog (using * [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator]) */ -class ViewTransitionRegistry { +class ViewTransitionRegistry : IViewTransitionRegistry { /** * A map of a unique token to a WeakReference of the View being transitioned. WeakReference * ensures that Views are garbage collected whenever they become eligible and avoid any * memory leaks */ - private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() } + private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() } /** * A [View.OnAttachStateChangeListener] to be attached to all views stored in the registry to @@ -45,8 +45,7 @@ class ViewTransitionRegistry { } override fun onViewDetachedFromWindow(view: View) { - (view.getTag(R.id.tag_view_transition_token) - as? ViewTransitionToken)?.let { token -> unregister(token) } + getViewToken(view)?.let { token -> unregister(token) } } } } @@ -57,12 +56,12 @@ class ViewTransitionRegistry { * @param token unique token associated with the transitioning view * @param view view undergoing transitions */ - fun register(token: ViewTransitionToken, view: View) { + override fun register(token: ViewTransitionToken, view: View) { // token embedded as a view tag enables to use a single listener for all views view.setTag(R.id.tag_view_transition_token, token) view.addOnAttachStateChangeListener(listener) registry[token] = WeakReference(view) - emitCountForTrace() + onRegistryUpdate() } /** @@ -70,30 +69,51 @@ class ViewTransitionRegistry { * * @param token unique token associated with the transitioning view */ - fun unregister(token: ViewTransitionToken) { + override fun unregister(token: ViewTransitionToken) { registry.remove(token)?.let { it.get()?.let { view -> view.removeOnAttachStateChangeListener(listener) view.setTag(R.id.tag_view_transition_token, null) } it.clear() + onRegistryUpdate() } - emitCountForTrace() } /** * Access a view from registry using unique "token" associated with it * WARNING - this returns a StrongReference to the View stored in the registry */ - fun getView(token: ViewTransitionToken): View? { + override fun getView(token: ViewTransitionToken): View? { return registry[token]?.get() } /** + * Return token mapped to the [view], if it is present in the registry + * + * @param view the transitioning view whose token we are requesting + * @return token associated with the [view] if present, else null + */ + override fun getViewToken(view: View): ViewTransitionToken? { + return (view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken)?.let { token -> + getView(token)?.let { token } + } + } + + /** Event call to run on registry update (on both [register] and [unregister]) */ + override fun onRegistryUpdate() { + emitCountForTrace() + } + + /** * Utility function to emit number of non-null views in the registry whenever the registry is * updated (via [register] or [unregister]) */ private fun emitCountForTrace() { Trace.setCounter("transition_registry_view_count", registry.count().toLong()) } + + companion object { + val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ViewTransitionRegistry() } + } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt index c211a8ed1de2..e011df01504f 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt @@ -16,17 +16,19 @@ package com.android.systemui.animation +import java.util.UUID + /** * A token uniquely mapped to a View in [ViewTransitionRegistry]. This token is guaranteed to be * unique as timestamp is appended to the token string * - * @constructor creates an instance of [ViewTransitionToken] with token as "timestamp" or - * "ClassName_timestamp" + * @constructor creates an instance of [ViewTransitionToken] with token as "UUID" or + * "ClassName_UUID" * * @property token String value of a unique token */ @JvmInline value class ViewTransitionToken private constructor(val token: String) { - constructor() : this(token = System.currentTimeMillis().toString()) - constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${System.currentTimeMillis()}") + constructor() : this(token = UUID.randomUUID().toString()) + constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${UUID.randomUUID()}") } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt index e492c63d095c..052d520ac92f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt @@ -17,16 +17,20 @@ package com.android.systemui.animation import android.os.HandlerThread +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.view.View import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.view.LaunchableFrameLayout import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -40,6 +44,14 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { } private val interactionJankMonitor = FakeInteractionJankMonitor() + private lateinit var transitionRegistry: FakeViewTransitionRegistry + private lateinit var transitioningView: View + + @Before + fun setup() { + transitioningView = LaunchableFrameLayout(mContext) + transitionRegistry = FakeViewTransitionRegistry() + } @Test fun animatingOrphanViewDoesNotCrash() { @@ -67,7 +79,7 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { parent.addView((launchView)) val launchController = GhostedViewTransitionAnimatorController( - launchView, + launchView, launchCujType = LAUNCH_CUJ, returnCujType = RETURN_CUJ, interactionJankMonitor = interactionJankMonitor @@ -96,6 +108,26 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ) } + @EnableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB) + @Test + fun testViewsAreRegisteredInTransitionRegistry() { + GhostedViewTransitionAnimatorController( + transitioningView = transitioningView, + transitionRegistry = transitionRegistry + ) + assertThat(transitionRegistry.registry).isNotEmpty() + } + + @DisableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB) + @Test + fun testNotUseRegistryIfDecouplingFlagDisabled() { + GhostedViewTransitionAnimatorController( + transitioningView = transitioningView, + transitionRegistry = transitionRegistry + ) + assertThat(transitionRegistry.registry).isEmpty() + } + /** * A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and * allows inspection. @@ -117,4 +149,30 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { return true } } + + private class FakeViewTransitionRegistry : IViewTransitionRegistry { + + val registry = mutableMapOf<ViewTransitionToken, View>() + + override fun register(token: ViewTransitionToken, view: View) { + registry[token] = view + view.setTag(R.id.tag_view_transition_token, token) + } + + override fun unregister(token: ViewTransitionToken) { + registry.remove(token)?.setTag(R.id.tag_view_transition_token, null) + } + + override fun getView(token: ViewTransitionToken): View? { + return registry[token] + } + + override fun getViewToken(view: View): ViewTransitionToken? { + return view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken + } + + override fun onRegistryUpdate() { + //empty + } + } } |