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

Commit 9a94d1f6 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Introduce overlays in SceneTransitionLayout (1/2)

This CL adds a first basic support for overlays. Overlays can be either
shown or hidden, and STLState.currentOverlays holds the "effective" set
of current overlays.

There is no overlay transitions in this CL yet, they will be added in
ag/28576084.

See go/sysui-stl-overlay for more details about overlays.

Bug: 353679003
Test: atest OverlayTest
Flag: com.android.systemui.scene_container
Change-Id: Ic0b26a2b7bff41829c409aed3293c47da47db3f5
parent 318638dc
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -121,6 +121,8 @@ private fun SceneScope.stateForQuickSettingsContent(
                        )
                }
            }
        is TransitionState.Transition.OverlayTransition ->
            TODO("b/359173565: Handle overlay transitions")
    }
}

@@ -212,7 +214,8 @@ private fun QuickSettingsContent(
                            addView(view)
                        }
                    },
                    // When the view changes (e.g. due to a theme change), this will be recomposed
                    // When the view changes (e.g. due to a theme change), this will be
                    // recomposed
                    // if needed and the new view will be attached to the FrameLayout here.
                    update = {
                        qsSceneAdapter.setState(state())
+8 −5
Original line number Diff line number Diff line
@@ -393,7 +393,8 @@ private class AnimatedStateImpl<T, Delta>(
        transition: TransitionState.Transition?,
    ): T? {
        if (transition == null) {
            return sharedValue[layoutImpl.state.transitionState.currentScene]
            return sharedValue[content]
                ?: sharedValue[layoutImpl.state.transitionState.currentScene]
        }

        val fromValue = sharedValue[transition.fromContent]
@@ -424,10 +425,12 @@ private class AnimatedStateImpl<T, Delta>(
        val targetValues = sharedValue.targetValues
        val transition =
            if (element != null) {
                layoutImpl.elements[element]?.stateByContent?.let { sceneStates ->
                    layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
                        transition.fromContent in sceneStates || transition.toContent in sceneStates
                    }
                layoutImpl.elements[element]?.let { element ->
                    elementState(
                        layoutImpl.state.transitionStates,
                        isInContent = { it in element.stateByContent },
                    )
                        as? TransitionState.Transition
                }
            } else {
                layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
+4 −1
Original line number Diff line number Diff line
@@ -43,12 +43,15 @@ internal fun CoroutineScope.animateToScene(
    }

    return when (transitionState) {
        is TransitionState.Idle -> {
        is TransitionState.Idle,
        is TransitionState.Transition.ShowOrHideOverlay,
        is TransitionState.Transition.ReplaceOverlay -> {
            animateToScene(
                layoutState,
                target,
                transitionKey,
                isInitiatedByUserInput = false,
                fromScene = transitionState.currentScene,
                replacedTransition = null,
            )
        }
+162 −55
Original line number Diff line number Diff line
@@ -46,7 +46,7 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastLastOrNull
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState
@@ -145,8 +145,9 @@ internal fun Modifier.element(
    // layout/drawing.
    // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
    // we can ensure that SceneTransitionLayoutImpl will compose new contents first.
    val currentTransitions = layoutImpl.state.currentTransitions
    return then(ElementModifier(layoutImpl, currentTransitions, content, key)).testTag(key.testTag)
    val currentTransitionStates = layoutImpl.state.transitionStates
    return then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
        .testTag(key.testTag)
}

/**
@@ -155,20 +156,21 @@ internal fun Modifier.element(
 */
private data class ElementModifier(
    private val layoutImpl: SceneTransitionLayoutImpl,
    private val currentTransitions: List<TransitionState.Transition>,
    private val currentTransitionStates: List<TransitionState>,
    private val content: Content,
    private val key: ElementKey,
) : ModifierNodeElement<ElementNode>() {
    override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, content, key)
    override fun create(): ElementNode =
        ElementNode(layoutImpl, currentTransitionStates, content, key)

    override fun update(node: ElementNode) {
        node.update(layoutImpl, currentTransitions, content, key)
        node.update(layoutImpl, currentTransitionStates, content, key)
    }
}

internal class ElementNode(
    private var layoutImpl: SceneTransitionLayoutImpl,
    private var currentTransitions: List<TransitionState.Transition>,
    private var currentTransitionStates: List<TransitionState>,
    private var content: Content,
    private var key: ElementKey,
) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
@@ -226,12 +228,12 @@ internal class ElementNode(

    fun update(
        layoutImpl: SceneTransitionLayoutImpl,
        currentTransitions: List<TransitionState.Transition>,
        currentTransitionStates: List<TransitionState>,
        content: Content,
        key: ElementKey,
    ) {
        check(layoutImpl == this.layoutImpl && content == this.content)
        this.currentTransitions = currentTransitions
        this.currentTransitionStates = currentTransitionStates

        removeNodeFromContentState()

@@ -287,16 +289,45 @@ internal class ElementNode(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val transitions = currentTransitions
        val transition = elementTransition(layoutImpl, element, transitions)
        val elementState = elementState(layoutImpl, element, currentTransitionStates)
        if (elementState == null) {
            // If the element is not part of any transition, place it normally in its idle scene.
            val currentState = currentTransitionStates.last()
            val placeInThisContent =
                elementContentWhenIdle(
                    layoutImpl,
                    currentState.currentScene,
                    currentState.currentOverlays,
                    isInContent = { it in element.stateByContent },
                ) == content.key

            return if (placeInThisContent) {
                placeNormally(measurable, constraints)
            } else {
                doNotPlace(measurable, constraints)
            }
        }

        val transition = elementState as? TransitionState.Transition

        // If this element is not supposed to be laid out now, either because it is not part of any
        // ongoing transition or the other content of its transition is overscrolling, then lay out
        // the element normally and don't place it.
        // If this element is not supposed to be laid out now because the other content of its
        // transition is overscrolling, then lay out the element normally and don't place it.
        val overscrollScene = transition?.currentOverscrollSpec?.scene
        val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key
        val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null
        if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) {
        if (isOtherSceneOverscrolling) {
            return doNotPlace(measurable, constraints)
        }

        val placeable =
            measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
        stateInContent.lastSize = placeable.size()
        return layout(placeable.width, placeable.height) { place(elementState, placeable) }
    }

    private fun ApproachMeasureScope.doNotPlace(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        recursivelyClearPlacementValues()
        stateInContent.lastSize = Element.SizeUnspecified

@@ -304,14 +335,26 @@ internal class ElementNode(
        return layout(placeable.width, placeable.height) { /* Do not place */ }
    }

        val placeable =
            measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
    private fun ApproachMeasureScope.placeNormally(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val placeable = measurable.measure(constraints)
        stateInContent.lastSize = placeable.size()
        return layout(placeable.width, placeable.height) { place(transition, placeable) }
        return layout(placeable.width, placeable.height) {
            coordinates?.let {
                with(layoutImpl.lookaheadScope) {
                    stateInContent.lastOffset =
                        lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
                }
            }

            placeable.place(0, 0)
        }
    }

    private fun Placeable.PlacementScope.place(
        transition: TransitionState.Transition?,
        elementState: TransitionState,
        placeable: Placeable,
    ) {
        with(layoutImpl.lookaheadScope) {
@@ -321,11 +364,12 @@ internal class ElementNode(
                coordinates ?: error("Element ${element.key} does not have any coordinates")

            // No need to place the element in this content if we don't want to draw it anyways.
            if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
            if (!shouldPlaceElement(layoutImpl, content.key, element, elementState)) {
                recursivelyClearPlacementValues()
                return
            }

            val transition = elementState as? TransitionState.Transition
            val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
            val targetOffset =
                computeValue(
@@ -391,11 +435,15 @@ internal class ElementNode(
                        return@placeWithLayer
                    }

                    val transition = elementTransition(layoutImpl, element, currentTransitions)
                    if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
                    val elementState = elementState(layoutImpl, element, currentTransitionStates)
                    if (
                        elementState == null ||
                            !shouldPlaceElement(layoutImpl, content.key, element, elementState)
                    ) {
                        return@placeWithLayer
                    }

                    val transition = elementState as? TransitionState.Transition
                    alpha = elementAlpha(layoutImpl, element, transition, stateInContent)
                    compositingStrategy = CompositingStrategy.ModulateAlpha
                }
@@ -425,7 +473,9 @@ internal class ElementNode(
    override fun ContentDrawScope.draw() {
        element.wasDrawnInAnyContent = true

        val transition = elementTransition(layoutImpl, element, currentTransitions)
        val transition =
            elementState(layoutImpl, element, currentTransitionStates)
                as? TransitionState.Transition
        val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent)
        if (drawScale == Scale.Default) {
            drawContent()
@@ -468,21 +518,15 @@ internal class ElementNode(
    }
}

/**
 * The transition that we should consider for [element]. This is the last transition where one of
 * its contents contains the element.
 */
private fun elementTransition(
/** The [TransitionState] that we should consider for [element]. */
private fun elementState(
    layoutImpl: SceneTransitionLayoutImpl,
    element: Element,
    transitions: List<TransitionState.Transition>,
): TransitionState.Transition? {
    val transition =
        transitions.fastLastOrNull { transition ->
            transition.fromContent in element.stateByContent ||
                transition.toContent in element.stateByContent
        }
    transitionStates: List<TransitionState>,
): TransitionState? {
    val state = elementState(transitionStates, isInContent = { it in element.stateByContent })

    val transition = state as? TransitionState.Transition
    val previousTransition = element.lastTransition
    element.lastTransition = transition

@@ -497,8 +541,67 @@ private fun elementTransition(
        }
    }

    return state
}

internal inline fun elementState(
    transitionStates: List<TransitionState>,
    isInContent: (ContentKey) -> Boolean,
): TransitionState? {
    val lastState = transitionStates.last()
    if (lastState is TransitionState.Idle) {
        check(transitionStates.size == 1)
        return lastState
    }

    // Find the last transition with a content that contains the element.
    transitionStates.fastForEachReversed { state ->
        val transition = state as TransitionState.Transition
        if (isInContent(transition.fromContent) || isInContent(transition.toContent)) {
            return transition
        }
    }

    return null
}

internal inline fun elementContentWhenIdle(
    layoutImpl: SceneTransitionLayoutImpl,
    idle: TransitionState.Idle,
    isInContent: (ContentKey) -> Boolean,
): ContentKey {
    val currentScene = idle.currentScene
    val overlays = idle.currentOverlays
    return elementContentWhenIdle(layoutImpl, currentScene, overlays, isInContent)
}

private inline fun elementContentWhenIdle(
    layoutImpl: SceneTransitionLayoutImpl,
    currentScene: SceneKey,
    overlays: Set<OverlayKey>,
    isInContent: (ContentKey) -> Boolean,
): ContentKey {
    if (overlays.isEmpty()) {
        return currentScene
    }

    // Find the overlay with highest zIndex that contains the element.
    // TODO(b/353679003): Should we cache enabledOverlays into a List<> to avoid a lot of
    // allocations here?
    var currentOverlay: OverlayKey? = null
    for (overlay in overlays) {
        if (
            isInContent(overlay) &&
                (currentOverlay == null ||
                    (layoutImpl.overlay(overlay).zIndex >
                        layoutImpl.overlay(currentOverlay).zIndex))
        ) {
            currentOverlay = overlay
        }
    }

    return currentOverlay ?: currentScene
}

private fun prepareInterruption(
    layoutImpl: SceneTransitionLayoutImpl,
@@ -693,11 +796,19 @@ private fun shouldPlaceElement(
    layoutImpl: SceneTransitionLayoutImpl,
    content: ContentKey,
    element: Element,
    transition: TransitionState.Transition?,
    elementState: TransitionState,
): Boolean {
    // Always place the element if we are idle.
    if (transition == null) {
        return true
    val transition =
        when (elementState) {
            is TransitionState.Idle -> {
                return content ==
                    elementContentWhenIdle(
                        layoutImpl,
                        elementState,
                        isInContent = { it in element.stateByContent },
                    )
            }
            is TransitionState.Transition -> elementState
        }

    // Don't place the element in this content if this content is not part of the current element
@@ -741,16 +852,12 @@ internal fun shouldPlaceOrComposeSharedElement(

    val scenePicker = element.contentPicker
    val pickedScene =
        when (transition) {
            is TransitionState.Transition.ChangeCurrentScene -> {
        scenePicker.contentDuringTransition(
            element = element,
            transition = transition,
                    fromContentZIndex = layoutImpl.scene(transition.fromScene).zIndex,
                    toContentZIndex = layoutImpl.scene(transition.toScene).zIndex,
            fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex,
            toContentZIndex = layoutImpl.content(transition.toContent).zIndex,
        )
            }
        }

    return pickedScene == content
}
+12 −0
Original line number Diff line number Diff line
@@ -63,6 +63,18 @@ class SceneKey(
    }
}

/** Key for an overlay. */
class OverlayKey(
    debugName: String,
    identity: Any = Object(),
) : ContentKey(debugName, identity) {
    override val testTag: String = "overlay:$debugName"

    override fun toString(): String {
        return "OverlayKey(debugName=$debugName)"
    }
}

/** Key for an element. */
open class ElementKey(
    debugName: String,
Loading