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

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

Merge "Make ContentPicker work with NestedSharedElements" into main

parents b5ce517b 76ab8a30
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -41,8 +41,8 @@ object MediaContentPicker : StaticElementContentPicker {
    override fun contentDuringTransition(
        element: ElementKey,
        transition: TransitionState.Transition,
        fromContentZIndex: Float,
        toContentZIndex: Float,
        fromContentZIndex: Long,
        toContentZIndex: Long,
    ): ContentKey {
        return when {
            transition.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade) -> {
+23 −36
Original line number Diff line number Diff line
@@ -60,7 +60,6 @@ import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.TransformationWithRange
import com.android.compose.modifiers.thenIf
import com.android.compose.ui.graphics.drawInContainer
import com.android.compose.ui.util.IntIndexedMap
import com.android.compose.ui.util.lerp
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@@ -73,14 +72,6 @@ internal class Element(val key: ElementKey) {
    // are first seen by composition then layout/drawing code. See b/316901148#comment2 for details.
    val stateByContent = SnapshotStateMap<ContentKey, State>()

    /**
     * A sorted map of nesting depth (key) to content key (value). For shared elements it is used to
     * determine which content this element should be rendered by. The nesting depth refers to the
     * number of STLs nested within each other, starting at 0 for the parent STL and increasing by
     * one for each nested [NestedSceneTransitionLayout].
     */
    val renderAuthority = IntIndexedMap<ContentKey>()

    /**
     * The last transition that was used when computing the state (size, position and alpha) of this
     * element in any content, or `null` if it was last laid out when idle.
@@ -285,7 +276,6 @@ internal class ElementNode(
        val element =
            layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
        _element = element
        addToRenderAuthority(element)
        if (!element.stateByContent.contains(content.key)) {
            val contents = buildList {
                layoutImpl.ancestors.fastForEach { add(it.inContent) }
@@ -318,22 +308,9 @@ internal class ElementNode(
        removeNodeFromContentState()
        maybePruneMaps(layoutImpl, element, stateInContent)

        removeFromRenderAuthority()
        _element = null
    }

    private fun addToRenderAuthority(element: Element) {
        val nestingDepth = layoutImpl.ancestors.size
        element.renderAuthority[nestingDepth] = content.key
    }

    private fun removeFromRenderAuthority() {
        val nestingDepth = layoutImpl.ancestors.size
        if (element.renderAuthority[nestingDepth] == content.key) {
            element.renderAuthority.remove(nestingDepth)
        }
    }

    private fun removeNodeFromContentState() {
        stateInContent.nodes.remove(this)
    }
@@ -677,12 +654,8 @@ internal inline fun elementState(
            // Check if any ancestor runs a transition that has a transformation for the element
            states.fastForEachReversed { state ->
                if (
                    state is TransitionState.Transition &&
                        (state.transformationSpec.hasTransformation(
                            elementKey,
                            state.fromContent,
                        ) ||
                            state.transformationSpec.hasTransformation(elementKey, state.toContent))
                    isSharedElement(state, isInContent) ||
                        hasTransformationForElement(state, elementKey)
                ) {
                    return state
                }
@@ -710,6 +683,21 @@ internal inline fun elementState(
    return null
}

private inline fun isSharedElement(
    state: TransitionState,
    isInContent: (ContentKey) -> Boolean,
): Boolean {
    return state is TransitionState.Transition &&
        isInContent(state.fromContent) &&
        isInContent(state.toContent)
}

private fun hasTransformationForElement(state: TransitionState, elementKey: ElementKey): Boolean {
    return state is TransitionState.Transition &&
        (state.transformationSpec.hasTransformation(elementKey, state.fromContent) ||
            state.transformationSpec.hasTransformation(elementKey, state.toContent))
}

internal inline fun elementContentWhenIdle(
    layoutImpl: SceneTransitionLayoutImpl,
    currentState: TransitionState,
@@ -964,8 +952,7 @@ private fun shouldPlaceElement(
    val transition =
        when (elementState) {
            is TransitionState.Idle -> {
                return element.shouldBeRenderedBy(content) &&
                    content ==
                return content ==
                    elementContentWhenIdle(
                        layoutImpl,
                        elementState,
+2 −2
Original line number Diff line number Diff line
@@ -212,8 +212,8 @@ private fun shouldComposeMoveableElement(
        scenePicker.contentDuringTransition(
            element = elementKey,
            transition = transition,
            fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex,
            toContentZIndex = layoutImpl.content(transition.toContent).zIndex,
            fromContentZIndex = layoutImpl.content(transition.fromContent).globalZIndex,
            toContentZIndex = layoutImpl.content(transition.toContent).globalZIndex,
        )

    return pickedScene == content
+28 −17
Original line number Diff line number Diff line
@@ -62,7 +62,7 @@ fun SceneTransitionLayout(
    swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
    swipeDetector: SwipeDetector = DefaultSwipeDetector,
    @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
    builder: SceneTransitionLayoutScope.() -> Unit,
    builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
) {
    SceneTransitionLayoutForTesting(
        state,
@@ -75,7 +75,7 @@ fun SceneTransitionLayout(
    )
}

interface SceneTransitionLayoutScope {
interface SceneTransitionLayoutScope<out CS : ContentScope> {
    /**
     * Add a scene to this layout, identified by [key].
     *
@@ -88,7 +88,7 @@ interface SceneTransitionLayoutScope {
    fun scene(
        key: SceneKey,
        userActions: Map<UserAction, UserActionResult> = emptyMap(),
        content: @Composable ContentScope.() -> Unit,
        content: @Composable CS.() -> Unit,
    )

    /**
@@ -118,7 +118,7 @@ interface SceneTransitionLayoutScope {
            mapOf(Back to UserActionResult.HideOverlay(key)),
        alignment: Alignment = Alignment.Center,
        isModal: Boolean = true,
        content: @Composable ContentScope.() -> Unit,
        content: @Composable CS.() -> Unit,
    )
}

@@ -253,18 +253,6 @@ interface BaseContentScope : ElementStateScope {
    fun Modifier.disableSwipesWhenScrolling(
        bounds: NestedScrollableBound = NestedScrollableBound.Any
    ): Modifier

    /**
     * A [NestedSceneTransitionLayout] will share its elements with its ancestor STLs therefore
     * enabling sharedElement transitions between them.
     */
    // TODO(b/380070506): Add more parameters when default params are supported in Kotlin 2.0.21
    @Composable
    fun NestedSceneTransitionLayout(
        state: SceneTransitionLayoutState,
        modifier: Modifier,
        builder: SceneTransitionLayoutScope.() -> Unit,
    )
}

@Stable
@@ -337,6 +325,29 @@ interface ContentScope : BaseContentScope {
        type: SharedValueType<T, *>,
        canOverflow: Boolean,
    ): AnimatedState<T>

    /**
     * A [NestedSceneTransitionLayout] will share its elements with its ancestor STLs therefore
     * enabling sharedElement transitions between them.
     */
    // TODO(b/380070506): Add more parameters when default params are supported in Kotlin 2.0.21
    @Composable
    fun NestedSceneTransitionLayout(
        state: SceneTransitionLayoutState,
        modifier: Modifier,
        builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
    )
}

internal interface InternalContentScope : ContentScope {

    @Composable
    fun NestedSceneTransitionLayoutForTesting(
        state: SceneTransitionLayoutState,
        modifier: Modifier,
        onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)?,
        builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
    )
}

/**
@@ -730,7 +741,7 @@ internal fun SceneTransitionLayoutForTesting(
    sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
    ancestors: List<Ancestor> = remember { emptyList() },
    lookaheadScope: LookaheadScope? = null,
    builder: SceneTransitionLayoutScope.() -> Unit,
    builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
) {
    val density = LocalDensity.current
    val directionChangeSlop = LocalViewConfiguration.current.touchSlop
+45 −22
Original line number Diff line number Diff line
@@ -45,6 +45,8 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.zIndex
@@ -79,7 +81,7 @@ internal class SceneTransitionLayoutImpl(
    internal var swipeSourceDetector: SwipeSourceDetector,
    internal var swipeDetector: SwipeDetector,
    internal var transitionInterceptionThreshold: Float,
    builder: SceneTransitionLayoutScope.() -> Unit,
    builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,

    /**
     * The scope that should be used by *animations started by this layout only*, i.e. animations
@@ -222,19 +224,30 @@ internal class SceneTransitionLayoutImpl(
        state.checkThread()
    }

    internal fun scene(key: SceneKey): Scene {
        return scenes[key] ?: error("Scene $key is not configured")
    private fun sceneOrNull(key: SceneKey): Scene? {
        return scenes[key]
            ?: ancestors
                .fastFirstOrNull { it.layoutImpl.scenes[key] != null }
                ?.layoutImpl
                ?.scenes
                ?.get(key)
    }

    internal fun contentOrNull(key: ContentKey): Content? {
        return when (key) {
            is SceneKey -> scenes[key]
            is OverlayKey -> overlays[key]
    private fun overlayOrNull(key: OverlayKey): Overlay? {
        return overlays[key]
            ?: ancestors
                .fastFirstOrNull { it.layoutImpl.overlays[key] != null }
                ?.layoutImpl
                ?.overlays
                ?.get(key)
    }

    internal fun scene(key: SceneKey): Scene {
        return sceneOrNull(key) ?: error("Scene $key is not configured")
    }

    internal fun overlay(key: OverlayKey): Overlay {
        return overlays[key] ?: error("Overlay $key is not configured")
        return overlayOrNull(key) ?: error("Overlay $key is not configured")
    }

    internal fun content(key: ContentKey): Content {
@@ -244,6 +257,10 @@ internal class SceneTransitionLayoutImpl(
        }
    }

    internal fun isAncestorContent(content: ContentKey): Boolean {
        return ancestors.fastAny { it.inContent == content }
    }

    internal fun contentForUserActions(): Content {
        return findOverlayWithHighestZIndex() ?: scene(state.transitionState.currentScene)
    }
@@ -267,7 +284,7 @@ internal class SceneTransitionLayoutImpl(
    }

    internal fun updateContents(
        builder: SceneTransitionLayoutScope.() -> Unit,
        builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
        layoutDirection: LayoutDirection,
    ) {
        // Keep a reference of the current contents. After processing [builder], the contents that
@@ -276,15 +293,17 @@ internal class SceneTransitionLayoutImpl(
        val overlaysToRemove =
            if (_overlays == null) mutableSetOf() else overlays.keys.toMutableSet()

        val parentZIndex =
            if (ancestors.isEmpty()) 0L else content(ancestors.last().inContent).globalZIndex
        // The incrementing zIndex of each scene.
        var zIndex = 0f
        var zIndex = 0
        var overlaysDefined = false

        object : SceneTransitionLayoutScope {
        object : SceneTransitionLayoutScope<InternalContentScope> {
                override fun scene(
                    key: SceneKey,
                    userActions: Map<UserAction, UserActionResult>,
                    content: @Composable ContentScope.() -> Unit,
                    content: @Composable InternalContentScope.() -> Unit,
                ) {
                    require(!overlaysDefined) { "all scenes must be defined before overlays" }

@@ -292,11 +311,14 @@ internal class SceneTransitionLayoutImpl(

                    val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection)
                    val scene = scenes[key]
                    val globalZIndex =
                        Content.calculateGlobalZIndex(parentZIndex, ++zIndex, ancestors.size)
                    if (scene != null) {
                        // Update an existing scene.
                        scene.content = content
                        scene.userActions = resolvedUserActions
                        scene.zIndex = zIndex
                        scene.zIndex = zIndex.toFloat()
                        scene.globalZIndex = globalZIndex
                    } else {
                        // New scene.
                        scenes[key] =
@@ -305,11 +327,10 @@ internal class SceneTransitionLayoutImpl(
                                this@SceneTransitionLayoutImpl,
                                content,
                                resolvedUserActions,
                                zIndex,
                                zIndex.toFloat(),
                                globalZIndex,
                            )
                    }

                    zIndex++
                }

                override fun overlay(
@@ -317,17 +338,20 @@ internal class SceneTransitionLayoutImpl(
                    userActions: Map<UserAction, UserActionResult>,
                    alignment: Alignment,
                    isModal: Boolean,
                    content: @Composable (ContentScope.() -> Unit),
                    content: @Composable (InternalContentScope.() -> Unit),
                ) {
                    overlaysDefined = true
                    overlaysToRemove.remove(key)

                    val overlay = overlays[key]
                    val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection)
                    val globalZIndex =
                        Content.calculateGlobalZIndex(parentZIndex, ++zIndex, ancestors.size)
                    if (overlay != null) {
                        // Update an existing overlay.
                        overlay.content = content
                        overlay.zIndex = zIndex
                        overlay.zIndex = zIndex.toFloat()
                        overlay.globalZIndex = globalZIndex
                        overlay.userActions = resolvedUserActions
                        overlay.alignment = alignment
                        overlay.isModal = isModal
@@ -339,13 +363,12 @@ internal class SceneTransitionLayoutImpl(
                                this@SceneTransitionLayoutImpl,
                                content,
                                resolvedUserActions,
                                zIndex,
                                zIndex.toFloat(),
                                globalZIndex,
                                alignment,
                                isModal,
                            )
                    }

                    zIndex++
                }
            }
            .builder()
Loading