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

Commit 9edbfbf7 authored by Andreas Miko's avatar Andreas Miko Committed by Android (Google) Code Review
Browse files

Merge changes from topic "transitionlink" into main

* changes:
  Make TransitionLinks match more flexible
  Add TransitionLink feature
parents 323755c1 25506a4f
Loading
Loading
Loading
Loading
+75 −8
Original line number Original line Diff line number Diff line
@@ -24,6 +24,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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 kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel
@@ -101,8 +105,9 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState
fun MutableSceneTransitionLayoutState(
fun MutableSceneTransitionLayoutState(
    initialScene: SceneKey,
    initialScene: SceneKey,
    transitions: SceneTransitions = SceneTransitions.Empty,
    transitions: SceneTransitions = SceneTransitions.Empty,
    stateLinks: List<StateLink> = emptyList(),
): MutableSceneTransitionLayoutState {
): MutableSceneTransitionLayoutState {
    return MutableSceneTransitionLayoutStateImpl(initialScene, transitions)
    return MutableSceneTransitionLayoutStateImpl(initialScene, transitions, stateLinks)
}
}


/**
/**
@@ -121,9 +126,12 @@ fun updateSceneTransitionLayoutState(
    currentScene: SceneKey,
    currentScene: SceneKey,
    onChangeScene: (SceneKey) -> Unit,
    onChangeScene: (SceneKey) -> Unit,
    transitions: SceneTransitions = SceneTransitions.Empty,
    transitions: SceneTransitions = SceneTransitions.Empty,
    stateLinks: List<StateLink> = emptyList(),
): SceneTransitionLayoutState {
): SceneTransitionLayoutState {
    return remember { HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene) }
    return remember {
        .apply { update(currentScene, onChangeScene, transitions) }
            HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene, stateLinks)
        }
        .apply { update(currentScene, onChangeScene, transitions, stateLinks) }
}
}


@Stable
@Stable
@@ -184,8 +192,10 @@ sealed interface TransitionState {
    }
    }
}
}


internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) :
internal abstract class BaseSceneTransitionLayoutState(
    SceneTransitionLayoutState {
    initialScene: SceneKey,
    protected var stateLinks: List<StateLink>,
) : SceneTransitionLayoutState {
    override var transitionState: TransitionState by
    override var transitionState: TransitionState by
        mutableStateOf(TransitionState.Idle(initialScene))
        mutableStateOf(TransitionState.Idle(initialScene))
        protected set
        protected set
@@ -196,6 +206,8 @@ internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) :
     */
     */
    internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
    internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty


    private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()

    /**
    /**
     * Called when the [current scene][TransitionState.currentScene] should be changed to [scene].
     * Called when the [current scene][TransitionState.currentScene] should be changed to [scene].
     *
     *
@@ -224,20 +236,71 @@ internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) :
            transitions
            transitions
                .transitionSpec(transition.fromScene, transition.toScene, key = transitionKey)
                .transitionSpec(transition.fromScene, transition.toScene, key = transitionKey)
                .transformationSpec()
                .transformationSpec()

        cancelActiveTransitionLinks()
        setupTransitionLinks(transition)
        transitionState = 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 matchingLinks =
                stateLink.transitionLinks.fastFilter { it.isMatchingLink(transitionState) }
            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 = 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
     * Notify that [transition] was finished and that we should settle to [idleScene]. This will do
     * nothing if [transition] was interrupted since it was started.
     * nothing if [transition] was interrupted since it was started.
     */
     */
    internal fun finishTransition(transition: TransitionState.Transition, idleScene: SceneKey) {
    internal fun finishTransition(transition: TransitionState.Transition, idleScene: SceneKey) {
        resolveActiveTransitionLinks(idleScene)
        if (transitionState == transition) {
        if (transitionState == transition) {
            transitionState = TransitionState.Idle(idleScene)
            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
     * 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.
@@ -271,7 +334,8 @@ internal class HoistedSceneTransitionLayoutScene(
    initialScene: SceneKey,
    initialScene: SceneKey,
    override var transitions: SceneTransitions,
    override var transitions: SceneTransitions,
    private var changeScene: (SceneKey) -> Unit,
    private var changeScene: (SceneKey) -> Unit,
) : BaseSceneTransitionLayoutState(initialScene) {
    stateLinks: List<StateLink> = emptyList(),
) : BaseSceneTransitionLayoutState(initialScene, stateLinks) {
    private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)
    private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)


    override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene(scene)
    override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene(scene)
@@ -281,10 +345,12 @@ internal class HoistedSceneTransitionLayoutScene(
        currentScene: SceneKey,
        currentScene: SceneKey,
        onChangeScene: (SceneKey) -> Unit,
        onChangeScene: (SceneKey) -> Unit,
        transitions: SceneTransitions,
        transitions: SceneTransitions,
        stateLinks: List<StateLink>,
    ) {
    ) {
        SideEffect {
        SideEffect {
            this.changeScene = onChangeScene
            this.changeScene = onChangeScene
            this.transitions = transitions
            this.transitions = transitions
            this.stateLinks = stateLinks


            targetSceneChannel.trySend(currentScene)
            targetSceneChannel.trySend(currentScene)
        }
        }
@@ -308,7 +374,8 @@ internal class HoistedSceneTransitionLayoutScene(
internal class MutableSceneTransitionLayoutStateImpl(
internal class MutableSceneTransitionLayoutStateImpl(
    initialScene: SceneKey,
    initialScene: SceneKey,
    override var transitions: SceneTransitions,
    override var transitions: SceneTransitions,
) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene) {
    stateLinks: List<StateLink> = emptyList(),
) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene, stateLinks) {
    override fun setTargetScene(
    override fun setTargetScene(
        targetScene: SceneKey,
        targetScene: SceneKey,
        coroutineScope: CoroutineScope,
        coroutineScope: CoroutineScope,
+46 −0
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.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
}
+62 −0
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.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 == null || sourceFrom == transition.fromScene) &&
                (sourceTo == null || sourceTo == transition.toScene)
        }

        internal fun targetIsInValidState(targetCurrentScene: SceneKey): Boolean {
            return (targetFrom == null || targetFrom == targetCurrentScene) &&
                targetTo != targetCurrentScene
        }
    }
}
+236 −43

File changed.

Preview size limit exceeded, changes collapsed.