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

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

Merge "Fix MovableElements during overscroll on overlays" into main

parents 5670baa0 16754b37
Loading
Loading
Loading
Loading
+18 −5
Original line number Diff line number Diff line
@@ -853,19 +853,32 @@ private fun shouldPlaceElement(
        content,
        element.key,
        transition,
        isInContent = { it in element.stateByContent },
    )
}

internal fun shouldPlaceOrComposeSharedElement(
internal inline fun shouldPlaceOrComposeSharedElement(
    layoutImpl: SceneTransitionLayoutImpl,
    content: ContentKey,
    element: ElementKey,
    transition: TransitionState.Transition,
    isInContent: (ContentKey) -> Boolean,
): Boolean {
    // If we are overscrolling, only place/compose the element in the overscrolling scene.
    val overscrollScene = transition.currentOverscrollSpec?.content
    if (overscrollScene != null) {
        return content == overscrollScene
    val overscrollContent = transition.currentOverscrollSpec?.content
    if (overscrollContent != null) {
        return when (transition) {
            // If we are overscrolling between scenes, only place/compose the element in the
            // overscrolling scene.
            is TransitionState.Transition.ChangeScene -> content == overscrollContent

            // If we are overscrolling an overlay, place/compose the element if [content] is the
            // overscrolling content or if [content] is the current scene and the overscrolling
            // overlay does not contain the element.
            is TransitionState.Transition.ReplaceOverlay,
            is TransitionState.Transition.ShowOrHideOverlay ->
                content == overscrollContent ||
                    (content == transition.currentScene && !isInContent(overscrollContent))
        }
    }

    val scenePicker = element.contentPicker
+4 −2
Original line number Diff line number Diff line
@@ -194,11 +194,13 @@ private fun shouldComposeMovableElement(
        is TransitionState.Transition -> {
            // During transitions, always compose movable elements in the scene picked by their
            // content picker.
            val contents = element.contentPicker.contents
            shouldPlaceOrComposeSharedElement(
                layoutImpl,
                content,
                element,
                elementState,
                isInContent = { contents.contains(it) }
            )
        }
    }
@@ -208,8 +210,8 @@ private fun movableElementState(
    element: MovableElementKey,
    transitionStates: List<TransitionState>,
): TransitionState? {
    val content = element.contentPicker.contents
    return elementState(transitionStates, isInContent = { content.contains(it) })
    val contents = element.contentPicker.contents
    return elementState(transitionStates, isInContent = { contents.contains(it) })
}

private fun movableElementContentWhenIdle(
+42 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.compose.animation.scene

import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -44,9 +45,12 @@ 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.test.assertSizeIsEqualTo
import com.android.compose.test.setContentAndCreateMainScope
import com.android.compose.test.subjects.assertThat
import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -648,4 +652,42 @@ class OverlayTest {
            }
        }
    }

    @Test
    fun overscrollingOverlay_movableElementNotInOverlay() {
        val state =
            rule.runOnUiThread {
                MutableSceneTransitionLayoutStateImpl(
                    SceneA,
                    transitions {
                        // Make OverlayA overscrollable.
                        overscroll(OverlayA, orientation = Orientation.Horizontal) {
                            translate(ElementKey("elementThatDoesNotExist"), x = 10.dp)
                        }
                    }
                )
            }

        val key = MovableElementKey("Foo", contents = setOf(SceneA))
        val movableElementChildTag = "movableElementChildTag"
        val scope =
            rule.setContentAndCreateMainScope {
                SceneTransitionLayout(state) {
                    scene(SceneA) {
                        MovableElement(key, Modifier) {
                            content { Box(Modifier.testTag(movableElementChildTag).size(100.dp)) }
                        }
                    }
                    overlay(OverlayA) { /* Does not contain the element. */ }
                }
            }

        // Overscroll on Overlay A.
        scope.launch { state.startTransition(transition(SceneA, OverlayA, progress = { 1.5f })) }
        rule
            .onNode(hasTestTag(movableElementChildTag) and inContent(SceneA))
            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
            .assertSizeIsEqualTo(100.dp)
            .assertIsDisplayed()
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.animation.scene.transition.link.StateLink
import com.android.compose.animation.scene.transition.seekToScene
import com.android.compose.test.MonotonicClockTestScope
import com.android.compose.test.TestTransition
import com.android.compose.test.TestSceneTransition
import com.android.compose.test.runMonotonicClockTest
import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
@@ -556,8 +556,8 @@ class SceneTransitionLayoutStateTest {

    @Test
    fun multipleTransitions() = runTest {
        val frozenTransitions = mutableSetOf<TestTransition>()
        fun onFreezeAndAnimate(transition: TestTransition): () -> Unit {
        val frozenTransitions = mutableSetOf<TestSceneTransition>()
        fun onFreezeAndAnimate(transition: TestSceneTransition): () -> Unit {
            // Instead of letting the transition finish when it is frozen, we put the transition in
            // the frozenTransitions set so that we can verify that freezeAndAnimateToCurrentState()
            // is called when expected and then we call finish() ourselves to finish the
+112 −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.SceneKey
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 TestOverlayTransition(
    fromScene: SceneKey,
    overlay: OverlayKey,
    replacedTransition: Transition?,
) :
    Transition.ShowOrHideOverlay(
        overlay = overlay,
        fromOrToScene = fromScene,
        fromContent = fromScene,
        toContent = overlay,
        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 [TestOverlayTransition] in tests. */
fun transition(
    fromScene: SceneKey,
    overlay: OverlayKey,
    isEffectivelyShown: () -> Boolean = { true },
    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: ((TestOverlayTransition) -> Unit)? = null,
    replacedTransition: Transition? = null,
): TestOverlayTransition {
    return object :
        TestOverlayTransition(fromScene, overlay, replacedTransition),
        TransitionState.HasOverscrollProperties {
        override val isEffectivelyShown: Boolean
            get() = isEffectivelyShown()

        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()
        }
    }
}
Loading