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

Commit 6da9977a authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Make it possible to disable shared animations (1/2)

This CL makes it possible to disable shared element animations, that are
enabled by default when an element is shared between two scenes. This
can for instance be used to disable the sharing of notifications when
going from the Shade to Lockscreen, or disabling the sharing of media
player when going from lockscreen to shade on the phone while keeping it
on tablets without having to use different keys.

See b/300867076#comment3 for videos.

Bug: 300867076
Test: atest SharedElementTest
Change-Id: I30384e064eb66f81f39ba0185f35c375f2741590
parent 97789b74
Loading
Loading
Loading
Loading
+42 −9
Original line number Original line Diff line number Diff line
@@ -151,7 +151,7 @@ internal fun Modifier.element(
                element.lastAlpha = alpha
                element.lastAlpha = alpha
            }
            }
        }
        }
        .testTag(key.name)
        .testTag(key.testTag)
}
}


private fun shouldDrawElement(
private fun shouldDrawElement(
@@ -167,7 +167,8 @@ private fun shouldDrawElement(
            state.fromScene == state.toScene ||
            state.fromScene == state.toScene ||
            !layoutImpl.isTransitionReady(state) ||
            !layoutImpl.isTransitionReady(state) ||
            state.fromScene !in element.sceneValues ||
            state.fromScene !in element.sceneValues ||
            state.toScene !in element.sceneValues
            state.toScene !in element.sceneValues ||
            !isSharedElementEnabled(layoutImpl, state, element.key)
    ) {
    ) {
        return true
        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
 * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
 * throughout the current transition, if any.
 * throughout the current transition, if any.
@@ -213,7 +234,7 @@ private fun Modifier.modifierTransformations(


            return layoutImpl.transitions
            return layoutImpl.transitions
                .transitionSpec(fromScene, state.toScene)
                .transitionSpec(fromScene, state.toScene)
                .transformations(element.key)
                .transformations(element.key, scene.key)
                .modifier
                .modifier
                .fold(this) { modifier, transformation ->
                .fold(this) { modifier, transformation ->
                    with(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.
    // 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
    // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
    // elements follow the finger direction.
    // 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(
        return lerp(
            sceneValue(fromValues),
            sceneValue(fromValues!!),
            sceneValue(toValues),
            sceneValue(toValues!!),
            transitionProgress,
            transitionProgress,
        )
        )
    }
    }


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

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


    // Interpolate between the value at rest and the value before entering/after leaving.
    // 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) {
    return if (isEntering) {
        lerp(targetValue, idleValue, rangeProgress)
        lerp(targetValue, idleValue, rangeProgress)
    } else {
    } else {
+37 −0
Original line number Original line 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 Original line Diff line number Diff line
@@ -16,6 +16,8 @@


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


    /** The unique [ElementKey] identifying this scene's root element. */
    /** The unique [ElementKey] identifying this scene's root element. */
    val rootElementKey = ElementKey(name, identity)
    val rootElementKey = ElementKey(name, identity)
@@ -61,7 +64,9 @@ class ElementKey(
     */
     */
    val isBackground: Boolean = false,
    val isBackground: Boolean = false,
) : Key(name, identity), ElementMatcher {
) : 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
        return key == this
    }
    }


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


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


    @Composable
    @Composable
    fun Content(modifier: Modifier = Modifier) {
    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 {
    override fun toString(): String {
+25 −12
Original line number Original line Diff line number Diff line
@@ -29,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.PropertyTransformation
import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
import com.android.compose.animation.scene.transformation.ScaleSize
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.Transformation
import com.android.compose.animation.scene.transformation.Translate
import com.android.compose.animation.scene.transformation.Translate
import com.android.compose.ui.util.fastForEach
import com.android.compose.ui.util.fastForEach
@@ -99,7 +100,8 @@ data class TransitionSpec(
    val transformations: List<Transformation>,
    val transformations: List<Transformation>,
    val spec: AnimationSpec<Float>,
    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 {
    internal fun reverse(): TransitionSpec {
        return copy(
        return copy(
@@ -109,12 +111,18 @@ data class TransitionSpec(
        )
        )
    }
    }


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


    /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
    /** 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>()
        val modifier = mutableListOf<ModifierTransformation>()
        var offset: PropertyTransformation<Offset>? = null
        var offset: PropertyTransformation<Offset>? = null
        var size: PropertyTransformation<IntSize>? = null
        var size: PropertyTransformation<IntSize>? = null
@@ -128,16 +136,16 @@ data class TransitionSpec(
                is Translate,
                is Translate,
                is EdgeTranslate,
                is EdgeTranslate,
                is AnchoredTranslate -> {
                is AnchoredTranslate -> {
                    throwIfNotNull(offset, element, property = "offset")
                    throwIfNotNull(offset, element, name = "offset")
                    offset = root as PropertyTransformation<Offset>
                    offset = root as PropertyTransformation<Offset>
                }
                }
                is ScaleSize,
                is ScaleSize,
                is AnchoredSize -> {
                is AnchoredSize -> {
                    throwIfNotNull(size, element, property = "size")
                    throwIfNotNull(size, element, name = "size")
                    size = root as PropertyTransformation<IntSize>
                    size = root as PropertyTransformation<IntSize>
                }
                }
                is Fade -> {
                is Fade -> {
                    throwIfNotNull(alpha, element, property = "alpha")
                    throwIfNotNull(alpha, element, name = "alpha")
                    alpha = root as PropertyTransformation<Float>
                    alpha = root as PropertyTransformation<Float>
                }
                }
                is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
                is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
@@ -145,32 +153,37 @@ data class TransitionSpec(
        }
        }


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


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


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


    private fun throwIfNotNull(
    private fun throwIfNotNull(
        previous: PropertyTransformation<*>?,
        previous: Transformation?,
        element: ElementKey,
        element: ElementKey,
        property: String,
        name: String,
    ) {
    ) {
        if (previous != null) {
        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. */
/** The transformations of an element during a transition. */
internal class ElementTransformations(
internal class ElementTransformations(
    val shared: SharedElementTransformation?,
    val modifier: List<ModifierTransformation>,
    val modifier: List<ModifierTransformation>,
    val offset: PropertyTransformation<Offset>?,
    val offset: PropertyTransformation<Offset>?,
    val size: PropertyTransformation<IntSize>?,
    val size: PropertyTransformation<IntSize>?,
Loading