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

Commit 58cc2587 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Remove linked transitions

This CL removes linked transitions driven by state links. This is a
feature that we thought would be useful for animating notifications or
media player at the same time as transitioning the shade, but it's still
not used and might never be used. Moreover, we now exposed new APIs to
create custom transitions, so it's actually quite easy for our users to
already do the same kind of delegated transitions between different STLs
if necessary.

In the worse case, if we realize that we actually need this feature we
will revert this CL.

Bug: 376438969
Test: Pure code deletion
Flag: EXEMPT code deletion
Change-Id: I3d5a16fd1d50ef593d4d45fef0539d613b35c0b1
parent 2dd8bf53
Loading
Loading
Loading
Loading
+3 −67
Original line number Original line Diff line number Diff line
@@ -25,17 +25,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEach
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transition.link.LinkedTransition
import com.android.compose.animation.scene.transition.link.StateLink
import kotlin.math.absoluteValue
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch


/**
/**
@@ -236,7 +232,6 @@ fun MutableSceneTransitionLayoutState(
    canShowOverlay: (OverlayKey) -> Boolean = { true },
    canShowOverlay: (OverlayKey) -> Boolean = { true },
    canHideOverlay: (OverlayKey) -> Boolean = { true },
    canHideOverlay: (OverlayKey) -> Boolean = { true },
    canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
    canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
    stateLinks: List<StateLink> = emptyList(),
): MutableSceneTransitionLayoutState {
): MutableSceneTransitionLayoutState {
    return MutableSceneTransitionLayoutStateImpl(
    return MutableSceneTransitionLayoutStateImpl(
        initialScene,
        initialScene,
@@ -246,7 +241,6 @@ fun MutableSceneTransitionLayoutState(
        canShowOverlay,
        canShowOverlay,
        canHideOverlay,
        canHideOverlay,
        canReplaceOverlay,
        canReplaceOverlay,
        stateLinks,
    )
    )
}
}


@@ -258,10 +252,7 @@ internal class MutableSceneTransitionLayoutStateImpl(
    internal val canChangeScene: (SceneKey) -> Boolean = { true },
    internal val canChangeScene: (SceneKey) -> Boolean = { true },
    internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
    internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
    internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
    internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
    internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
    internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
        true
    },
    private val stateLinks: List<StateLink> = emptyList(),
) : MutableSceneTransitionLayoutState {
) : MutableSceneTransitionLayoutState {
    private val creationThread: Thread = Thread.currentThread()
    private val creationThread: Thread = Thread.currentThread()


@@ -364,20 +355,11 @@ internal class MutableSceneTransitionLayoutStateImpl(
        checkThread()
        checkThread()


        try {
        try {
            // Keep a reference to the previous transition (if any).
            val previousTransition = currentTransition

            // Start the transition.
            // Start the transition.
            startTransitionInternal(transition, chain)
            startTransitionInternal(transition, chain)


            // Handle transition links.
            previousTransition?.let { cancelActiveTransitionLinks(it) }
            coroutineScope {
                setupTransitionLinks(transition)

            // Run the transition until it is finished.
            // Run the transition until it is finished.
            transition.runInternal()
            transition.runInternal()
            }
        } finally {
        } finally {
            finishTransition(transition)
            finishTransition(transition)
        }
        }
@@ -471,42 +453,6 @@ internal class MutableSceneTransitionLayoutStateImpl(
        )
        )
    }
    }


    private fun cancelActiveTransitionLinks(transition: TransitionState.Transition) {
        transition.activeTransitionLinks.forEach { (link, linkedTransition) ->
            link.target.finishTransition(linkedTransition)
        }
        transition.activeTransitionLinks.clear()
    }

    private fun CoroutineScope.setupTransitionLinks(transition: TransitionState.Transition) {
        stateLinks.fastForEach { stateLink ->
            val matchingLinks =
                stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) }
            if (matchingLinks.isEmpty()) return@fastForEach
            if (matchingLinks.size > 1) error("More than one link matched.")

            val targetCurrentScene = stateLink.target.transitionState.currentScene
            val matchingLink = matchingLinks[0]

            if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach

            val linkedTransition =
                LinkedTransition(
                    originalTransition = transition,
                    fromScene = targetCurrentScene,
                    toScene = matchingLink.targetTo,
                    key = matchingLink.targetTransitionKey,
                )

            // Start with UNDISPATCHED so that startTransition is called directly and the new linked
            // transition is observable directly.
            launch(start = CoroutineStart.UNDISPATCHED) {
                stateLink.target.startTransition(linkedTransition)
            }
            transition.activeTransitionLinks[stateLink] = linkedTransition
        }
    }

    /**
    /**
     * Notify that [transition] was finished and that it settled to its
     * Notify that [transition] was finished and that it settled to its
     * [currentScene][TransitionState.currentScene]. This will do nothing if [transition] was
     * [currentScene][TransitionState.currentScene]. This will do nothing if [transition] was
@@ -535,9 +481,6 @@ internal class MutableSceneTransitionLayoutStateImpl(
        // Mark this transition as finished.
        // Mark this transition as finished.
        finishedTransitions.add(transition)
        finishedTransitions.add(transition)


        // Finish all linked transitions.
        finishActiveTransitionLinks(transition)

        // Keep a reference to the last transition, in case we remove all transitions and should
        // Keep a reference to the last transition, in case we remove all transitions and should
        // settle to Idle.
        // settle to Idle.
        val lastTransition = transitionStates.last()
        val lastTransition = transitionStates.last()
@@ -584,13 +527,6 @@ internal class MutableSceneTransitionLayoutStateImpl(
        transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
        transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
    }
    }


    private fun finishActiveTransitionLinks(transition: TransitionState.Transition) {
        for ((link, linkedTransition) in transition.activeTransitionLinks) {
            link.target.finishTransition(linkedTransition)
        }
        transition.activeTransitionLinks.clear()
    }

    /**
    /**
     * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap
     * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap
     * to the closest scene.
     * to the closest scene.
+1 −17
Original line number Original line Diff line number Diff line
@@ -34,9 +34,6 @@ import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpecImpl
import com.android.compose.animation.scene.TransformationSpecImpl
import com.android.compose.animation.scene.TransitionKey
import com.android.compose.animation.scene.TransitionKey
import com.android.compose.animation.scene.transition.link.LinkedTransition
import com.android.compose.animation.scene.transition.link.StateLink
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch


/** The state associated to a [SceneTransitionLayout] at some specific point in time. */
/** The state associated to a [SceneTransitionLayout] at some specific point in time. */
@@ -281,15 +278,9 @@ sealed interface TransitionState {
         */
         */
        private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
        private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null


        /** The map of active links that connects this transition to other transitions. */
        internal val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()

        /** Whether this transition was already started. */
        /** Whether this transition was already started. */
        private var wasStarted = false
        private var wasStarted = false


        /** A completable to [await] this transition. */
        private val completable = CompletableDeferred<Unit>()

        init {
        init {
            check(fromContent != toContent)
            check(fromContent != toContent)
            check(
            check(
@@ -348,18 +339,11 @@ sealed interface TransitionState {
         */
         */
        abstract fun freezeAndAnimateToCurrentState()
        abstract fun freezeAndAnimateToCurrentState()


        /** Wait for this transition to finish. */
        internal suspend fun await() = completable.await()

        internal suspend fun runInternal() {
        internal suspend fun runInternal() {
            check(!wasStarted) { "A Transition can be started only once." }
            check(!wasStarted) { "A Transition can be started only once." }
            wasStarted = true
            wasStarted = true


            try {
            run()
            run()
            } finally {
                completable.complete(Unit)
            }
        }
        }


        internal fun updateOverscrollSpecs(
        internal fun updateOverscrollSpecs(
+0 −59
Original line number Original line Diff line number Diff line
/*
 * 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.TransitionKey
import com.android.compose.animation.scene.content.state.TransitionState

/** A linked transition which is driven by a [originalTransition]. */
internal class LinkedTransition(
    private val originalTransition: TransitionState.Transition,
    fromScene: SceneKey,
    toScene: SceneKey,
    override val key: TransitionKey? = null,
) : TransitionState.Transition.ChangeScene(fromScene, toScene) {

    override val currentScene: SceneKey
        get() {
            return when (originalTransition.currentScene) {
                originalTransition.fromContent -> fromScene
                originalTransition.toContent -> 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

    override val progressVelocity: Float
        get() = originalTransition.progressVelocity

    override suspend fun run() {
        originalTransition.await()
    }

    override fun freezeAndAnimateToCurrentState() {
        originalTransition.freezeAndAnimateToCurrentState()
    }
}
+0 −63
Original line number Original line Diff line number Diff line
/*
 * 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.ContentKey
import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl
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.content.state.TransitionState

/** A link between a source (implicit) and [target] `SceneTransitionLayoutState`. */
class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<TransitionLink>) {

    internal val target = target as MutableSceneTransitionLayoutStateImpl

    /**
     * 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: ContentKey?,
        val sourceTo: ContentKey?,
        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 == null || sourceFrom == transition.fromContent) &&
                (sourceTo == null || sourceTo == transition.toContent)
        }

        internal fun targetIsInValidState(targetCurrentContent: ContentKey): Boolean {
            return (targetFrom == null || targetFrom == targetCurrentContent) &&
                targetTo != targetCurrentContent
        }
    }
}
+0 −188
Original line number Original line Diff line number Diff line
@@ -26,10 +26,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.TestScenes.SceneD
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.animation.scene.transition.link.StateLink
import com.android.compose.animation.scene.transition.seekToScene
import com.android.compose.animation.scene.transition.seekToScene
import com.android.compose.test.MonotonicClockTestScope
import com.android.compose.test.MonotonicClockTestScope
import com.android.compose.test.TestSceneTransition
import com.android.compose.test.TestSceneTransition
@@ -133,147 +131,6 @@ class SceneTransitionLayoutStateTest {
        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
    }
    }


    private fun setupLinkedStates(
        parentInitialScene: SceneKey = SceneC,
        childInitialScene: SceneKey = SceneA,
        sourceFrom: SceneKey? = SceneA,
        sourceTo: SceneKey? = SceneB,
        targetFrom: SceneKey? = SceneC,
        targetTo: SceneKey = SceneD,
    ): Pair<MutableSceneTransitionLayoutStateImpl, MutableSceneTransitionLayoutStateImpl> {
        val parentState = MutableSceneTransitionLayoutState(parentInitialScene)
        val link =
            listOf(
                StateLink(
                    parentState,
                    listOf(StateLink.TransitionLink(sourceFrom, sourceTo, targetFrom, targetTo)),
                )
            )
        val childState = MutableSceneTransitionLayoutState(childInitialScene, stateLinks = link)
        return Pair(
            parentState as MutableSceneTransitionLayoutStateImpl,
            childState as MutableSceneTransitionLayoutStateImpl,
        )
    }

    @Test
    fun linkedTransition_startsLinkAndFinishesLinkInToState() = runTest {
        val (parentState, childState) = setupLinkedStates()

        val childTransition = transition(SceneA, SceneB)

        val job =
            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()

        childTransition.finish()
        job.join()
        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
    }

    @Test
    fun linkedTransition_transitiveLink() = runTest {
        val parentParentState =
            MutableSceneTransitionLayoutState(SceneB) as MutableSceneTransitionLayoutStateImpl
        val parentLink =
            listOf(
                StateLink(
                    parentParentState,
                    listOf(StateLink.TransitionLink(SceneC, SceneD, SceneB, SceneC)),
                )
            )
        val parentState =
            MutableSceneTransitionLayoutState(SceneC, stateLinks = parentLink)
                as MutableSceneTransitionLayoutStateImpl
        val link =
            listOf(
                StateLink(
                    parentState,
                    listOf(StateLink.TransitionLink(SceneA, SceneB, SceneC, SceneD)),
                )
            )
        val childState =
            MutableSceneTransitionLayoutState(SceneA, stateLinks = link)
                as MutableSceneTransitionLayoutStateImpl

        val childTransition = transition(SceneA, SceneB)

        val job =
            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
        assertThat(parentParentState.isTransitioning(SceneB, SceneC)).isTrue()

        childTransition.finish()
        job.join()
        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() = runTest {
        val (parentState, childState) = setupLinkedStates()

        var progress = 0f
        val childTransition = transition(SceneA, SceneB, progress = { progress })

        childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        assertThat(parentState.currentTransition?.progress).isEqualTo(0f)

        progress = .5f
        assertThat(parentState.currentTransition?.progress).isEqualTo(.5f)
    }

    @Test
    fun linkedTransition_reverseTransitionIsNotLinked() = runTest {
        val (parentState, childState) = setupLinkedStates()

        val childTransition = transition(SceneB, SceneA, current = { SceneB })

        val job =
            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        assertThat(childState.isTransitioning(SceneB, SceneA)).isTrue()
        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))

        childTransition.finish()
        job.join()
        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
    }

    @Test
    fun linkedTransition_startsLinkAndFinishesLinkInFromState() = runTest {
        val (parentState, childState) = setupLinkedStates()

        val childTransition = transition(SceneA, SceneB, current = { SceneA })
        val job =
            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)

        childTransition.finish()
        job.join()
        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
    }

    @Test
    fun linkedTransition_startsLinkButLinkedStateIsTakenOver() = runTest {
        val (parentState, childState) = setupLinkedStates()

        val childTransition = transition(SceneA, SceneB)
        val parentTransition = transition(SceneC, SceneA)
        val job =
            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        parentState.startTransitionImmediately(animationScope = backgroundScope, parentTransition)

        childTransition.finish()
        job.join()
        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
        assertThat(parentState.transitionState).isEqualTo(parentTransition)
    }

    @Test
    @Test
    fun setTargetScene_withTransitionKey() = runMonotonicClockTest {
    fun setTargetScene_withTransitionKey() = runMonotonicClockTest {
        val transitionkey = TransitionKey(debugName = "foo")
        val transitionkey = TransitionKey(debugName = "foo")
@@ -394,51 +251,6 @@ class SceneTransitionLayoutStateTest {
        assertThat(state.isTransitioning()).isTrue()
        assertThat(state.isTransitioning()).isTrue()
    }
    }


    @Test
    fun linkedTransition_fuzzyLinksAreMatchedAndStarted() = runTest {
        val (parentState, childState) = setupLinkedStates(SceneC, SceneA, null, null, null, SceneD)
        val childTransition = transition(SceneA, SceneB)

        val job =
            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()

        childTransition.finish()
        job.join()
        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
    }

    @Test
    fun linkedTransition_fuzzyLinksAreMatchedAndResetToProperPreviousScene() = runTest {
        val (parentState, childState) =
            setupLinkedStates(SceneC, SceneA, SceneA, null, null, SceneD)

        val childTransition = transition(SceneA, SceneB, current = { SceneA })

        val job =
            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()

        childTransition.finish()
        job.join()
        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
    }

    @Test
    fun linkedTransition_fuzzyLinksAreNotMatched() = runTest {
        val (parentState, childState) =
            setupLinkedStates(SceneC, SceneA, SceneB, null, SceneC, SceneD)
        val childTransition = transition(SceneA, SceneB)

        childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
        assertThat(parentState.isTransitioning(SceneC, SceneD)).isFalse()
    }

    private fun MonotonicClockTestScope.startOverscrollableTransistionFromAtoB(
    private fun MonotonicClockTestScope.startOverscrollableTransistionFromAtoB(
        progress: () -> Float,
        progress: () -> Float,
        sceneTransitions: SceneTransitions,
        sceneTransitions: SceneTransitions,