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

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

Merge changes Ic5def4bd,I1f513191,I4ffceda1,I1b985338 into main

* changes:
  Add STLState.isTransitioning(from?, to?)
  Refactor STL Element maps logic
  Recompile STL sources when building tests
  Don't create a derived State for Element.drawScale
parents 65e60b26 5be587fb
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -21,12 +21,19 @@ package {
    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
}

filegroup {
    name: "PlatformComposeSceneTransitionLayout-srcs",
    srcs: [
        "src/**/*.kt",
    ],
}

android_library {
    name: "PlatformComposeSceneTransitionLayout",
    manifest: "AndroidManifest.xml",

    srcs: [
        "src/**/*.kt",
        ":PlatformComposeSceneTransitionLayout-srcs",
    ],

    static_libs: [
+105 −41
Original line number Diff line number Diff line
@@ -17,18 +17,14 @@
package com.android.compose.animation.scene

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
@@ -39,6 +35,7 @@ import androidx.compose.ui.layout.IntermediateMeasureScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -46,6 +43,7 @@ import androidx.compose.ui.unit.round
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.ui.util.lerp
import kotlinx.coroutines.launch

/** An element on screen, that can be composed in one or more scenes. */
internal class Element(val key: ElementKey) {
@@ -92,13 +90,20 @@ internal class Element(val key: ElementKey) {
    }

    /** The target values of this element in a given scene. */
    class TargetValues {
    class TargetValues(val scene: SceneKey) {
        val lastValues = Values()

        var targetSize by mutableStateOf(SizeUnspecified)
        var targetOffset by mutableStateOf(Offset.Unspecified)

        val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()

        /**
         * The attached [ElementNode] a Modifier.element() for a given element and scene. During
         * composition, this set could have 0 to 2 elements. After composition and after all
         * modifier nodes have been attached/detached, this set should contain exactly 1 element.
         */
        val nodes = mutableSetOf<ElementNode>()
    }

    /** A shared value of this element. */
@@ -125,50 +130,31 @@ internal fun Modifier.element(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: Scene,
    key: ElementKey,
): Modifier = composed {
    val sceneValues = remember(scene, key) { Element.TargetValues() }
    val element =
): Modifier {
    val element: Element
    val sceneValues: Element.TargetValues

    // Get the element associated to [key] if it was already composed in another scene,
    // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
    // withoutReadObservation() because there is no need to recompose when that map is mutated.
    Snapshot.withoutReadObservation {
            val element =
                layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
            val previousValues = element.sceneValues[scene.key]
            if (previousValues == null) {
                element.sceneValues[scene.key] = sceneValues
            } else if (previousValues != sceneValues) {
                error("$key was composed multiple times in $scene")
            }

            element
        }

    DisposableEffect(scene, sceneValues, element) {
        onDispose {
            element.sceneValues.remove(scene.key)

            // This was the last scene this element was in, so remove it from the map.
            if (element.sceneValues.isEmpty()) {
                layoutImpl.elements.remove(element.key)
            }
        }
    }

    val drawScale by
        remember(layoutImpl, element, scene, sceneValues) {
            derivedStateOf { getDrawScale(layoutImpl, element, scene, sceneValues) }
        element = layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
        sceneValues =
            element.sceneValues[scene.key]
                ?: Element.TargetValues(scene.key).also { element.sceneValues[scene.key] = it }
    }

    drawWithContent {
    return this.then(ElementModifier(layoutImpl, element, sceneValues))
        .drawWithContent {
            if (shouldDrawElement(layoutImpl, scene, element)) {
                val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
                if (drawScale == Scale.Default) {
                    this@drawWithContent.drawContent()
                    drawContent()
                } else {
                    scale(
                        drawScale.scaleX,
                        drawScale.scaleY,
                        if (drawScale.pivot.isUnspecified) center else drawScale.pivot
                        if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
                    ) {
                        this@drawWithContent.drawContent()
                    }
@@ -186,6 +172,84 @@ internal fun Modifier.element(
        .testTag(key.testTag)
}

/**
 * An element associated to [ElementNode]. Note that this element does not support updates as its
 * arguments should always be the same.
 */
private data class ElementModifier(
    private val layoutImpl: SceneTransitionLayoutImpl,
    private val element: Element,
    private val sceneValues: Element.TargetValues,
) : ModifierNodeElement<ElementNode>() {
    override fun create(): ElementNode = ElementNode(layoutImpl, element, sceneValues)

    override fun update(node: ElementNode) {
        node.update(layoutImpl, element, sceneValues)
    }
}

internal class ElementNode(
    layoutImpl: SceneTransitionLayoutImpl,
    element: Element,
    sceneValues: Element.TargetValues,
) : Modifier.Node() {
    private var layoutImpl: SceneTransitionLayoutImpl = layoutImpl
    private var element: Element = element
    private var sceneValues: Element.TargetValues = sceneValues

    override fun onAttach() {
        super.onAttach()
        addNodeToSceneValues()
    }

    private fun addNodeToSceneValues() {
        sceneValues.nodes.add(this)

        coroutineScope.launch {
            // At this point all [CodeLocationNode] have been attached or detached, which means that
            // [sceneValues.codeLocations] should have exactly 1 element, otherwise this means that
            // this element was composed multiple times in the same scene.
            val nCodeLocations = sceneValues.nodes.size
            if (nCodeLocations != 1 || !sceneValues.nodes.contains(this@ElementNode)) {
                error("${element.key} was composed $nCodeLocations times in ${sceneValues.scene}")
            }
        }
    }

    override fun onDetach() {
        super.onDetach()
        removeNodeFromSceneValues()
    }

    private fun removeNodeFromSceneValues() {
        sceneValues.nodes.remove(this)

        // If element is not composed from this scene anymore, remove the scene values. This works
        // because [onAttach] is called before [onDetach], so if an element is moved from the UI
        // tree we will first add the new code location then remove the old one.
        if (sceneValues.nodes.isEmpty()) {
            element.sceneValues.remove(sceneValues.scene)
        }

        // If the element is not composed in any scene, remove it from the elements map.
        if (element.sceneValues.isEmpty()) {
            layoutImpl.elements.remove(element.key)
        }
    }

    fun update(
        layoutImpl: SceneTransitionLayoutImpl,
        element: Element,
        sceneValues: Element.TargetValues,
    ) {
        removeNodeFromSceneValues()
        this.layoutImpl = layoutImpl
        this.element = element
        this.sceneValues = sceneValues
        addNodeToSceneValues()
    }
}

private fun shouldDrawElement(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: Scene,
+8 −2
Original line number Diff line number Diff line
@@ -43,7 +43,10 @@ class SceneKey(
    name: String,
    identity: Any = Object(),
) : Key(name, identity) {
    @VisibleForTesting val testTag: String = "scene:$name"
    @VisibleForTesting
    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
    // access internal members.
    val testTag: String = "scene:$name"

    /** The unique [ElementKey] identifying this scene's root element. */
    val rootElementKey = ElementKey(name, identity)
@@ -64,7 +67,10 @@ class ElementKey(
     */
    val isBackground: Boolean = false,
) : Key(name, identity), ElementMatcher {
    @VisibleForTesting val testTag: String = "element:$name"
    @VisibleForTesting
    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
    // access internal members.
    val testTag: String = "element:$name"

    override fun matches(key: ElementKey, scene: SceneKey): Boolean {
        return key == this
+2 −0
Original line number Diff line number Diff line
@@ -76,6 +76,8 @@ private class SceneScopeImpl(
    private val layoutImpl: SceneTransitionLayoutImpl,
    private val scene: Scene,
) : SceneScope {
    override val layoutState: SceneTransitionLayoutState = layoutImpl.state

    override fun Modifier.element(key: ElementKey): Modifier {
        return element(layoutImpl, scene, key)
    }
+5 −10
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.compose.animation.scene

import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
@@ -37,8 +36,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

@VisibleForTesting
class SceneGestureHandler(
internal class SceneGestureHandler(
    internal val layoutImpl: SceneTransitionLayoutImpl,
    internal val orientation: Orientation,
    private val coroutineScope: CoroutineScope,
@@ -63,12 +61,10 @@ class SceneGestureHandler(
    internal val currentScene: Scene
        get() = layoutImpl.scene(transitionState.currentScene)

    @VisibleForTesting
    val isDrivingTransition
    internal val isDrivingTransition
        get() = transitionState == swipeTransition

    @VisibleForTesting
    var isAnimatingOffset
    internal var isAnimatingOffset
        get() = swipeTransition.isAnimatingOffset
        private set(value) {
            swipeTransition.isAnimatingOffset = value
@@ -81,7 +77,7 @@ class SceneGestureHandler(
     * The velocity threshold at which the intent of the user is to swipe up or down. It is the same
     * as SwipeableV2Defaults.VelocityThreshold.
     */
    @VisibleForTesting val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }
    internal val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }

    /**
     * The positional threshold at which the intent of the user is to swipe to the next scene. It is
@@ -533,8 +529,7 @@ private class SceneDraggableHandler(
    }
}

@VisibleForTesting
class SceneNestedScrollHandler(
internal class SceneNestedScrollHandler(
    private val gestureHandler: SceneGestureHandler,
    private val startBehavior: NestedScrollBehavior,
    private val endBehavior: NestedScrollBehavior,
Loading