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

Commit 669d20ab authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Use Transition.ReplaceOverlay.currentScene when getting placeholder size

This CL fixes the logic for the placeholder size of movable elements
during a ReplaceOverlay transition: we now use the size of the current
scene if the other scene never contained that element.

Bug: 373799480
Test: atest MovableElementTest
Flag: com.android.systemui.scene_container
Change-Id: Idc7501e80b8aa0f3cf6e7b6df424d2825cd5b737
parent 11beddbf
Loading
Loading
Loading
Loading
+8 −2
Original line number Diff line number Diff line
@@ -241,6 +241,10 @@ private fun placeholderContentSize(
        return targetValueInScene
    }

    fun TransitionState.Transition.otherContent(): ContentKey {
        return if (fromContent == content) toContent else fromContent
    }

    // If the element content was already composed in the other overlay/scene, we use that
    // target size assuming it doesn't change between scenes.
    // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is
@@ -249,8 +253,10 @@ private fun placeholderContentSize(
        when (val state = movableElementState(elementKey, transitionStates)) {
            null -> return IntSize.Zero
            is TransitionState.Idle -> movableElementContentWhenIdle(layoutImpl, elementKey, state)
            is TransitionState.Transition ->
                if (state.fromContent == content) state.toContent else state.fromContent
            is TransitionState.Transition.ReplaceOverlay -> {
                state.otherContent().takeIf { it in element.stateByContent } ?: state.currentScene
            }
            is TransitionState.Transition -> state.otherContent()
        }

    val targetValueInOtherContent = element.stateByContent[otherContent]?.targetSize
+63 −0
Original line number Diff line number Diff line
@@ -43,12 +43,17 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestOverlays.OverlayA
import com.android.compose.animation.scene.TestOverlays.OverlayB
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.test.assertSizeIsEqualTo
import com.android.compose.test.setContentAndCreateMainScope
import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -341,4 +346,62 @@ class MovableElementTest {
        rule.onNodeWithTag("bottomEnd").assertPositionInRootIsEqualTo(200.dp, 200.dp)
        rule.onNodeWithTag("matchParentSize").assertSizeIsEqualTo(200.dp, 200.dp)
    }

    @Test
    fun useCurrentSceneSizeForPlaceholderWhenReplacingOverlay() {
        val foo =
            MovableElementKey(
                "foo",

                // Always compose foo in SceneA.
                contentPicker =
                    object : StaticElementContentPicker {
                        override val contents: Set<ContentKey> = setOf(SceneA, OverlayB)

                        override fun contentDuringTransition(
                            element: ElementKey,
                            transition: TransitionState.Transition,
                            fromContentZIndex: Float,
                            toContentZIndex: Float,
                        ): ContentKey {
                            return SceneA
                        }
                    },
            )
        val fooSize = 50.dp
        val fooParentInOverlayTag = "fooParentTagInOverlay"

        @Composable
        fun SceneScope.Foo(modifier: Modifier = Modifier) {
            // Foo wraps its content, so there is no way for STL to know its size in advance.
            MovableElement(foo, modifier) { content { Box(Modifier.size(fooSize)) } }
        }

        val state =
            rule.runOnUiThread {
                MutableSceneTransitionLayoutState(
                    initialScene = SceneA,
                    initialOverlays = setOf(OverlayA),
                )
            }

        val scope =
            rule.setContentAndCreateMainScope {
                SceneTransitionLayout(state) {
                    scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                    overlay(OverlayA) { /* empty */ }
                    overlay(OverlayB) { Box(Modifier.testTag(fooParentInOverlayTag)) { Foo() } }
                }
            }

        // Start an overlay replace transition.
        scope.launch {
            state.startTransition(transition(from = OverlayA, to = OverlayB, progress = { 0.5f }))
        }

        // The parent of foo should have a correct size in OverlayB even if Foo was never composed
        // there by using the size information from SceneA.
        rule.waitForIdle()
        rule.onNodeWithTag(fooParentInOverlayTag).assertSizeIsEqualTo(fooSize)
    }
}
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.test

import androidx.compose.foundation.gestures.Orientation
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.Transition
import kotlinx.coroutines.CompletableDeferred

/** A [Transition.ShowOrHideOverlay] for tests that will be finished once [finish] is called. */
abstract class TestReplaceOverlayTransition(
    fromOverlay: OverlayKey,
    toOverlay: OverlayKey,
    replacedTransition: Transition?,
) :
    Transition.ReplaceOverlay(
        fromOverlay = fromOverlay,
        toOverlay = toOverlay,
        replacedTransition = replacedTransition,
    ) {
    private val finishCompletable = CompletableDeferred<Unit>()

    override suspend fun run() {
        finishCompletable.await()
    }

    /** Finish this transition. */
    fun finish() {
        finishCompletable.complete(Unit)
    }
}

/** A utility to easily create a [TestReplaceOverlayTransition] in tests. */
fun transition(
    from: OverlayKey,
    to: OverlayKey,
    effectivelyShownOverlay: () -> OverlayKey = { to },
    progress: () -> Float = { 0f },
    progressVelocity: () -> Float = { 0f },
    previewProgress: () -> Float = { 0f },
    previewProgressVelocity: () -> Float = { 0f },
    isInPreviewStage: () -> Boolean = { false },
    interruptionProgress: () -> Float = { 0f },
    isInitiatedByUserInput: Boolean = false,
    isUserInputOngoing: Boolean = false,
    isUpOrLeft: Boolean = false,
    bouncingContent: ContentKey? = null,
    orientation: Orientation = Orientation.Horizontal,
    onFreezeAndAnimate: ((TestReplaceOverlayTransition) -> Unit)? = null,
    replacedTransition: Transition? = null,
): TestReplaceOverlayTransition {
    return object :
        TestReplaceOverlayTransition(from, to, replacedTransition),
        TransitionState.HasOverscrollProperties {
        override val effectivelyShownOverlay: OverlayKey
            get() = effectivelyShownOverlay()

        override val progress: Float
            get() = progress()

        override val progressVelocity: Float
            get() = progressVelocity()

        override val previewProgress: Float
            get() = previewProgress()

        override val previewProgressVelocity: Float
            get() = previewProgressVelocity()

        override val isInPreviewStage: Boolean
            get() = isInPreviewStage()

        override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
        override val isUserInputOngoing: Boolean = isUserInputOngoing
        override val isUpOrLeft: Boolean = isUpOrLeft
        override val bouncingContent: ContentKey? = bouncingContent
        override val orientation: Orientation = orientation
        override val absoluteDistance = 0f

        override fun freezeAndAnimateToCurrentState() {
            if (onFreezeAndAnimate != null) {
                onFreezeAndAnimate(this)
            } else {
                finish()
            }
        }

        override fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float {
            return interruptionProgress()
        }
    }
}