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

Commit 58613adc authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge changes from topic "stl-elevate-in-content" into main

* changes:
  Move Grids.kt back to PlatformComposeCore
  Introduce sharedElement.elevateInContent (1/2)
parents 7fb087af 3064191c
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -40,6 +40,8 @@ android_library {
    static_libs: [
        "androidx.compose.runtime_runtime",
        "androidx.compose.material3_material3",

        "PlatformComposeCore",
    ],

    kotlincflags: ["-Xjvm-default=all"],
+52 −1
Original line number Diff line number Diff line
@@ -48,10 +48,13 @@ import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.Element.State
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.modifiers.thenIf
import com.android.compose.ui.graphics.drawInContainer
import com.android.compose.ui.util.lerp
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@@ -146,10 +149,58 @@ internal fun Modifier.element(
    // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
    // we can ensure that SceneTransitionLayoutImpl will compose new contents first.
    val currentTransitionStates = layoutImpl.state.transitionStates
    return then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
    return thenIf(layoutImpl.state.isElevationPossible(content.key, key)) {
            Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
        }
        .then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
        .testTag(key.testTag)
}

private fun Modifier.maybeElevateInContent(
    layoutImpl: SceneTransitionLayoutImpl,
    content: Content,
    key: ElementKey,
    transitionStates: List<TransitionState>,
): Modifier {
    fun isSharedElement(
        stateByContent: Map<ContentKey, State>,
        transition: TransitionState.Transition,
    ): Boolean {
        fun inFromContent() = transition.fromContent in stateByContent
        fun inToContent() = transition.toContent in stateByContent
        fun inCurrentScene() = transition.currentScene in stateByContent

        return if (transition is TransitionState.Transition.ReplaceOverlay) {
            (inFromContent() && (inToContent() || inCurrentScene())) ||
                (inToContent() && inCurrentScene())
        } else {
            inFromContent() && inToContent()
        }
    }

    return drawInContainer(
        content.containerState,
        enabled = {
            val stateByContent = layoutImpl.elements.getValue(key).stateByContent
            val state = elementState(transitionStates, isInContent = { it in stateByContent })

            state is TransitionState.Transition &&
                state.transformationSpec
                    .transformations(key, content.key)
                    .shared
                    ?.elevateInContent == content.key &&
                isSharedElement(stateByContent, state) &&
                isSharedElementEnabled(key, state) &&
                shouldPlaceElement(
                    layoutImpl,
                    content.key,
                    layoutImpl.elements.getValue(key),
                    state,
                )
        },
    )
}

/**
 * An element associated to [ElementNode]. Note that this element does not support updates as its
 * arguments should always be the same.
+47 −0
Original line number Diff line number Diff line
@@ -19,13 +19,16 @@ package com.android.compose.animation.scene
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import com.android.compose.animation.scene.content.state.TransitionState
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
@@ -271,6 +274,14 @@ internal class MutableSceneTransitionLayoutStateImpl(
        mutableStateOf(listOf(TransitionState.Idle(initialScene, initialOverlays)))
        private set

    /**
     * The flattened list of [SharedElementTransformation] within all the transitions in
     * [transitionStates].
     */
    private val transformationsWithElevation: List<SharedElementTransformation> by derivedStateOf {
        transformationsWithElevation(transitionStates)
    }

    override val currentScene: SceneKey
        get() = transitionState.currentScene

@@ -743,6 +754,42 @@ internal class MutableSceneTransitionLayoutStateImpl(

        animate()
    }

    private fun transformationsWithElevation(
        transitionStates: List<TransitionState>
    ): List<SharedElementTransformation> {
        return buildList {
            transitionStates.fastForEach { state ->
                if (state !is TransitionState.Transition) {
                    return@fastForEach
                }

                state.transformationSpec.transformations.fastForEach { transformation ->
                    if (
                        transformation is SharedElementTransformation &&
                            transformation.elevateInContent != null
                    ) {
                        add(transformation)
                    }
                }
            }
        }
    }

    /**
     * Return whether we might need to elevate [element] (or any element if [element] is `null`) in
     * [content].
     *
     * This is used to compose `Modifier.container()` and `Modifier.drawInContainer()` only when
     * necessary, for performance.
     */
    internal fun isElevationPossible(content: ContentKey, element: ElementKey?): Boolean {
        if (transformationsWithElevation.isEmpty()) return false
        return transformationsWithElevation.fastAny { transformation ->
            transformation.elevateInContent == content &&
                (element == null || transformation.matcher.matches(element, content))
        }
    }
}

private const val TAG = "SceneTransitionLayoutState"
+10 −1
Original line number Diff line number Diff line
@@ -204,8 +204,17 @@ interface TransitionBuilder : BaseTransitionBuilder {
     *
     * @param enabled whether the matched element(s) should actually be shared in this transition.
     *   Defaults to true.
     * @param elevateInContent the content in which we should elevate the element when it is shared,
     *   drawing above all other composables of that content. If `null` (the default), we will
     *   simply draw this element in its original location. If not `null`, it has to be either the
     *   [fromContent][TransitionState.Transition.fromContent] or
     *   [toContent][TransitionState.Transition.toContent] of the transition.
     */
    fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true)
    fun sharedElement(
        matcher: ElementMatcher,
        enabled: Boolean = true,
        elevateInContent: ContentKey? = null,
    )

    /**
     * Adds the transformations in [builder] but in reversed order. This allows you to partially
Loading