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

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

Merge changes from topic "transitions-disable-shared" into main

* changes:
  Make it possible to disable shared animations (1/2)
  Add TransitionBuilder.reversed()
parents c5f8d163 6da9977a
Loading
Loading
Loading
Loading
+42 −9
Original line number Diff line number Diff line
@@ -151,7 +151,7 @@ internal fun Modifier.element(
                element.lastAlpha = alpha
            }
        }
        .testTag(key.name)
        .testTag(key.testTag)
}

private fun shouldDrawElement(
@@ -167,7 +167,8 @@ private fun shouldDrawElement(
            state.fromScene == state.toScene ||
            !layoutImpl.isTransitionReady(state) ||
            state.fromScene !in element.sceneValues ||
            state.toScene !in element.sceneValues
            state.toScene !in element.sceneValues ||
            !isSharedElementEnabled(layoutImpl, state, element.key)
    ) {
        return true
    }
@@ -191,6 +192,26 @@ private fun shouldDrawElement(
    }
}

private fun isSharedElementEnabled(
    layoutImpl: SceneTransitionLayoutImpl,
    transition: TransitionState.Transition,
    element: ElementKey,
): Boolean {
    val spec = layoutImpl.transitions.transitionSpec(transition.fromScene, transition.toScene)
    val sharedInFromScene = spec.transformations(element, transition.fromScene).shared
    val sharedInToScene = spec.transformations(element, transition.toScene).shared

    // The sharedElement() transformation must either be null or be the same in both scenes.
    if (sharedInFromScene != sharedInToScene) {
        error(
            "Different sharedElement() transformations matched $element (from=$sharedInFromScene " +
                "to=$sharedInToScene)"
        )
    }

    return sharedInFromScene?.enabled ?: true
}

/**
 * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
 * throughout the current transition, if any.
@@ -213,7 +234,7 @@ private fun Modifier.modifierTransformations(

            return layoutImpl.transitions
                .transitionSpec(fromScene, state.toScene)
                .transformations(element.key)
                .transformations(element.key, scene.key)
                .modifier
                .fold(this) { modifier, transformation ->
                    with(transformation) {
@@ -407,17 +428,20 @@ private inline fun <T> computeValue(
    // The element is shared: interpolate between the value in fromScene and the value in toScene.
    // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
    // elements follow the finger direction.
    if (fromValues != null && toValues != null) {
    val isSharedElement = fromValues != null && toValues != null
    if (isSharedElement && isSharedElementEnabled(layoutImpl, state, element.key)) {
        return lerp(
            sceneValue(fromValues),
            sceneValue(toValues),
            sceneValue(fromValues!!),
            sceneValue(toValues!!),
            transitionProgress,
        )
    }

    val transformation =
        transformation(
            layoutImpl.transitions.transitionSpec(fromScene, toScene).transformations(element.key)
            layoutImpl.transitions
                .transitionSpec(fromScene, toScene)
                .transformations(element.key, scene.key)
        )
        // If there is no transformation explicitly associated to this element value, let's use
        // the value given by the system (like the current position and size given by the layout
@@ -426,12 +450,21 @@ private inline fun <T> computeValue(

    // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
    // end (for leaving elements) of the transition.
    val sceneValues =
        checkNotNull(
            when {
                isSharedElement && scene.key == fromScene -> fromValues
                isSharedElement -> toValues
                else -> fromValues ?: toValues
            }
        )

    val targetValue =
        transformation.transform(
            layoutImpl,
            scene,
            element,
            fromValues ?: toValues!!,
            sceneValues,
            state,
            idleValue,
        )
@@ -440,7 +473,7 @@ private inline fun <T> computeValue(
    val rangeProgress = transformation.range?.progress(transitionProgress) ?: transitionProgress

    // Interpolate between the value at rest and the value before entering/after leaving.
    val isEntering = fromValues == null
    val isEntering = scene.key == toScene
    return if (isEntering) {
        lerp(targetValue, idleValue, rangeProgress)
    } else {
+37 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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

/** An interface to match one or more elements. */
interface ElementMatcher {
    /** Whether the element with key [key] in scene [scene] matches this matcher. */
    fun matches(key: ElementKey, scene: SceneKey): Boolean
}

/**
 * Returns an [ElementMatcher] that matches elements in [scene] also matching [this]
 * [ElementMatcher].
 */
fun ElementMatcher.inScene(scene: SceneKey): ElementMatcher {
    val delegate = this
    val matcherScene = scene
    return object : ElementMatcher {
        override fun matches(key: ElementKey, scene: SceneKey): Boolean {
            return scene == matcherScene && delegate.matches(key, scene)
        }
    }
}
+9 −2
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.compose.animation.scene

import androidx.annotation.VisibleForTesting

/**
 * A base class to create unique keys, associated to an [identity] that is used to check the
 * equality of two key instances.
@@ -41,6 +43,7 @@ class SceneKey(
    name: String,
    identity: Any = Object(),
) : Key(name, identity) {
    @VisibleForTesting val testTag: String = "scene:$name"

    /** The unique [ElementKey] identifying this scene's root element. */
    val rootElementKey = ElementKey(name, identity)
@@ -61,7 +64,9 @@ class ElementKey(
     */
    val isBackground: Boolean = false,
) : Key(name, identity), ElementMatcher {
    override fun matches(key: ElementKey): Boolean {
    @VisibleForTesting val testTag: String = "element:$name"

    override fun matches(key: ElementKey, scene: SceneKey): Boolean {
        return key == this
    }

@@ -73,7 +78,9 @@ class ElementKey(
        /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */
        fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher {
            return object : ElementMatcher {
                override fun matches(key: ElementKey): Boolean = predicate(key.identity)
                override fun matches(key: ElementKey, scene: SceneKey): Boolean {
                    return predicate(key.identity)
                }
            }
        }
    }
+4 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.zIndex

@@ -45,7 +46,9 @@ internal class Scene(

    @Composable
    fun Content(modifier: Modifier = Modifier) {
        Box(modifier.zIndex(zIndex).onPlaced { size = it.size }) { scope.content() }
        Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) {
            scope.content()
        }
    }

    override fun toString(): String {
+29 −14
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.compose.animation.scene

import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.snap
import androidx.compose.ui.geometry.Offset
@@ -28,6 +29,7 @@ import com.android.compose.animation.scene.transformation.ModifierTransformation
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
import com.android.compose.animation.scene.transformation.ScaleSize
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transformation.Transformation
import com.android.compose.animation.scene.transformation.Translate
import com.android.compose.ui.util.fastForEach
@@ -35,11 +37,12 @@ import com.android.compose.ui.util.fastMap

/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions(
    private val transitionSpecs: List<TransitionSpec>,
    @get:VisibleForTesting val transitionSpecs: List<TransitionSpec>,
) {
    private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()

    internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
    @VisibleForTesting
    fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
        return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
    }

@@ -97,7 +100,8 @@ data class TransitionSpec(
    val transformations: List<Transformation>,
    val spec: AnimationSpec<Float>,
) {
    private val cache = mutableMapOf<ElementKey, ElementTransformations>()
    // TODO(b/302300957): Make sure this cache does not infinitely grow.
    private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()

    internal fun reverse(): TransitionSpec {
        return copy(
@@ -107,12 +111,18 @@ data class TransitionSpec(
        )
    }

    internal fun transformations(element: ElementKey): ElementTransformations {
        return cache.getOrPut(element) { computeTransformations(element) }
    internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations {
        return cache
            .getOrPut(element) { mutableMapOf() }
            .getOrPut(scene) { computeTransformations(element, scene) }
    }

    /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
    private fun computeTransformations(element: ElementKey): ElementTransformations {
    private fun computeTransformations(
        element: ElementKey,
        scene: SceneKey,
    ): ElementTransformations {
        var shared: SharedElementTransformation? = null
        val modifier = mutableListOf<ModifierTransformation>()
        var offset: PropertyTransformation<Offset>? = null
        var size: PropertyTransformation<IntSize>? = null
@@ -126,16 +136,16 @@ data class TransitionSpec(
                is Translate,
                is EdgeTranslate,
                is AnchoredTranslate -> {
                    throwIfNotNull(offset, element, property = "offset")
                    throwIfNotNull(offset, element, name = "offset")
                    offset = root as PropertyTransformation<Offset>
                }
                is ScaleSize,
                is AnchoredSize -> {
                    throwIfNotNull(size, element, property = "size")
                    throwIfNotNull(size, element, name = "size")
                    size = root as PropertyTransformation<IntSize>
                }
                is Fade -> {
                    throwIfNotNull(alpha, element, property = "alpha")
                    throwIfNotNull(alpha, element, name = "alpha")
                    alpha = root as PropertyTransformation<Float>
                }
                is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
@@ -143,32 +153,37 @@ data class TransitionSpec(
        }

        transformations.fastForEach { transformation ->
            if (!transformation.matcher.matches(element)) {
            if (!transformation.matcher.matches(element, scene)) {
                return@fastForEach
            }

            when (transformation) {
                is SharedElementTransformation -> {
                    throwIfNotNull(shared, element, name = "shared")
                    shared = transformation
                }
                is ModifierTransformation -> modifier.add(transformation)
                is PropertyTransformation<*> -> onPropertyTransformation(transformation)
            }
        }

        return ElementTransformations(modifier, offset, size, alpha)
        return ElementTransformations(shared, modifier, offset, size, alpha)
    }

    private fun throwIfNotNull(
        previous: PropertyTransformation<*>?,
        previous: Transformation?,
        element: ElementKey,
        property: String,
        name: String,
    ) {
        if (previous != null) {
            error("$element has multiple transformations for its $property property")
            error("$element has multiple $name transformations")
        }
    }
}

/** The transformations of an element during a transition. */
internal class ElementTransformations(
    val shared: SharedElementTransformation?,
    val modifier: List<ModifierTransformation>,
    val offset: PropertyTransformation<Offset>?,
    val size: PropertyTransformation<IntSize>?,
Loading