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 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 {
+25 −12
Original line number 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.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
@@ -99,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(
@@ -109,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
@@ -128,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)
@@ -145,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