Loading packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt +42 −9 Original line number Diff line number Diff line Loading @@ -151,7 +151,7 @@ internal fun Modifier.element( element.lastAlpha = alpha } } .testTag(key.name) .testTag(key.testTag) } private fun shouldDrawElement( Loading @@ -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 } Loading @@ -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. Loading @@ -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) { Loading Loading @@ -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 Loading @@ -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, ) Loading @@ -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 { Loading packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ElementMatcher.kt 0 → 100644 +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) } } } packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt +9 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) Loading @@ -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 } Loading @@ -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) } } } } Loading packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt +4 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt +29 −14 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) } } Loading Loading @@ -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( Loading @@ -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 Loading @@ -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) Loading @@ -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 Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt +42 −9 Original line number Diff line number Diff line Loading @@ -151,7 +151,7 @@ internal fun Modifier.element( element.lastAlpha = alpha } } .testTag(key.name) .testTag(key.testTag) } private fun shouldDrawElement( Loading @@ -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 } Loading @@ -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. Loading @@ -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) { Loading Loading @@ -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 Loading @@ -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, ) Loading @@ -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 { Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ElementMatcher.kt 0 → 100644 +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) } } }
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt +9 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) Loading @@ -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 } Loading @@ -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) } } } } Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt +4 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt +29 −14 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) } } Loading Loading @@ -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( Loading @@ -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 Loading @@ -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) Loading @@ -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