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

Commit 1016e95f authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge changes I6b4d256d,Ib62baf67,Ia9427c81 into main

* changes:
  Rename Key.name to Key.debugName
  Add support for movable elements
  Revert "[flexiglass] Added isUserInputOngoing to transition model"
parents f7f16510 028dbf43
Loading
Loading
Loading
Loading
+4 −19
Original line number Diff line number Diff line
@@ -108,7 +108,7 @@ private fun CoroutineScope.animate(
) {
    val fromScene = layoutImpl.state.transitionState.currentScene
    val isUserInput =
        (layoutImpl.state.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput
        (layoutImpl.state.transitionState as? TransitionState.Transition)?.isUserInputDriven
            ?: false

    val animationSpec = layoutImpl.transitions.transitionSpec(fromScene, target).spec
@@ -119,23 +119,9 @@ private fun CoroutineScope.animate(
    val targetProgress = if (reversed) 0f else 1f
    val transition =
        if (reversed) {
            OneOffTransition(
                fromScene = target,
                toScene = fromScene,
                currentScene = target,
                isUserInput,
                isUserInputOngoing = false,
                animatable,
            )
            OneOffTransition(target, fromScene, currentScene = target, isUserInput, animatable)
        } else {
            OneOffTransition(
                fromScene = fromScene,
                toScene = target,
                currentScene = target,
                isUserInput,
                isUserInputOngoing = false,
                animatable,
            )
            OneOffTransition(fromScene, target, currentScene = target, isUserInput, animatable)
        }

    // Change the current layout state to use this new transition.
@@ -156,8 +142,7 @@ private class OneOffTransition(
    override val fromScene: SceneKey,
    override val toScene: SceneKey,
    override val currentScene: SceneKey,
    override val isInitiatedByUserInput: Boolean,
    override val isUserInputOngoing: Boolean,
    override val isUserInputDriven: Boolean,
    private val animatable: Animatable<Float, AnimationVector1D>,
) : TransitionState.Transition {
    override val progress: Float
+12 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -59,6 +60,17 @@ internal class Element(val key: ElementKey) {
    /** The mapping between a scene and the values/state this element has in that scene, if any. */
    val sceneValues = SnapshotStateMap<SceneKey, TargetValues>()

    /**
     * The movable content of this element, if this element is composed using
     * [SceneScope.MovableElement].
     */
    val movableContent by
        // This is only accessed from the composition (main) thread, so no need to use the default
        // lock of lazy {} to synchronize.
        lazy(mode = LazyThreadSafetyMode.NONE) {
            movableContentOf { content: @Composable () -> Unit -> content() }
        }

    override fun toString(): String {
        return "Element(key=$key)"
    }
+5 −5
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ import androidx.annotation.VisibleForTesting
 * A base class to create unique keys, associated to an [identity] that is used to check the
 * equality of two key instances.
 */
sealed class Key(val name: String, val identity: Any) {
sealed class Key(val debugName: String, val identity: Any) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (this.javaClass != other?.javaClass) return false
@@ -34,7 +34,7 @@ sealed class Key(val name: String, val identity: Any) {
    }

    override fun toString(): String {
        return "Key(name=$name)"
        return "Key(debugName=$debugName)"
    }
}

@@ -49,7 +49,7 @@ class SceneKey(
    val rootElementKey = ElementKey(name, identity)

    override fun toString(): String {
        return "SceneKey(name=$name)"
        return "SceneKey(debugName=$debugName)"
    }
}

@@ -71,7 +71,7 @@ class ElementKey(
    }

    override fun toString(): String {
        return "ElementKey(name=$name)"
        return "ElementKey(debugName=$debugName)"
    }

    companion object {
@@ -89,6 +89,6 @@ class ElementKey(
/** Key for a shared value of an element. */
class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) {
    override fun toString(): String {
        return "ValueKey(name=$name)"
        return "ValueKey(debugName=$debugName)"
    }
}
+180 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.animation.scene

import android.graphics.Picture
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize

private const val TAG = "MovableElement"

private object MovableElementScopeImpl : MovableElementScope

@Composable
internal fun MovableElement(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: Scene,
    key: ElementKey,
    modifier: Modifier,
    content: @Composable MovableElementScope.() -> Unit,
) {
    Box(modifier.element(layoutImpl, scene, key)) {
        // Get the Element from the map. It will always be the same and we don't want to recompose
        // every time an element is added/removed from SceneTransitionLayoutImpl.elements, so we
        // disable read observation during the look-up in that map.
        val element = Snapshot.withoutReadObservation { layoutImpl.elements.getValue(key) }

        // The [Picture] to which we save the last drawing commands of this element. This is
        // necessary because the content of this element might not be composed in this scene, in
        // which case we still need to draw it.
        val picture = remember { Picture() }

        if (shouldComposeMovableElement(layoutImpl, scene.key, element)) {
            Box(
                Modifier.drawWithCache {
                    val width = size.width.toInt()
                    val height = size.height.toInt()

                    onDrawWithContent {
                        // Save the draw commands into [picture] for later to draw the last content
                        // even when this movable content is not composed.
                        val pictureCanvas = Canvas(picture.beginRecording(width, height))
                        draw(this, this.layoutDirection, pictureCanvas, this.size) {
                            this@onDrawWithContent.drawContent()
                        }
                        picture.endRecording()

                        // Draw the content.
                        drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
                    }
                }
            ) {
                element.movableContent { MovableElementScopeImpl.content() }
            }
        } else {
            // If we are not composed, we draw the previous drawing commands at the same size as the
            // movable content when it was composed in this scene.
            val sceneValues = element.sceneValues.getValue(scene.key)

            Spacer(
                Modifier.layout { measurable, _ ->
                        val size =
                            sceneValues.targetSize.takeIf { it != Element.SizeUnspecified }
                                ?: IntSize.Zero
                        val placeable =
                            measurable.measure(Constraints.fixed(size.width, size.height))
                        layout(size.width, size.height) { placeable.place(0, 0) }
                    }
                    .drawBehind {
                        drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
                    }
            )
        }
    }
}

private fun shouldComposeMovableElement(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: SceneKey,
    element: Element,
): Boolean {
    val transitionState = layoutImpl.state.transitionState

    // If we are idle, there is only one [scene] that is composed so we can compose our movable
    // content here.
    if (transitionState is TransitionState.Idle) {
        check(transitionState.currentScene == scene)
        return true
    }

    val fromScene = (transitionState as TransitionState.Transition).fromScene
    val toScene = transitionState.toScene
    if (fromScene == toScene) {
        check(fromScene == scene)
        return true
    }

    val fromReady = layoutImpl.isSceneReady(fromScene)
    val toReady = layoutImpl.isSceneReady(toScene)

    val otherScene =
        when (scene) {
            fromScene -> toScene
            toScene -> fromScene
            else ->
                error(
                    "shouldComposeMovableElement(scene=$scene) called with fromScene=$fromScene " +
                        "and toScene=$toScene"
                )
        }

    val isShared = otherScene in element.sceneValues

    if (isShared && !toReady && !fromReady) {
        // This should usually not happen given that fromScene should be ready, but let's log a
        // warning here in case it does so it helps debugging flicker issues caused by this part of
        // the code.
        Log.w(
            TAG,
            "MovableElement $element might have to be composed for the first time in both " +
                "fromScene=$fromScene and toScene=$toScene. This will probably lead to a flicker " +
                "where the size of the element will jump from IntSize.Zero to its actual size " +
                "during the transition."
        )
    }

    // Element is not shared in this transition.
    if (!isShared) {
        return true
    }

    // toScene is not ready (because we are composing it for the first time), so we compose it there
    // first. This is the most common scenario when starting a transition that has a shared movable
    // element.
    if (!toReady) {
        return scene == toScene
    }

    // This should usually not happen, but if we are also composing for the first time in fromScene
    // then we should compose it there only.
    if (!fromReady) {
        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
    }
}
+2 −10
Original line number Diff line number Diff line
@@ -52,14 +52,7 @@ sealed class ObservableTransitionState {
         * scene, this value will remain true after the pointer is no longer touching the screen and
         * will be true in any transition created to animate back to the original position.
         */
        val isInitiatedByUserInput: Boolean,

        /**
         * Whether user input is currently driving the transition. For example, if a user is
         * dragging a pointer, this emits true. Once they lift their finger, this emits false while
         * the transition completes/settles.
         */
        val isUserInputOngoing: Flow<Boolean>,
        val isUserInputDriven: Boolean,
    ) : ObservableTransitionState()
}

@@ -80,8 +73,7 @@ fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTrans
                            fromScene = state.fromScene,
                            toScene = state.toScene,
                            progress = snapshotFlow { state.progress },
                            isInitiatedByUserInput = state.isInitiatedByUserInput,
                            isUserInputOngoing = snapshotFlow { state.isUserInputOngoing },
                            isUserInputDriven = state.isUserInputDriven,
                        )
                    }
                }
Loading