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

Commit df62b8a7 authored by Omar Miatello's avatar Omar Miatello
Browse files

MM Vertical reveal set new spec when is not in a transition 1/3

Vertical reveal gradually exposes a composable element during a
transition. It's crucial that all elements are fully visible by the
transition's end, even if the animation initially hides some due to
incomplete exposure.

Test: VerticalTactileSurfaceRevealModifierTest
Test: Manually tested on dual shade with multiple scrollable page of qs
Bug: 419520966
Flag: com.android.systemui.scene_container
Change-Id: I00e72bcd89dfd2c010d9e8c4e62799d7a8d89f70
parent 35fc858d
Loading
Loading
Loading
Loading
+51 −22
Original line number Diff line number Diff line
@@ -21,25 +21,30 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toIntRect
import androidx.compose.ui.unit.toRect
import androidx.compose.ui.util.fastCoerceAtLeast
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.mechanics.gestureContextOrDefault
import com.android.mechanics.MotionValue
import com.android.mechanics.debug.findMotionValueDebugger
import com.android.mechanics.effects.FixedValue
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.directionalMotionSpec
import com.android.mechanics.spec.builder.effectsMotionSpec
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -104,14 +109,14 @@ private data class FadeContentRevealElement(
    }
}

internal class FadeContentRevealNode(
private class FadeContentRevealNode(
    private var contentScope: ContentScope,
    private var motionBuilderContext: MotionBuilderContext,
    private var container: ElementKey,
    private var deltaY: Float,
    label: String?,
    private val debug: Boolean,
) : Modifier.Node(), ApproachLayoutModifierNode {
) : Modifier.Node(), ApproachLayoutModifierNode, ObserverModifierNode {

    private val motionValue =
        MotionValue(
@@ -119,14 +124,10 @@ internal class FadeContentRevealNode(
                with(contentScope) {
                    val containerHeight =
                        container.lastSize(contentKey)?.height ?: return@MotionValue 0f
                    val containerCoordinates =
                        container.targetCoordinates(contentKey) ?: return@MotionValue 0f
                    val localCoordinates = lastCoordinates ?: return@MotionValue 0f

                    val offsetY = containerCoordinates.localPositionOf(localCoordinates).y
                    containerHeight - offsetY + deltaY
                    containerHeight + deltaY
                }
            },
            initialSpec = MotionSpec(directionalMotionSpec(Mapping.Zero)),
            gestureContext = contentScope.gestureContextOrDefault(),
            label = "FadeContentReveal(${label.orEmpty()})",
        )
@@ -141,12 +142,14 @@ internal class FadeContentRevealNode(
        this.motionBuilderContext = motionBuilderContext
        this.container = container
        this.deltaY = deltaY
        updateMotionSpec()
        updateMotionSpec(contentScope.layoutState.transitionState)
    }

    private var motionValueJob: Job? = null

    override fun onAttach() {
        onObservedReadsChanged()

        motionValueJob =
            coroutineScope.launch {
                val disposableHandle =
@@ -167,22 +170,46 @@ internal class FadeContentRevealNode(
        motionValueJob?.cancel()
    }

    private fun isAnimating(): Boolean {
        return contentScope.layoutState.currentTransition != null || !motionValue.isStable
    override fun onObservedReadsChanged() {
        observeReads { updateMotionSpec(contentScope.layoutState.transitionState) }
    }

    override fun isMeasurementApproachInProgress(lookaheadSize: IntSize) = isAnimating()

    private var targetBounds = Rect.Zero
    private var isContentTransition = false

    private var lastCoordinates: LayoutCoordinates? = null
    private fun updateMotionSpec(transitionState: TransitionState) {
        isContentTransition = transitionState is TransitionState.Transition

        val height = targetBounds.height
        if (height == 0f) {
            // We cannot compute specs for height 0.
            motionValue.spec = MotionSpec(directionalMotionSpec(Mapping.Fixed(0f)))
            return
        }

    private fun updateMotionSpec() {
        motionValue.spec =
            when (transitionState) {
                is TransitionState.Idle -> {
                    val containerMinHeight = 0
                    val currentScene = transitionState.currentScene
                    val isRevealed =
                        with(contentScope) {
                            val targetHeight = container.targetSize(currentScene)?.height ?: 0
                            targetHeight > containerMinHeight
                        }
                    MotionSpec(directionalMotionSpec(Mapping.Fixed(if (isRevealed) 1f else 0f)))
                }
                is TransitionState.Transition -> {
                    motionBuilderContext.effectsMotionSpec(Mapping.Zero) {
                        after(targetBounds.bottom, FixedValue.One)
                    }
                }
            }
    }

    override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
        return isContentTransition || !motionValue.isStable
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
@@ -192,11 +219,13 @@ internal class FadeContentRevealNode(
        return layout(placeable.width, placeable.height) {
            val coordinates = coordinates
            if (isLookingAhead && coordinates != null) {
                lastCoordinates = coordinates
                val bounds = coordinates.boundsInParent()
                val containerCoordinates =
                    with(contentScope) { container.targetCoordinates(contentKey)!! }
                val containerOffset = containerCoordinates.localPositionOf(coordinates)
                val bounds = coordinates.size.toIntRect().toRect().translate(containerOffset)
                if (targetBounds != bounds) {
                    targetBounds = bounds
                    updateMotionSpec()
                    updateMotionSpec(contentScope.layoutState.transitionState)
                }
            }
            placeable.place(IntOffset.Zero)
+55 −26
Original line number Diff line number Diff line
@@ -21,26 +21,31 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toIntRect
import androidx.compose.ui.unit.toRect
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.mechanics.gestureContextOrDefault
import com.android.mechanics.MotionValue
import com.android.mechanics.debug.findMotionValueDebugger
import com.android.mechanics.effects.RevealOnThreshold
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.directionalMotionSpec
import com.android.mechanics.spec.builder.spatialMotionSpec
import kotlin.math.roundToInt
import kotlinx.coroutines.Job
@@ -123,7 +128,7 @@ private class VerticalTactileSurfaceRevealNode(
    private var revealOnThreshold: RevealOnThreshold,
    label: String?,
    private val debug: Boolean,
) : Modifier.Node(), ApproachLayoutModifierNode {
) : Modifier.Node(), ApproachLayoutModifierNode, ObserverModifierNode {

    private val motionValue =
        MotionValue(
@@ -131,14 +136,10 @@ private class VerticalTactileSurfaceRevealNode(
                with(contentScope) {
                    val containerHeight =
                        container.lastSize(contentKey)?.height ?: return@MotionValue 0f
                    val containerCoordinates =
                        container.targetCoordinates(contentKey) ?: return@MotionValue 0f
                    val localCoordinates = lastCoordinates ?: return@MotionValue 0f

                    val offsetY = containerCoordinates.localPositionOf(localCoordinates).y
                    containerHeight - offsetY + deltaY
                    containerHeight + deltaY
                }
            },
            initialSpec = MotionSpec(directionalMotionSpec(Mapping.Zero)),
            gestureContext = contentScope.gestureContextOrDefault(),
            label = "TactileSurfaceReveal(${label.orEmpty()})",
            stableThreshold = MotionBuilderContext.StableThresholdSpatial,
@@ -156,12 +157,14 @@ private class VerticalTactileSurfaceRevealNode(
        this.container = container
        this.deltaY = deltaY
        this.revealOnThreshold = revealOnThreshold
        updateMotionSpec()
        updateMotionSpec(contentScope.layoutState.transitionState)
    }

    private var motionValueJob: Job? = null

    override fun onAttach() {
        onObservedReadsChanged()

        motionValueJob =
            coroutineScope.launch {
                val disposableHandle =
@@ -182,18 +185,36 @@ private class VerticalTactileSurfaceRevealNode(
        motionValueJob?.cancel()
    }

    private fun isAnimating(): Boolean {
        return contentScope.layoutState.currentTransition != null || !motionValue.isStable
    override fun onObservedReadsChanged() {
        observeReads { updateMotionSpec(contentScope.layoutState.transitionState) }
    }

    override fun isMeasurementApproachInProgress(lookaheadSize: IntSize) = isAnimating()

    private var targetBounds = Rect.Zero
    private var isContentTransition = false

    private var lastCoordinates: LayoutCoordinates? = null
    private fun updateMotionSpec(transitionState: TransitionState) {
        isContentTransition = transitionState is TransitionState.Transition

        val height = targetBounds.height
        if (height == 0f) {
            // We cannot compute specs for height 0.
            motionValue.spec = MotionSpec(directionalMotionSpec(Mapping.Fixed(0f)))
            return
        }

    private fun updateMotionSpec() {
        motionValue.spec =
            when (transitionState) {
                is TransitionState.Idle -> {
                    val containerMinHeight = 0
                    val currentScene = transitionState.currentScene
                    val isRevealed =
                        with(contentScope) {
                            val targetHeight = container.targetSize(currentScene)?.height ?: 0
                            targetHeight > containerMinHeight
                        }
                    MotionSpec(directionalMotionSpec(Mapping.Fixed(if (isRevealed) height else 0f)))
                }
                is TransitionState.Transition -> {
                    motionBuilderContext.spatialMotionSpec(Mapping.Zero) {
                        between(
                            start = targetBounds.top,
@@ -202,6 +223,12 @@ private class VerticalTactileSurfaceRevealNode(
                        )
                    }
                }
            }
    }

    override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
        return isContentTransition || !motionValue.isStable
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
@@ -211,11 +238,13 @@ private class VerticalTactileSurfaceRevealNode(
        return layout(placeable.width, placeable.height) {
            val coordinates = coordinates
            if (isLookingAhead && coordinates != null) {
                lastCoordinates = coordinates
                val bounds = coordinates.boundsInParent()
                val containerCoordinates =
                    with(contentScope) { container.targetCoordinates(contentKey)!! }
                val containerOffset = containerCoordinates.localPositionOf(coordinates)
                val bounds = coordinates.size.toIntRect().toRect().translate(containerOffset)
                if (targetBounds != bounds) {
                    targetBounds = bounds
                    updateMotionSpec()
                    updateMotionSpec(contentScope.layoutState.transitionState)
                }
            }
            placeable.place(IntOffset.Zero)
@@ -227,7 +256,7 @@ private class VerticalTactileSurfaceRevealNode(
        constraints: Constraints,
    ): MeasureResult {
        val height = motionValue.output.roundToInt().fastCoerceAtLeast(0)
        val animatedConstraints = Constraints.fixed(width = constraints.maxWidth, height = height)
        val animatedConstraints = constraints.copy(maxHeight = height)
        return measurable.measure(animatedConstraints).run {
            layout(width, height) {
                val revealAlpha = (height / revealOnThreshold.minSize.toPx()).fastCoerceIn(0f, 1f)
+52 −0
Original line number Diff line number Diff line
// Copyright (C) 2025 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 {
    default_team: "trendy_team_motion",
    default_applicable_licenses: ["Android-Apache-2.0"],
}

android_test {
    name: "mechanics-compose_tests",
    manifest: "AndroidManifest.xml",
    defaults: ["MotionTestDefaults"],
    test_suites: ["device-tests"],

    srcs: [
        "src/**/*.kt",
    ],

    static_libs: [
        "//frameworks/libs/systemui/mechanics:mechanics",
        "//frameworks/libs/systemui/mechanics:mechanics-compose",
        "//frameworks/libs/systemui/mechanics:mechanics-testing",
        "PlatformComposeSceneTransitionLayoutTestsUtils",
        "platform-test-annotations",
        "PlatformMotionTestingCompose",
        "androidx.compose.runtime_runtime",
        "androidx.compose.animation_animation-core",
        "androidx.compose.ui_ui-test-junit4",
        "androidx.compose.ui_ui-test-manifest",
        "androidx.test.runner",
        "androidx.test.ext.junit",
        "kotlin-test",
        "testables",
        "truth",
    ],
    associates: [
        "mechanics-compose",
    ],
    asset_dirs: ["goldens"],
    kotlincflags: ["-Xjvm-default=all"],
}
+1288 −0

File added.

Preview size limit exceeded, changes collapsed.

+1390 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading