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

Commit 3824e87d authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Don't assume that graphicsLayer blocks run after effects" into main

parents 7815011b 38231c3c
Loading
Loading
Loading
Loading
+24 −5
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.annotation.FrequentlyChangingValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
@@ -56,6 +57,8 @@ import kotlin.math.roundToInt
 */
@Stable
interface AnimatedState<T> : State<T> {
    @get:FrequentlyChangingValue override val value: T

    /**
     * Return a [State] that can be read during composition.
     *
@@ -337,7 +340,14 @@ internal fun <T> animateSharedValueAsState(
    }

    return remember(layoutImpl, content, element, canOverflow) {
        AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow)
        AnimatedStateImpl<T, Any>(
            layoutImpl,
            content,
            element,
            key,
            canOverflow,
            fallbackValue = value,
        )
    }
}

@@ -358,13 +368,20 @@ private fun maybePruneMaps(
    }
}

private fun <T, Delta> sharedValueOrNull(
    layoutImpl: SceneTransitionLayoutImpl,
    key: ValueKey,
    element: ElementKey?,
): SharedValue<T, Delta>? {
    return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> }
}

private fun <T, Delta> sharedValue(
    layoutImpl: SceneTransitionLayoutImpl,
    key: ValueKey,
    element: ElementKey?,
): SharedValue<T, Delta> {
    return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> }
        ?: error(valueReadTooEarlyMessage(key))
    return sharedValueOrNull(layoutImpl, key, element) ?: error(valueReadTooEarlyMessage(key))
}

private fun valueReadTooEarlyMessage(key: ValueKey) =
@@ -401,12 +418,14 @@ private class AnimatedStateImpl<T, Delta>(
    private val element: ElementKey?,
    private val key: ValueKey,
    private val canOverflow: Boolean,
    private val fallbackValue: T, // needed because of b/432799675.
) : AnimatedState<T> {
    override val value: T
        get() = value()

    private fun value(): T {
        val sharedValue = sharedValue<T, Delta>(layoutImpl, key, element)
        val sharedValue =
            sharedValueOrNull<T, Delta>(layoutImpl, key, element) ?: return fallbackValue
        val transition = transition(sharedValue)
        val value: T =
            valueOrNull(sharedValue, transition)
@@ -414,7 +433,7 @@ private class AnimatedStateImpl<T, Delta>(
                // scene value, but we have to because code of removed nodes can still run if they
                // are placed with a graphics layer.
                ?: sharedValue[content]
                ?: error(valueReadTooEarlyMessage(key))
                ?: return fallbackValue
        val interruptedValue = computeInterruptedValue(sharedValue, transition, value)
        sharedValue.lastValue = interruptedValue
        return interruptedValue
+44 −12
Original line number Diff line number Diff line
@@ -26,10 +26,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Dp
@@ -49,7 +52,6 @@ import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -286,17 +288,6 @@ class AnimatedSharedAsStateTest {
        }
    }

    @Test
    fun readingAnimatedStateValueDuringCompositionThrows() {
        assertThrows(IllegalStateException::class.java) {
            rule.testTransition(
                fromSceneContent = { animateContentIntAsState(0, TestValues.Value1).value },
                toSceneContent = {},
                transition = {},
            ) {}
        }
    }

    @Test
    fun readingAnimatedStateValueDuringCompositionIsStillPossible() {
        @Composable
@@ -631,4 +622,45 @@ class AnimatedSharedAsStateTest {
        assertThat(targetValues[SceneE]?.value).isEqualTo(null) // not composed
        assertThat(targetValues[SceneF]?.value).isEqualTo(null) // not composed
    }

    @Test
    // Regression test for b/432799675.
    fun animatedSharedValueInGraphicsLayer() {
        val lastGraphicsLayerRotationPerContent = mutableMapOf<ContentKey, Float>()

        @Composable
        fun ContentScope.SharedFoo(rotation: Float, modifier: Modifier = Modifier) {
            val contentKey = this.contentKey
            ElementWithValues(TestElements.Foo, modifier) {
                val animatedRotation by animateElementFloatAsState(rotation, TestValues.Value1)
                Box(
                    Modifier.graphicsLayer {
                        rotationZ =
                            animatedRotation.also {
                                lastGraphicsLayerRotationPerContent[contentKey] = it
                            }
                    }
                )
            }
        }

        rule.setContent {
            val scope = rememberCoroutineScope()
            val state = remember {
                MutableSceneTransitionLayoutStateForTests(SceneA).apply {
                    startTransitionImmediately(
                        scope,
                        transition(SceneA, SceneB, progress = { 0.5f }),
                    )
                }
            }
            SceneTransitionLayout(state) {
                scene(SceneA) { SharedFoo(rotation = 0f, Modifier.fillMaxSize()) }
                scene(SceneB) { SharedFoo(rotation = 180f, Modifier.fillMaxSize(0.5f)) }
            }
        }

        // SharedFoo is only placed in sceneB, the scene with highest zIndex.
        assertThat(lastGraphicsLayerRotationPerContent[SceneB]).isWithin(0.1f).of(90f)
    }
}