diff options
| author | 2024-02-01 12:14:16 +0100 | |
|---|---|---|
| committer | 2024-02-06 15:35:00 +0100 | |
| commit | d25d0d8dbea0acbafe377dcb3b25b5ce556418f9 (patch) | |
| tree | 6a0d540377353fce07aff8048e44e8bf9f33bb6f | |
| parent | aed78ff2b00eb08a40295d73f69fcd049a82aa62 (diff) | |
Add TransitionLink feature
Transitions can now be linked. E.g. when one STL transitions A->B then
the linked STL should automatically play C->D. The configuration can
be passed to SceneTransitionLayoutState via `transitionLinks`.
Test: SceneTransitionLayoutStateTest
Bug: b/320257219
Flag: NONE
Change-Id: I6192ddf489f1d2fd895b20af3b4428aa6e18fa32
4 files changed, 369 insertions, 51 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 61d9bceef33b..2661301fcb83 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -24,6 +24,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.util.fastFilter +import androidx.compose.ui.util.fastForEach +import com.android.compose.animation.scene.transition.link.LinkedTransition +import com.android.compose.animation.scene.transition.link.StateLink import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -101,8 +105,9 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState fun MutableSceneTransitionLayoutState( initialScene: SceneKey, transitions: SceneTransitions = SceneTransitions.Empty, + stateLinks: List<StateLink> = emptyList(), ): MutableSceneTransitionLayoutState { - return MutableSceneTransitionLayoutStateImpl(initialScene, transitions) + return MutableSceneTransitionLayoutStateImpl(initialScene, transitions, stateLinks) } /** @@ -121,9 +126,12 @@ fun updateSceneTransitionLayoutState( currentScene: SceneKey, onChangeScene: (SceneKey) -> Unit, transitions: SceneTransitions = SceneTransitions.Empty, + stateLinks: List<StateLink> = emptyList(), ): SceneTransitionLayoutState { - return remember { HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene) } - .apply { update(currentScene, onChangeScene, transitions) } + return remember { + HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene, stateLinks) + } + .apply { update(currentScene, onChangeScene, transitions, stateLinks) } } @Stable @@ -184,8 +192,10 @@ sealed interface TransitionState { } } -internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) : - SceneTransitionLayoutState { +internal abstract class BaseSceneTransitionLayoutState( + initialScene: SceneKey, + protected var stateLinks: List<StateLink>, +) : SceneTransitionLayoutState { override var transitionState: TransitionState by mutableStateOf(TransitionState.Idle(initialScene)) protected set @@ -196,6 +206,8 @@ internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) : */ internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty + private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>() + /** * Called when the [current scene][TransitionState.currentScene] should be changed to [scene]. * @@ -224,20 +236,68 @@ internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) : transitions .transitionSpec(transition.fromScene, transition.toScene, key = transitionKey) .transformationSpec() - + cancelActiveTransitionLinks() + setupTransitionLinks(transition) transitionState = transition } + private fun cancelActiveTransitionLinks() { + for ((link, linkedTransition) in activeTransitionLinks) { + link.target.finishTransition(linkedTransition, linkedTransition.currentScene) + } + activeTransitionLinks.clear() + } + + private fun setupTransitionLinks(transitionState: TransitionState) { + if (transitionState !is TransitionState.Transition) return + stateLinks.fastForEach { stateLink -> + val matchingLink = + stateLink.transitionLinks.firstOrNull() { it.isMatchingLink(transitionState) } ?: return@fastForEach + + val targetCurrentScene = stateLink.target.transitionState.currentScene + + if (targetCurrentScene != matchingLink.targetFrom) return@fastForEach + + val linkedTransition = + LinkedTransition( + originalTransition = transitionState, + fromScene = targetCurrentScene, + toScene = matchingLink.targetTo, + ) + + stateLink.target.startTransition(linkedTransition, matchingLink.targetTransitionKey) + activeTransitionLinks[stateLink] = linkedTransition + } + } + /** * Notify that [transition] was finished and that we should settle to [idleScene]. This will do * nothing if [transition] was interrupted since it was started. */ internal fun finishTransition(transition: TransitionState.Transition, idleScene: SceneKey) { + resolveActiveTransitionLinks(idleScene) if (transitionState == transition) { transitionState = TransitionState.Idle(idleScene) } } + private fun resolveActiveTransitionLinks(idleScene: SceneKey) { + val previousTransition = this.transitionState as? TransitionState.Transition ?: return + for ((link, linkedTransition) in activeTransitionLinks) { + if (previousTransition.fromScene == idleScene) { + // The transition ended by arriving at the fromScene, move link to Idle(fromScene). + link.target.finishTransition(linkedTransition, linkedTransition.fromScene) + } else if (previousTransition.toScene == idleScene) { + // The transition ended by arriving at the toScene, move link to Idle(toScene). + link.target.finishTransition(linkedTransition, linkedTransition.toScene) + } else { + // The transition was interrupted by something else, we reset to initial state. + link.target.finishTransition(linkedTransition, linkedTransition.fromScene) + } + } + activeTransitionLinks.clear() + } + /** * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap * to the closest scene. @@ -271,7 +331,8 @@ internal class HoistedSceneTransitionLayoutScene( initialScene: SceneKey, override var transitions: SceneTransitions, private var changeScene: (SceneKey) -> Unit, -) : BaseSceneTransitionLayoutState(initialScene) { + stateLinks: List<StateLink> = emptyList(), +) : BaseSceneTransitionLayoutState(initialScene, stateLinks) { private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED) override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene(scene) @@ -281,10 +342,12 @@ internal class HoistedSceneTransitionLayoutScene( currentScene: SceneKey, onChangeScene: (SceneKey) -> Unit, transitions: SceneTransitions, + stateLinks: List<StateLink>, ) { SideEffect { this.changeScene = onChangeScene this.transitions = transitions + this.stateLinks = stateLinks targetSceneChannel.trySend(currentScene) } @@ -308,7 +371,8 @@ internal class HoistedSceneTransitionLayoutScene( internal class MutableSceneTransitionLayoutStateImpl( initialScene: SceneKey, override var transitions: SceneTransitions, -) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene) { + stateLinks: List<StateLink> = emptyList(), +) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene, stateLinks) { override fun setTargetScene( targetScene: SceneKey, coroutineScope: CoroutineScope, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt new file mode 100644 index 000000000000..33b57b25fd10 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 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.transition.link + +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.TransitionState + +/** A linked transition which is driven by a [originalTransition]. */ +internal class LinkedTransition( + private val originalTransition: TransitionState.Transition, + fromScene: SceneKey, + toScene: SceneKey, +) : TransitionState.Transition(fromScene, toScene) { + + override val currentScene: SceneKey + get() { + return when (originalTransition.currentScene) { + originalTransition.fromScene -> fromScene + originalTransition.toScene -> toScene + else -> error("Original currentScene is neither FromScene nor ToScene") + } + } + + override val isInitiatedByUserInput: Boolean + get() = originalTransition.isInitiatedByUserInput + + override val isUserInputOngoing: Boolean + get() = originalTransition.isUserInputOngoing + + override val progress: Float + get() = originalTransition.progress +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt new file mode 100644 index 000000000000..9b51e447b58b --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 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.transition.link + +import com.android.compose.animation.scene.BaseSceneTransitionLayoutState +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayoutState +import com.android.compose.animation.scene.TransitionKey +import com.android.compose.animation.scene.TransitionState + +/** A link between a source (implicit) and [target] `SceneTransitionLayoutState`. */ +class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<TransitionLink>) { + + internal val target = target as BaseSceneTransitionLayoutState + + /** + * Links two transitions (source and target) together. + * + * `null` can be passed to indicate that any SceneKey should match. e.g. passing `null`, `null`, + * `null`, `SceneA` means that any transition at the source will trigger a transition in the + * target to `SceneA` from any current scene. + */ + class TransitionLink( + val sourceFrom: SceneKey, + val sourceTo: SceneKey, + val targetFrom: SceneKey, + val targetTo: SceneKey, + val targetTransitionKey: TransitionKey? = null, + ) { + init { + if ( + (sourceFrom != null && sourceFrom == sourceTo) || + (targetFrom != null && targetFrom == targetTo) + ) + error("From and To can't be the same") + } + + internal fun isMatchingLink(transition: TransitionState.Transition): Boolean { + return (sourceFrom == transition.fromScene) && + (sourceTo == transition.toScene) + } + + internal fun targetIsInValidState(targetCurrentScene: SceneKey): Boolean { + return (targetFrom == targetCurrentScene) && + targetTo != targetCurrentScene + } + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 302fc0b08ab0..2a5a355f67fd 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -18,10 +18,14 @@ package com.android.compose.animation.scene import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.animation.scene.TestScenes.SceneC +import com.android.compose.animation.scene.TestScenes.SceneD +import com.android.compose.animation.scene.transition.link.StateLink import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test @@ -31,93 +35,235 @@ import org.junit.runner.RunWith class SceneTransitionLayoutStateTest { @get:Rule val rule = createComposeRule() + class TestableTransition( + fromScene: SceneKey, + toScene: SceneKey, + ) : TransitionState.Transition(fromScene, toScene) { + override var currentScene: SceneKey = fromScene + override var progress: Float = 0.0f + override var isInitiatedByUserInput: Boolean = false + override var isUserInputOngoing: Boolean = false + } + @Test fun isTransitioningTo_idle() { - val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty) + val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) assertThat(state.isTransitioning()).isFalse() - assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse() - assertThat(state.isTransitioning(to = TestScenes.SceneB)).isFalse() - assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)) - .isFalse() + assertThat(state.isTransitioning(from = SceneA)).isFalse() + assertThat(state.isTransitioning(to = SceneB)).isFalse() + assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isFalse() } @Test fun isTransitioningTo_transition() { - val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty) - state.startTransition( - transition(from = TestScenes.SceneA, to = TestScenes.SceneB), - transitionKey = null - ) + val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) + state.startTransition(transition(from = SceneA, to = SceneB), transitionKey = null) assertThat(state.isTransitioning()).isTrue() - assertThat(state.isTransitioning(from = TestScenes.SceneA)).isTrue() - assertThat(state.isTransitioning(from = TestScenes.SceneB)).isFalse() - assertThat(state.isTransitioning(to = TestScenes.SceneB)).isTrue() - assertThat(state.isTransitioning(to = TestScenes.SceneA)).isFalse() - assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue() + assertThat(state.isTransitioning(from = SceneA)).isTrue() + assertThat(state.isTransitioning(from = SceneB)).isFalse() + assertThat(state.isTransitioning(to = SceneB)).isTrue() + assertThat(state.isTransitioning(to = SceneA)).isFalse() + assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue() } @Test fun setTargetScene_idleToSameScene() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) - assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull() + val state = MutableSceneTransitionLayoutState(SceneA) + assertThat(state.setTargetScene(SceneA, coroutineScope = this)).isNull() } @Test fun setTargetScene_idleToDifferentScene() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) - val transition = state.setTargetScene(TestScenes.SceneB, coroutineScope = this) + val state = MutableSceneTransitionLayoutState(SceneA) + val transition = state.setTargetScene(SceneB, coroutineScope = this) assertThat(transition).isNotNull() assertThat(state.transitionState).isEqualTo(transition) testScheduler.advanceUntilIdle() - assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB)) + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) } @Test fun setTargetScene_transitionToSameScene() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) - assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull() - assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNull() + val state = MutableSceneTransitionLayoutState(SceneA) + assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() + assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNull() testScheduler.advanceUntilIdle() - assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB)) + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) } @Test fun setTargetScene_transitionToDifferentScene() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) - assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull() - assertThat(state.setTargetScene(TestScenes.SceneC, coroutineScope = this)).isNotNull() + val state = MutableSceneTransitionLayoutState(SceneA) + assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() + assertThat(state.setTargetScene(SceneC, coroutineScope = this)).isNotNull() testScheduler.advanceUntilIdle() - assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneC)) + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneC)) } @Test fun setTargetScene_transitionToOriginalScene() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) - assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull() + val state = MutableSceneTransitionLayoutState(SceneA) + assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() // Progress is 0f, so we don't animate at all and directly snap back to A. - assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull() - assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA)) + assertThat(state.setTargetScene(SceneA, coroutineScope = this)).isNull() + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneA)) } @Test fun setTargetScene_coroutineScopeCancelled() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) + val state = MutableSceneTransitionLayoutState(SceneA) lateinit var transition: TransitionState.Transition val job = launch(start = CoroutineStart.UNDISPATCHED) { - transition = state.setTargetScene(TestScenes.SceneB, coroutineScope = this)!! + transition = state.setTargetScene(SceneB, coroutineScope = this)!! } assertThat(state.transitionState).isEqualTo(transition) // Cancelling the scope/job still sets the state to Idle(targetScene). job.cancel() testScheduler.advanceUntilIdle() - assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB)) + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) + } + + private fun setupLinkedStates(): + Pair<BaseSceneTransitionLayoutState, BaseSceneTransitionLayoutState> { + val parentState = MutableSceneTransitionLayoutState(SceneC) + val link = + listOf( + StateLink( + parentState, + listOf(StateLink.TransitionLink(SceneA, SceneB, SceneC, SceneD)) + ) + ) + val childState = MutableSceneTransitionLayoutState(SceneA, stateLinks = link) + return Pair( + parentState as BaseSceneTransitionLayoutState, + childState as BaseSceneTransitionLayoutState + ) + } + + @Test + fun linkedTransition_startsLinkAndFinishesLinkInToState() { + val (parentState, childState) = setupLinkedStates() + + val childTransition = TestableTransition(SceneA, SceneB) + + childState.startTransition(childTransition, null) + assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue() + assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue() + + childState.finishTransition(childTransition, SceneB) + assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) + assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD)) + } + + @Test + fun linkedTransition_transitiveLink() { + val parentParentState = + MutableSceneTransitionLayoutState(SceneB) as BaseSceneTransitionLayoutState + val parentLink = + listOf( + StateLink( + parentParentState, + listOf(StateLink.TransitionLink(SceneC, SceneD, SceneB, SceneC)) + ) + ) + val parentState = + MutableSceneTransitionLayoutState(SceneC, stateLinks = parentLink) + as BaseSceneTransitionLayoutState + val link = + listOf( + StateLink( + parentState, + listOf(StateLink.TransitionLink(SceneA, SceneB, SceneC, SceneD)) + ) + ) + val childState = + MutableSceneTransitionLayoutState(SceneA, stateLinks = link) + as BaseSceneTransitionLayoutState + + val childTransition = TestableTransition(SceneA, SceneB) + + childState.startTransition(childTransition, null) + assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue() + assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue() + assertThat(parentParentState.isTransitioning(SceneB, SceneC)).isTrue() + + childState.finishTransition(childTransition, SceneB) + assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) + assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD)) + assertThat(parentParentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) + } + + @Test + fun linkedTransition_linkProgressIsEqual() { + val (parentState, childState) = setupLinkedStates() + + val childTransition = TestableTransition(SceneA, SceneB) + + childState.startTransition(childTransition, null) + assertThat(parentState.currentTransition?.progress).isEqualTo(0f) + + childTransition.progress = .5f + assertThat(parentState.currentTransition?.progress).isEqualTo(.5f) + } + + @Test + fun linkedTransition_reverseTransitionIsNotLinked() { + val (parentState, childState) = setupLinkedStates() + + val childTransition = TestableTransition(SceneB, SceneA) + + childState.startTransition(childTransition, null) + assertThat(childState.isTransitioning(SceneB, SceneA)).isTrue() + assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) + + childState.finishTransition(childTransition, SceneB) + assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) + assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) + } + + @Test + fun linkedTransition_startsLinkAndFinishesLinkInFromState() { + val (parentState, childState) = setupLinkedStates() + + val childTransition = TestableTransition(SceneA, SceneB) + childState.startTransition(childTransition, null) + + childState.finishTransition(childTransition, SceneA) + assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA)) + assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) + } + + @Test + fun linkedTransition_startsLinkAndFinishesLinkInUnknownState() { + val (parentState, childState) = setupLinkedStates() + + val childTransition = TestableTransition(SceneA, SceneB) + childState.startTransition(childTransition, null) + + childState.finishTransition(childTransition, SceneD) + assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneD)) + assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) + } + + @Test + fun linkedTransition_startsLinkButLinkedStateIsTakenOver() { + val (parentState, childState) = setupLinkedStates() + + val childTransition = TestableTransition(SceneA, SceneB) + val parentTransition = TestableTransition(SceneC, SceneA) + childState.startTransition(childTransition, null) + parentState.startTransition(parentTransition, null) + + childState.finishTransition(childTransition, SceneB) + assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) + assertThat(parentState.transitionState).isEqualTo(parentTransition) } @Test @@ -125,11 +271,11 @@ class SceneTransitionLayoutStateTest { val transitionkey = TransitionKey(debugName = "foo") val state = MutableSceneTransitionLayoutState( - TestScenes.SceneA, + SceneA, transitions = transitions { - from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) } - from(TestScenes.SceneA, to = TestScenes.SceneB, key = transitionkey) { + from(SceneA, to = SceneB) { fade(TestElements.Foo) } + from(SceneA, to = SceneB, key = transitionkey) { fade(TestElements.Foo) fade(TestElements.Bar) } @@ -138,19 +284,19 @@ class SceneTransitionLayoutStateTest { as MutableSceneTransitionLayoutStateImpl // Default transition from A to B. - assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull() + assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() assertThat(state.transformationSpec.transformations).hasSize(1) // Go back to A. - state.setTargetScene(TestScenes.SceneA, coroutineScope = this) + state.setTargetScene(SceneA, coroutineScope = this) testScheduler.advanceUntilIdle() assertThat(state.currentTransition).isNull() - assertThat(state.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + assertThat(state.transitionState.currentScene).isEqualTo(SceneA) // Specific transition from A to B. assertThat( state.setTargetScene( - TestScenes.SceneB, + SceneB, coroutineScope = this, transitionKey = transitionkey, ) |