Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +23 −8 Original line number Diff line number Diff line Loading @@ -828,15 +828,21 @@ private fun shouldPlaceElement( // Don't place the element in this content if this content is not part of the current element // transition. if (content != transition.fromContent && content != transition.toContent) { val isReplacingOverlay = transition is TransitionState.Transition.ReplaceOverlay if ( content != transition.fromContent && content != transition.toContent && (!isReplacingOverlay || content != transition.currentScene) ) { return false } // Place the element if it is not shared. if ( transition.fromContent !in element.stateByContent || transition.toContent !in element.stateByContent ) { var copies = 0 if (transition.fromContent in element.stateByContent) copies++ if (transition.toContent in element.stateByContent) copies++ if (isReplacingOverlay && transition.currentScene in element.stateByContent) copies++ if (copies <= 1) { return true } Loading Loading @@ -1269,9 +1275,10 @@ private inline fun <T> computeValue( // If we are replacing an overlay and the element is both in a single overlay and in the current // scene, interpolate the state of the element using the current scene as the other scene. var currentSceneState: Element.State? = null if (!isSharedElement && transition is TransitionState.Transition.ReplaceOverlay) { val currentSceneState = element.stateByContent[transition.currentScene] if (currentSceneState != null) { currentSceneState = element.stateByContent[transition.currentScene] if (currentSceneState != null && isSharedElementEnabled(element.key, transition)) { return interpolateSharedElement( transition = transition, contentValue = contentValue, Loading @@ -1290,6 +1297,8 @@ private inline fun <T> computeValue( when { isSharedElement && currentContent == fromContent -> fromState isSharedElement -> toState currentSceneState != null && currentContent == transition.currentScene -> currentSceneState else -> fromState ?: toState } ) Loading Loading @@ -1409,7 +1418,13 @@ private inline fun <T> computeValue( val rangeProgress = transformation.range?.progress(progress) ?: progress // Interpolate between the value at rest and the value before entering/after leaving. val isEntering = content == toContent val isEntering = when { content == toContent -> true content == fromContent -> false content == transition.currentScene -> toState == null else -> content == toContent } return if (isEntering) { lerp(targetValue, idleValue, rangeProgress) } else { Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt +79 −0 Original line number Diff line number Diff line Loading @@ -653,6 +653,85 @@ class OverlayTest { } } @Test fun replaceAnimation_elementInCurrentSceneAndOneOverlay_sharedElementDisabled() { rule.testReplaceOverlayTransition( currentSceneContent = { Box(Modifier.size(width = 180.dp, height = 120.dp)) { Foo(width = 60.dp, height = 40.dp) } }, fromContent = {}, fromAlignment = Alignment.TopStart, toContent = { Foo(width = 100.dp, height = 80.dp) }, transition = { // 4 frames of animation spec = tween(4 * 16, easing = LinearEasing) // Scale Foo to/from size 0 in each content instead of sharing it. sharedElement(TestElements.Foo, enabled = false) scaleSize(TestElements.Foo, width = 0f, height = 0f) }, ) { before { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(60.dp, 40.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() } at(16) { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(45.dp, 30.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(25.dp, 20.dp) .assertPositionInRootIsEqualTo(((180 - 25) / 2f).dp, ((120 - 20) / 2f).dp) } at(32) { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(30.dp, 20.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(50.dp, 40.dp) .assertPositionInRootIsEqualTo(((180 - 50) / 2f).dp, ((120 - 40) / 2f).dp) } at(48) { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(15.dp, 10.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(75.dp, 60.dp) .assertPositionInRootIsEqualTo(((180 - 75) / 2f).dp, ((120 - 60) / 2f).dp) } after { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertExists() .assertIsNotDisplayed() rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(100.dp, 80.dp) .assertPositionInRootIsEqualTo(40.dp, 20.dp) } } } @Test fun overscrollingOverlay_movableElementNotInOverlay() { val state = Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +23 −8 Original line number Diff line number Diff line Loading @@ -828,15 +828,21 @@ private fun shouldPlaceElement( // Don't place the element in this content if this content is not part of the current element // transition. if (content != transition.fromContent && content != transition.toContent) { val isReplacingOverlay = transition is TransitionState.Transition.ReplaceOverlay if ( content != transition.fromContent && content != transition.toContent && (!isReplacingOverlay || content != transition.currentScene) ) { return false } // Place the element if it is not shared. if ( transition.fromContent !in element.stateByContent || transition.toContent !in element.stateByContent ) { var copies = 0 if (transition.fromContent in element.stateByContent) copies++ if (transition.toContent in element.stateByContent) copies++ if (isReplacingOverlay && transition.currentScene in element.stateByContent) copies++ if (copies <= 1) { return true } Loading Loading @@ -1269,9 +1275,10 @@ private inline fun <T> computeValue( // If we are replacing an overlay and the element is both in a single overlay and in the current // scene, interpolate the state of the element using the current scene as the other scene. var currentSceneState: Element.State? = null if (!isSharedElement && transition is TransitionState.Transition.ReplaceOverlay) { val currentSceneState = element.stateByContent[transition.currentScene] if (currentSceneState != null) { currentSceneState = element.stateByContent[transition.currentScene] if (currentSceneState != null && isSharedElementEnabled(element.key, transition)) { return interpolateSharedElement( transition = transition, contentValue = contentValue, Loading @@ -1290,6 +1297,8 @@ private inline fun <T> computeValue( when { isSharedElement && currentContent == fromContent -> fromState isSharedElement -> toState currentSceneState != null && currentContent == transition.currentScene -> currentSceneState else -> fromState ?: toState } ) Loading Loading @@ -1409,7 +1418,13 @@ private inline fun <T> computeValue( val rangeProgress = transformation.range?.progress(progress) ?: progress // Interpolate between the value at rest and the value before entering/after leaving. val isEntering = content == toContent val isEntering = when { content == toContent -> true content == fromContent -> false content == transition.currentScene -> toState == null else -> content == toContent } return if (isEntering) { lerp(targetValue, idleValue, rangeProgress) } else { Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt +79 −0 Original line number Diff line number Diff line Loading @@ -653,6 +653,85 @@ class OverlayTest { } } @Test fun replaceAnimation_elementInCurrentSceneAndOneOverlay_sharedElementDisabled() { rule.testReplaceOverlayTransition( currentSceneContent = { Box(Modifier.size(width = 180.dp, height = 120.dp)) { Foo(width = 60.dp, height = 40.dp) } }, fromContent = {}, fromAlignment = Alignment.TopStart, toContent = { Foo(width = 100.dp, height = 80.dp) }, transition = { // 4 frames of animation spec = tween(4 * 16, easing = LinearEasing) // Scale Foo to/from size 0 in each content instead of sharing it. sharedElement(TestElements.Foo, enabled = false) scaleSize(TestElements.Foo, width = 0f, height = 0f) }, ) { before { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(60.dp, 40.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() } at(16) { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(45.dp, 30.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(25.dp, 20.dp) .assertPositionInRootIsEqualTo(((180 - 25) / 2f).dp, ((120 - 20) / 2f).dp) } at(32) { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(30.dp, 20.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(50.dp, 40.dp) .assertPositionInRootIsEqualTo(((180 - 50) / 2f).dp, ((120 - 40) / 2f).dp) } at(48) { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertSizeIsEqualTo(15.dp, 10.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(75.dp, 60.dp) .assertPositionInRootIsEqualTo(((180 - 75) / 2f).dp, ((120 - 60) / 2f).dp) } after { rule .onNode(isElement(TestElements.Foo, content = SceneA)) .assertExists() .assertIsNotDisplayed() rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() rule .onNode(isElement(TestElements.Foo, content = OverlayB)) .assertSizeIsEqualTo(100.dp, 80.dp) .assertPositionInRootIsEqualTo(40.dp, 20.dp) } } } @Test fun overscrollingOverlay_movableElementNotInOverlay() { val state = Loading