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

Commit 88e55d16 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Improve ContentScope.layoutState for Nested STLs

This CL makes the transition state utilities in ContentScope.layoutState
(isTransitioning(Between|FromOrTo)) consider ancestor states too, so
that it is easier for STL users to introduce nested STLs without having
to change the checks in existing scenes.

Bug: 438951613
Test: NestedSceneTransitionLayoutStateTest
Flag: com.android.systemui.scene_container
Change-Id: I31093c10858d8ea9e091949a0a5a3e6e6e0b2aa8
parent 55403d3d
Loading
Loading
Loading
Loading
+63 −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.compose.animation.scene

import androidx.compose.ui.util.fastForEach

/**
 * An implementation of [SceneTransitionLayoutState] for nested STLs that takes ancestors into
 * account in its state check functions.
 */
internal class NestedSceneTransitionLayoutState(
    internal val ancestors: List<SceneTransitionLayoutState>,
    internal val delegate: SceneTransitionLayoutState,
) : SceneTransitionLayoutState by delegate {
    init {
        check(ancestors.isNotEmpty()) {
            "NestedSceneTransitionLayoutState should not be used for non nested STLs"
        }
    }

    override fun isIdle(content: ContentKey?): Boolean {
        var foundContent = content == null
        forEachState { state ->
            if (!state.isIdle()) return false
            foundContent = foundContent || state.isIdle(content)
        }
        return foundContent
    }

    override fun isTransitioning(from: ContentKey?, to: ContentKey?): Boolean {
        forEachState { state -> if (state.isTransitioning(from, to)) return true }
        return false
    }

    override fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
        forEachState { state -> if (state.isTransitioningBetween(content, other)) return true }
        return false
    }

    override fun isTransitioningFromOrTo(content: ContentKey): Boolean {
        forEachState { state -> if (state.isTransitioningFromOrTo(content)) return true }
        return false
    }

    private inline fun forEachState(action: (SceneTransitionLayoutState) -> Unit) {
        action(delegate)
        ancestors.fastForEach { action(it) }
    }
}
+12 −1
Original line number Diff line number Diff line
@@ -198,7 +198,18 @@ interface BaseContentScope : ElementStateScope {
    /** The key of this content. */
    val contentKey: ContentKey

    /** The state of the [SceneTransitionLayout] in which this content is contained. */
    /**
     * The state of the [SceneTransitionLayout] in which this content is contained.
     *
     * Important: Inside a [ContentScope.NestedSceneTransitionLayout], this will *not* be the state
     * passed to [ContentScope.NestedSceneTransitionLayout] but a new one that delegates to it
     * instead, so that checks on the current state also consider the ancestor STL states.
     *
     * @see SceneTransitionLayoutState.isIdle
     * @see SceneTransitionLayoutState.isTransitioning
     * @see SceneTransitionLayoutState.isTransitioningBetween
     * @see SceneTransitionLayoutState.isTransitioningFromOrTo
     */
    val layoutState: SceneTransitionLayoutState

    /** The [LookaheadScope] used by the [SceneTransitionLayout]. */
+35 −4
Original line number Diff line number Diff line
@@ -91,15 +91,42 @@ sealed interface SceneTransitionLayoutState {
    val transitions: SceneTransitions

    /**
     * Whether we are transitioning. If [from] or [to] is empty, we will also check that they match
     * the contents we are animating from and/or to.
     * Whether we are idle. If [content] isn't `null`, return `true` if idle and current content
     * contains [content]. If [content] is `null`, will return `true` if idle, regardless of current
     * content.
     *
     * If this is the state of a [ContentScope.NestedSceneTransitionLayout], then this will also
     * consider ancestors and return `true` iff *all* ancestor states are idle (and at least one of
     * them is idle at [content], if it is not null).
     */
    fun isIdle(content: ContentKey? = null): Boolean

    /**
     * Whether we are transitioning. If [from] or [to] are `null`, only the non-`null` one would be
     * checked; if both are `null`, will return `true` if any transition is ongoing.
     *
     * If this is the state of a [ContentScope.NestedSceneTransitionLayout], then this will also
     * consider ancestors and return `true` if *any* ancestor is transitioning (from [from] to [to],
     * if they are not null).
     */
    fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean

    /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
    /**
     * Whether we are transitioning from [content] to [other], or from [other] to [content].
     *
     * If this is the state of a [ContentScope.NestedSceneTransitionLayout], then this will also
     * consider ancestors and return `true` if *any* ancestor is transitioning (between [from] and
     * [to], if they are not null).
     */
    fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean

    /** Whether we are transitioning from or to [content]. */
    /**
     * Whether we are transitioning from or to [content].
     *
     * If this is the state of a [ContentScope.NestedSceneTransitionLayout], then this will also
     * consider ancestors and return `true` if *any* ancestor is transitioning (from or to
     * [content], if it is not null).
     */
    fun isTransitioningFromOrTo(content: ContentKey): Boolean
}

@@ -390,6 +417,10 @@ internal class MutableSceneTransitionLayoutStateImpl(
        }
    }

    override fun isIdle(content: ContentKey?): Boolean {
        return transitionState.isIdle(content)
    }

    override fun isTransitioning(from: ContentKey?, to: ContentKey?): Boolean {
        val transition = currentTransition ?: return false
        return transition.isTransitioning(from, to)
+11 −1
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
@@ -65,6 +66,7 @@ import com.android.compose.animation.scene.InternalContentScope
import com.android.compose.animation.scene.MovableElement
import com.android.compose.animation.scene.MovableElementContentScope
import com.android.compose.animation.scene.MovableElementKey
import com.android.compose.animation.scene.NestedSceneTransitionLayoutState
import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.SceneTransitionLayoutScope
@@ -295,7 +297,15 @@ internal class ContentScopeImpl(
    override val contentKey: ContentKey
        get() = content.key

    override val layoutState: SceneTransitionLayoutState = layoutImpl.state
    override val layoutState: SceneTransitionLayoutState =
        if (layoutImpl.ancestors.isEmpty()) {
            layoutImpl.state
        } else {
            NestedSceneTransitionLayoutState(
                ancestors = layoutImpl.ancestors.fastMap { it.layoutImpl.state },
                delegate = layoutImpl.state,
            )
        }

    override val lookaheadScope: LookaheadScope
        get() = layoutImpl.lookaheadScope
+35 −10
Original line number Diff line number Diff line
@@ -63,11 +63,40 @@ sealed interface TransitionState {
     */
    val currentOverlays: Set<OverlayKey>

    /**
     * Whether we are idle. If [content] isn't `null`, return `true` if idle and current content
     * contains [content]. If [content] is `null`, will return `true` if idle, regardless of current
     * content.
     */
    fun isIdle(content: ContentKey? = null): Boolean

    /**
     * Whether we are transitioning. If [from] or [to] are `null`, only the non-`null` one would be
     * checked; if both are `null`, will return `true` if any transition is ongoing.
     */
    fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean

    /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
    fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean

    /** Whether we are transitioning from or to [content]. */
    fun isTransitioningFromOrTo(content: ContentKey): Boolean

    /** The scene [currentScene] is idle. */
    data class Idle(
        override val currentScene: SceneKey,
        override val currentOverlays: Set<OverlayKey> = emptySet(),
    ) : TransitionState
    ) : TransitionState {
        override fun isIdle(content: ContentKey?): Boolean {
            return content == null || content == currentScene || currentOverlays.contains(content)
        }

        override fun isTransitioning(from: ContentKey?, to: ContentKey?): Boolean = false

        override fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean = false

        override fun isTransitioningFromOrTo(content: ContentKey): Boolean = false
    }

    sealed class Transition(
        val fromContent: ContentKey,
@@ -337,22 +366,18 @@ sealed interface TransitionState {
            }
        }

        /**
         * Whether we are transitioning. If [from] or [to] is empty, we will also check that they
         * match the contents we are animating from and/or to.
         */
        fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean {
        override fun isIdle(content: ContentKey?): Boolean = false

        override fun isTransitioning(from: ContentKey?, to: ContentKey?): Boolean {
            return (from == null || fromContent == from) && (to == null || toContent == to)
        }

        /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
        fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
        override fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
            return isTransitioning(from = content, to = other) ||
                isTransitioning(from = other, to = content)
        }

        /** Whether we are transitioning from or to [content]. */
        fun isTransitioningFromOrTo(content: ContentKey): Boolean {
        override fun isTransitioningFromOrTo(content: ContentKey): Boolean {
            return fromContent == content || toContent == content
        }

Loading