Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 39a6efe9 authored by Luca Zuccarini's avatar Luca Zuccarini
Browse files

Introduce ComposableControllerFactory.

This Compose-specific controller factory will be used to ensure that
composables are available as soon as composed and only while composed.

Bug: 202516970
Flag: EXEMPT unused, will be protected on usage
Test: manual with prototype, see ag/31506293
Change-Id: I33491b6d50c075ccca19e4ffe1b1ca83049447c3
parent 7479cb10
Loading
Loading
Loading
Loading
+60 −0
Original line number Diff line number Diff line
/*
 * 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.content.ComponentName
import android.util.Log
import com.android.systemui.animation.ActivityTransitionAnimator.Controller
import com.android.systemui.animation.ActivityTransitionAnimator.ControllerFactory
import kotlinx.coroutines.flow.MutableStateFlow

private const val TAG = "ComposableControllerFactory"

/**
 * [ControllerFactory] extension for Compose. Since composables are not guaranteed to be part of the
 * composition when [ControllerFactory.createController] is called, this class provides a way for
 * the composable to register itself at the time of composition, and deregister itself when
 * disposed.
 */
abstract class ComposableControllerFactory(
    cookie: ActivityTransitionAnimator.TransitionCookie,
    component: ComponentName?,
    launchCujType: Int? = null,
    returnCujType: Int? = null,
) : ControllerFactory(cookie, component, launchCujType, returnCujType) {
    /**
     * The object to be used to create [Controller]s, when its associate composable is in the
     * composition.
     */
    protected val expandable = MutableStateFlow<Expandable?>(null)

    /** To be called when the composable to be animated enters composition. */
    fun onCompose(expandable: Expandable) {
        if (TransitionAnimator.DEBUG) {
            Log.d(TAG, "Composable entered composition (expandable=$expandable")
        }
        this.expandable.value = expandable
    }

    /** To be called when the composable to be animated exits composition. */
    fun onDispose() {
        if (TransitionAnimator.DEBUG) {
            Log.d(TAG, "Composable left composition (expandable=${this.expandable.value}")
        }
        this.expandable.value = null
    }
}
+24 −1
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.android.compose.modifiers.thenIf
import com.android.compose.ui.graphics.FullScreenComposeViewInOverlay
import com.android.systemui.animation.ComposableControllerFactory
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.TransitionAnimator
import kotlin.math.max
@@ -119,6 +120,10 @@ import kotlin.math.min
 *    }
 * ```
 *
 * [transitionControllerFactory] must be defined when this [Expandable] is registered for a
 * long-term launch or return animation, to ensure that animation controllers can be created
 * correctly.
 *
 * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen
 * @sample com.android.systemui.compose.gallery.DialogLaunchScreen
 */
@@ -134,10 +139,17 @@ fun Expandable(
    // TODO(b/285250939): Default this to true then remove once the Compose QS expandables have
    // proven that the new implementation is robust.
    useModifierBasedImplementation: Boolean = false,
    transitionControllerFactory: ComposableControllerFactory? = null,
    content: @Composable (Expandable) -> Unit,
) {
    Expandable(
        rememberExpandableController(color, shape, contentColor, borderStroke),
        rememberExpandableController(
            color,
            shape,
            contentColor,
            borderStroke,
            transitionControllerFactory,
        ),
        modifier,
        onClick,
        interactionSource,
@@ -183,6 +195,17 @@ fun Expandable(
) {
    val controller = controller as ExpandableControllerImpl

    if (controller.transitionControllerFactory != null) {
        DisposableEffect(controller.transitionControllerFactory) {
            // Notify the transition controller factory that the expandable is now available, so it
            // can move forward with any pending requests.
            controller.transitionControllerFactory.onCompose(controller.expandable)
            // Once this composable is gone, the transition controller factory must be notified so
            // it doesn't accepts requests providing stale content.
            onDispose { controller.transitionControllerFactory.onDispose() }
        }
    }

    if (useModifierBasedImplementation) {
        Box(modifier.expandable(controller, onClick, interactionSource)) {
            WrappedContent(controller.expandable, controller.contentColor, content)
+5 −0
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.ComposableControllerFactory
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
@@ -77,6 +78,7 @@ fun rememberExpandableController(
    shape: Shape,
    contentColor: Color = contentColorFor(color),
    borderStroke: BorderStroke? = null,
    transitionControllerFactory: ComposableControllerFactory? = null,
): ExpandableController {
    val composeViewRoot = LocalView.current
    val density = LocalDensity.current
@@ -95,6 +97,7 @@ fun rememberExpandableController(
            composeViewRoot,
            density,
            layoutDirection,
            transitionControllerFactory,
        ) {
            ExpandableControllerImpl(
                color,
@@ -103,6 +106,7 @@ fun rememberExpandableController(
                borderStroke,
                composeViewRoot,
                density,
                transitionControllerFactory,
                layoutDirection,
                { isComposed },
            )
@@ -127,6 +131,7 @@ internal class ExpandableControllerImpl(
    internal val borderStroke: BorderStroke?,
    internal val composeViewRoot: View,
    internal val density: Density,
    internal val transitionControllerFactory: ComposableControllerFactory?,
    private val layoutDirection: LayoutDirection,
    private val isComposed: () -> Boolean,
) : ExpandableController {