Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +43 −19 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize 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.modifiers.thenIf import com.android.compose.ui.util.lerp Loading Loading @@ -196,29 +197,44 @@ private fun shouldDrawElement( state.fromScene == state.toScene || !layoutImpl.isTransitionReady(state) || state.fromScene !in element.sceneValues || state.toScene !in element.sceneValues || !isSharedElementEnabled(layoutImpl, state, element.key) state.toScene !in element.sceneValues ) { return true } val otherScene = layoutImpl.scenes.getValue( if (scene.key == state.fromScene) { state.toScene } else { state.fromScene val sharedTransformation = sharedElementTransformation(layoutImpl, state, element.key) if (sharedTransformation?.enabled == false) { return true } ) // When the element is shared, draw the one in the highest scene unless it is a background, i.e. // it is usually drawn below everything else. val isHighestScene = scene.zIndex > otherScene.zIndex return if (element.key.isBackground) { !isHighestScene } else { isHighestScene return shouldDrawOrComposeSharedElement( layoutImpl, state, scene.key, element.key, sharedTransformation, ) } internal fun shouldDrawOrComposeSharedElement( layoutImpl: SceneTransitionLayoutImpl, transition: TransitionState.Transition, scene: SceneKey, element: ElementKey, sharedTransformation: SharedElementTransformation? ): Boolean { val scenePicker = sharedTransformation?.scenePicker ?: DefaultSharedElementScenePicker val fromScene = transition.fromScene val toScene = transition.toScene return scenePicker.sceneDuringTransition( element = element, fromScene = fromScene, toScene = toScene, progress = transition::progress, fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex, toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex, ) == scene } private fun isSharedElementEnabled( Loading @@ -226,6 +242,14 @@ private fun isSharedElementEnabled( transition: TransitionState.Transition, element: ElementKey, ): Boolean { return sharedElementTransformation(layoutImpl, transition, element)?.enabled ?: true } internal fun sharedElementTransformation( layoutImpl: SceneTransitionLayoutImpl, transition: TransitionState.Transition, element: ElementKey, ): SharedElementTransformation? { 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 Loading @@ -238,7 +262,7 @@ private fun isSharedElementEnabled( ) } return sharedInFromScene?.enabled ?: true return sharedInFromScene } /** Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +19 −9 Original line number Diff line number Diff line Loading @@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier Loading Loading @@ -60,7 +62,16 @@ internal fun MovableElement( // which case we still need to draw it. val picture = remember { Picture() } if (shouldComposeMovableElement(layoutImpl, scene.key, element)) { // Whether we should compose the movable element here. The scene picker logic to know in // which scene we should compose/draw a movable element might depend on the current // transition progress, so we put this in a derivedStateOf to prevent many recompositions // during the transition. val shouldComposeMovableElement by remember(layoutImpl, scene.key, element) { derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) } } if (shouldComposeMovableElement) { Box( Modifier.drawWithCache { val width = size.width.toInt() Loading Loading @@ -172,14 +183,13 @@ private fun shouldComposeMovableElement( return scene == fromScene } // If we are ready in both scenes, then compose in the scene that has the highest zIndex (unless // it is a background) given that this is the one that is going to be drawn. val isHighestScene = layoutImpl.scene(scene).zIndex > layoutImpl.scene(otherScene).zIndex return if (element.key.isBackground) { !isHighestScene } else { isHighestScene } return shouldDrawOrComposeSharedElement( layoutImpl, transitionState, scene, element.key, sharedElementTransformation(layoutImpl, transitionState, element.key), ) } private class MovableElementScopeImpl( Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +45 −1 Original line number Diff line number Diff line Loading @@ -120,8 +120,14 @@ interface TransitionBuilder : PropertyTransformationBuilder { * * @param enabled whether the matched element(s) should actually be shared in this transition. * Defaults to true. * @param scenePicker the [SharedElementScenePicker] to use when deciding in which scene we * should draw or compose this shared element. */ fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true) fun sharedElement( matcher: ElementMatcher, enabled: Boolean = true, scenePicker: SharedElementScenePicker = DefaultSharedElementScenePicker, ) /** * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and Loading @@ -144,6 +150,44 @@ interface TransitionBuilder : PropertyTransformationBuilder { fun reversed(builder: TransitionBuilder.() -> Unit) } interface SharedElementScenePicker { /** * Return the scene in which [element] should be drawn (when using `Modifier.element(key)`) or * composed (when using `MovableElement(key)`) during the transition from [fromScene] to * [toScene]. */ fun sceneDuringTransition( element: ElementKey, fromScene: SceneKey, toScene: SceneKey, progress: () -> Float, fromSceneZIndex: Float, toSceneZIndex: Float, ): SceneKey } object DefaultSharedElementScenePicker : SharedElementScenePicker { override fun sceneDuringTransition( element: ElementKey, fromScene: SceneKey, toScene: SceneKey, progress: () -> Float, fromSceneZIndex: Float, toSceneZIndex: Float ): SceneKey { // By default shared elements are drawn in the highest scene possible, unless it is a // background. return if ( (fromSceneZIndex > toSceneZIndex && !element.isBackground) || (fromSceneZIndex < toSceneZIndex && element.isBackground) ) { fromScene } else { toScene } } } @TransitionDsl interface PropertyTransformationBuilder { /** Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +6 −2 Original line number Diff line number Diff line Loading @@ -111,8 +111,12 @@ internal class TransitionBuilderImpl : TransitionBuilder { range = null } override fun sharedElement(matcher: ElementMatcher, enabled: Boolean) { transformations.add(SharedElementTransformation(matcher, enabled)) override fun sharedElement( matcher: ElementMatcher, enabled: Boolean, scenePicker: SharedElementScenePicker, ) { transformations.add(SharedElementTransformation(matcher, enabled, scenePicker)) } override fun timestampRange( Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.Scene import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.SharedElementScenePicker import com.android.compose.animation.scene.TransitionState /** A transformation applied to one or more elements during a transition. */ Loading Loading @@ -48,6 +49,7 @@ sealed interface Transformation { internal class SharedElementTransformation( override val matcher: ElementMatcher, internal val enabled: Boolean, internal val scenePicker: SharedElementScenePicker, ) : Transformation /** A transformation that is applied on the element during the whole transition. */ Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +43 −19 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize 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.modifiers.thenIf import com.android.compose.ui.util.lerp Loading Loading @@ -196,29 +197,44 @@ private fun shouldDrawElement( state.fromScene == state.toScene || !layoutImpl.isTransitionReady(state) || state.fromScene !in element.sceneValues || state.toScene !in element.sceneValues || !isSharedElementEnabled(layoutImpl, state, element.key) state.toScene !in element.sceneValues ) { return true } val otherScene = layoutImpl.scenes.getValue( if (scene.key == state.fromScene) { state.toScene } else { state.fromScene val sharedTransformation = sharedElementTransformation(layoutImpl, state, element.key) if (sharedTransformation?.enabled == false) { return true } ) // When the element is shared, draw the one in the highest scene unless it is a background, i.e. // it is usually drawn below everything else. val isHighestScene = scene.zIndex > otherScene.zIndex return if (element.key.isBackground) { !isHighestScene } else { isHighestScene return shouldDrawOrComposeSharedElement( layoutImpl, state, scene.key, element.key, sharedTransformation, ) } internal fun shouldDrawOrComposeSharedElement( layoutImpl: SceneTransitionLayoutImpl, transition: TransitionState.Transition, scene: SceneKey, element: ElementKey, sharedTransformation: SharedElementTransformation? ): Boolean { val scenePicker = sharedTransformation?.scenePicker ?: DefaultSharedElementScenePicker val fromScene = transition.fromScene val toScene = transition.toScene return scenePicker.sceneDuringTransition( element = element, fromScene = fromScene, toScene = toScene, progress = transition::progress, fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex, toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex, ) == scene } private fun isSharedElementEnabled( Loading @@ -226,6 +242,14 @@ private fun isSharedElementEnabled( transition: TransitionState.Transition, element: ElementKey, ): Boolean { return sharedElementTransformation(layoutImpl, transition, element)?.enabled ?: true } internal fun sharedElementTransformation( layoutImpl: SceneTransitionLayoutImpl, transition: TransitionState.Transition, element: ElementKey, ): SharedElementTransformation? { 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 Loading @@ -238,7 +262,7 @@ private fun isSharedElementEnabled( ) } return sharedInFromScene?.enabled ?: true return sharedInFromScene } /** Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +19 −9 Original line number Diff line number Diff line Loading @@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier Loading Loading @@ -60,7 +62,16 @@ internal fun MovableElement( // which case we still need to draw it. val picture = remember { Picture() } if (shouldComposeMovableElement(layoutImpl, scene.key, element)) { // Whether we should compose the movable element here. The scene picker logic to know in // which scene we should compose/draw a movable element might depend on the current // transition progress, so we put this in a derivedStateOf to prevent many recompositions // during the transition. val shouldComposeMovableElement by remember(layoutImpl, scene.key, element) { derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) } } if (shouldComposeMovableElement) { Box( Modifier.drawWithCache { val width = size.width.toInt() Loading Loading @@ -172,14 +183,13 @@ private fun shouldComposeMovableElement( return scene == fromScene } // If we are ready in both scenes, then compose in the scene that has the highest zIndex (unless // it is a background) given that this is the one that is going to be drawn. val isHighestScene = layoutImpl.scene(scene).zIndex > layoutImpl.scene(otherScene).zIndex return if (element.key.isBackground) { !isHighestScene } else { isHighestScene } return shouldDrawOrComposeSharedElement( layoutImpl, transitionState, scene, element.key, sharedElementTransformation(layoutImpl, transitionState, element.key), ) } private class MovableElementScopeImpl( Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +45 −1 Original line number Diff line number Diff line Loading @@ -120,8 +120,14 @@ interface TransitionBuilder : PropertyTransformationBuilder { * * @param enabled whether the matched element(s) should actually be shared in this transition. * Defaults to true. * @param scenePicker the [SharedElementScenePicker] to use when deciding in which scene we * should draw or compose this shared element. */ fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true) fun sharedElement( matcher: ElementMatcher, enabled: Boolean = true, scenePicker: SharedElementScenePicker = DefaultSharedElementScenePicker, ) /** * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and Loading @@ -144,6 +150,44 @@ interface TransitionBuilder : PropertyTransformationBuilder { fun reversed(builder: TransitionBuilder.() -> Unit) } interface SharedElementScenePicker { /** * Return the scene in which [element] should be drawn (when using `Modifier.element(key)`) or * composed (when using `MovableElement(key)`) during the transition from [fromScene] to * [toScene]. */ fun sceneDuringTransition( element: ElementKey, fromScene: SceneKey, toScene: SceneKey, progress: () -> Float, fromSceneZIndex: Float, toSceneZIndex: Float, ): SceneKey } object DefaultSharedElementScenePicker : SharedElementScenePicker { override fun sceneDuringTransition( element: ElementKey, fromScene: SceneKey, toScene: SceneKey, progress: () -> Float, fromSceneZIndex: Float, toSceneZIndex: Float ): SceneKey { // By default shared elements are drawn in the highest scene possible, unless it is a // background. return if ( (fromSceneZIndex > toSceneZIndex && !element.isBackground) || (fromSceneZIndex < toSceneZIndex && element.isBackground) ) { fromScene } else { toScene } } } @TransitionDsl interface PropertyTransformationBuilder { /** Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +6 −2 Original line number Diff line number Diff line Loading @@ -111,8 +111,12 @@ internal class TransitionBuilderImpl : TransitionBuilder { range = null } override fun sharedElement(matcher: ElementMatcher, enabled: Boolean) { transformations.add(SharedElementTransformation(matcher, enabled)) override fun sharedElement( matcher: ElementMatcher, enabled: Boolean, scenePicker: SharedElementScenePicker, ) { transformations.add(SharedElementTransformation(matcher, enabled, scenePicker)) } override fun timestampRange( Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.Scene import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.SharedElementScenePicker import com.android.compose.animation.scene.TransitionState /** A transformation applied to one or more elements during a transition. */ Loading Loading @@ -48,6 +49,7 @@ sealed interface Transformation { internal class SharedElementTransformation( override val matcher: ElementMatcher, internal val enabled: Boolean, internal val scenePicker: SharedElementScenePicker, ) : Transformation /** A transformation that is applied on the element during the whole transition. */ Loading