Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedSceneTransitionLayoutState.kt 0 → 100644 +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) } } } packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +12 −1 Original line number Diff line number Diff line Loading @@ -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]. */ Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +35 −4 Original line number Diff line number Diff line Loading @@ -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 } Loading Loading @@ -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) Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +11 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +35 −10 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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 Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedSceneTransitionLayoutState.kt 0 → 100644 +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) } } }
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +12 −1 Original line number Diff line number Diff line Loading @@ -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]. */ Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +35 −4 Original line number Diff line number Diff line Loading @@ -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 } Loading Loading @@ -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) Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +11 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +35 −10 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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