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

Commit 4af3b89e authored by Android Build Coastguard Worker's avatar Android Build Coastguard Worker
Browse files

Snap for 13518610 from 4876737b to 25Q3-release

Change-Id: Ia620b4ca677f6119ca415b3257c7bf1aaea4574a
parents fe22e5b5 4876737b
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -25,6 +25,8 @@ android_library {
        "src/**/*.kt",
    ],
    static_libs: [
        "PlatformComposeCore",
        "PlatformComposeSceneTransitionLayout",
        "//frameworks/libs/systemui/mechanics:mechanics",
        "platform-test-annotations",
        "PlatformMotionTestingCompose",
+229 −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 com.android.mechanics.compose.modifier

import androidx.compose.ui.Modifier
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.Placeable
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.node.ModifierNodeElement
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.util.fastCoerceAtLeast
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
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.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.effectsMotionSpec
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

/**
 * This component remains hidden until it reach its target height.
 *
 * TODO: Once b/413283893 is done, [motionBuilderContext] can be read internally via
 *   CompositionLocalConsumerModifierNode, instead of passing it.
 */
fun Modifier.verticalFadeContentReveal(
    contentScope: ContentScope,
    motionBuilderContext: MotionBuilderContext,
    container: ElementKey,
    deltaY: Float = 0f,
    label: String? = null,
    debug: Boolean = false,
): Modifier =
    this then
        FadeContentRevealElement(
            contentScope = contentScope,
            motionBuilderContext = motionBuilderContext,
            container = container,
            deltaY = deltaY,
            label = label,
            debug = debug,
        )

private data class FadeContentRevealElement(
    val contentScope: ContentScope,
    val motionBuilderContext: MotionBuilderContext,
    val container: ElementKey,
    val deltaY: Float,
    val label: String?,
    val debug: Boolean,
) : ModifierNodeElement<FadeContentRevealNode>() {
    override fun create(): FadeContentRevealNode =
        FadeContentRevealNode(
            contentScope = contentScope,
            motionBuilderContext = motionBuilderContext,
            container = container,
            deltaY = deltaY,
            label = label,
            debug = debug,
        )

    override fun update(node: FadeContentRevealNode) {
        node.update(
            contentScope = contentScope,
            motionBuilderContext = motionBuilderContext,
            container = container,
            deltaY = deltaY,
        )
    }

    override fun InspectorInfo.inspectableProperties() {
        name = "fadeContentReveal"
        properties["container"] = container
        properties["deltaY"] = deltaY
        properties["label"] = label
        properties["debug"] = debug
    }
}

internal 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 {

    private val motionValue =
        MotionValue(
            currentInput = {
                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
                }
            },
            gestureContext = contentScope.gestureContextOrDefault(),
            label = "FadeContentReveal(${label.orEmpty()})",
        )

    fun update(
        contentScope: ContentScope,
        motionBuilderContext: MotionBuilderContext,
        container: ElementKey,
        deltaY: Float,
    ) {
        this.contentScope = contentScope
        this.motionBuilderContext = motionBuilderContext
        this.container = container
        this.deltaY = deltaY
        updateMotionSpec()
    }

    private var motionValueJob: Job? = null

    override fun onAttach() {
        motionValueJob =
            coroutineScope.launch {
                val disposableHandle =
                    if (debug) {
                        findMotionValueDebugger()?.register(motionValue)
                    } else {
                        null
                    }
                try {
                    motionValue.keepRunning()
                } finally {
                    disposableHandle?.dispose()
                }
            }
    }

    override fun onDetach() {
        motionValueJob?.cancel()
    }

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

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

    override fun Placeable.PlacementScope.isPlacementApproachInProgress(
        lookaheadCoordinates: LayoutCoordinates
    ) = isAnimating()

    private var targetBounds = Rect.Zero

    private var lastCoordinates: LayoutCoordinates? = null

    private fun updateMotionSpec() {
        motionValue.spec =
            motionBuilderContext.effectsMotionSpec(Mapping.Zero) {
                after(targetBounds.bottom, FixedValue.One)
            }
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val placeable = measurable.measure(constraints)
        return layout(placeable.width, placeable.height) {
            val coordinates = coordinates
            if (isLookingAhead && coordinates != null) {
                lastCoordinates = coordinates
                val bounds = coordinates.boundsInParent()
                if (targetBounds != bounds) {
                    targetBounds = bounds
                    updateMotionSpec()
                }
            }
            placeable.place(IntOffset.Zero)
        }
    }

    override fun ApproachMeasureScope.approachMeasure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        return measurable.measure(constraints).run {
            layout(width, height) {
                val revealAlpha = motionValue.output
                if (revealAlpha < 1) {
                    placeWithLayer(IntOffset.Zero) {
                        alpha = revealAlpha.fastCoerceAtLeast(0f)
                        compositingStrategy = CompositingStrategy.ModulateAlpha
                    }
                } else {
                    place(IntOffset.Zero)
                }
            }
        }
    }
}
+250 −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 com.android.mechanics.compose.modifier

import androidx.compose.ui.Modifier
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.Placeable
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.node.ModifierNodeElement
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.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.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.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.spatialMotionSpec
import kotlin.math.roundToInt
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

/**
 * This component remains hidden until its target height meets a minimum threshold. At that point,
 * it reveals itself by animating its height from 0 to the current target height.
 *
 * TODO: Once b/413283893 is done, [motionBuilderContext] can be read internally via
 *   CompositionLocalConsumerModifierNode, instead of passing it.
 */
fun Modifier.verticalTactileSurfaceReveal(
    contentScope: ContentScope,
    motionBuilderContext: MotionBuilderContext,
    container: ElementKey,
    deltaY: Float = 0f,
    revealOnThreshold: RevealOnThreshold = DefaultRevealOnThreshold,
    label: String? = null,
    debug: Boolean = false,
): Modifier =
    this then
        VerticalTactileSurfaceRevealElement(
            contentScope = contentScope,
            motionBuilderContext = motionBuilderContext,
            container = container,
            deltaY = deltaY,
            revealOnThreshold = revealOnThreshold,
            label = label,
            debug = debug,
        )

private val DefaultRevealOnThreshold = RevealOnThreshold()

private data class VerticalTactileSurfaceRevealElement(
    val contentScope: ContentScope,
    val motionBuilderContext: MotionBuilderContext,
    val container: ElementKey,
    val deltaY: Float,
    val revealOnThreshold: RevealOnThreshold,
    val label: String?,
    val debug: Boolean,
) : ModifierNodeElement<VerticalTactileSurfaceRevealNode>() {
    override fun create(): VerticalTactileSurfaceRevealNode =
        VerticalTactileSurfaceRevealNode(
            contentScope = contentScope,
            motionBuilderContext = motionBuilderContext,
            container = container,
            deltaY = deltaY,
            revealOnThreshold = revealOnThreshold,
            label = label,
            debug = debug,
        )

    override fun update(node: VerticalTactileSurfaceRevealNode) {
        node.update(
            contentScope = contentScope,
            motionBuilderContext = motionBuilderContext,
            container = container,
            deltaY = deltaY,
            revealOnThreshold = revealOnThreshold,
        )
    }

    override fun InspectorInfo.inspectableProperties() {
        name = "tactileSurfaceReveal"
        properties["container"] = container
        properties["deltaY"] = deltaY
        properties["revealOnThreshold"] = revealOnThreshold
        properties["label"] = label
        properties["debug"] = debug
    }
}

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

    private val motionValue =
        MotionValue(
            currentInput = {
                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
                }
            },
            gestureContext = contentScope.gestureContextOrDefault(),
            label = "TactileSurfaceReveal(${label.orEmpty()})",
            stableThreshold = MotionBuilderContext.StableThresholdSpatial,
        )

    fun update(
        contentScope: ContentScope,
        motionBuilderContext: MotionBuilderContext,
        container: ElementKey,
        deltaY: Float,
        revealOnThreshold: RevealOnThreshold,
    ) {
        this.contentScope = contentScope
        this.motionBuilderContext = motionBuilderContext
        this.container = container
        this.deltaY = deltaY
        this.revealOnThreshold = revealOnThreshold
        updateMotionSpec()
    }

    private var motionValueJob: Job? = null

    override fun onAttach() {
        motionValueJob =
            coroutineScope.launch {
                val disposableHandle =
                    if (debug) {
                        findMotionValueDebugger()?.register(motionValue)
                    } else {
                        null
                    }
                try {
                    motionValue.keepRunning()
                } finally {
                    disposableHandle?.dispose()
                }
            }
    }

    override fun onDetach() {
        motionValueJob?.cancel()
    }

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

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

    override fun Placeable.PlacementScope.isPlacementApproachInProgress(
        lookaheadCoordinates: LayoutCoordinates
    ) = isAnimating()

    private var targetBounds = Rect.Zero

    private var lastCoordinates: LayoutCoordinates? = null

    private fun updateMotionSpec() {
        motionValue.spec =
            motionBuilderContext.spatialMotionSpec(Mapping.Zero) {
                between(
                    start = targetBounds.top,
                    end = targetBounds.bottom,
                    effect = revealOnThreshold,
                )
            }
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val placeable = measurable.measure(constraints)
        return layout(placeable.width, placeable.height) {
            val coordinates = coordinates
            if (isLookingAhead && coordinates != null) {
                lastCoordinates = coordinates
                val bounds = coordinates.boundsInParent()
                if (targetBounds != bounds) {
                    targetBounds = bounds
                    updateMotionSpec()
                }
            }
            placeable.place(IntOffset.Zero)
        }
    }

    override fun ApproachMeasureScope.approachMeasure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val height = motionValue.output.roundToInt().fastCoerceAtLeast(0)
        val animatedConstraints = Constraints.fixed(width = constraints.maxWidth, height = height)
        return measurable.measure(animatedConstraints).run {
            layout(width, height) {
                val revealAlpha = (height / revealOnThreshold.minSize.toPx()).fastCoerceIn(0f, 1f)
                if (revealAlpha < 1) {
                    placeWithLayer(IntOffset.Zero) {
                        alpha = revealAlpha
                        compositingStrategy = CompositingStrategy.ModulateAlpha
                    }
                } else {
                    place(IntOffset.Zero)
                }
            }
        }
    }
}
+56 −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 com.android.mechanics.effects

import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceAtMost
import com.android.mechanics.spec.BreakpointKey
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.builder.Effect
import com.android.mechanics.spec.builder.EffectApplyScope
import com.android.mechanics.spec.builder.EffectPlacement

/** An effect that reveals a component when the available space reaches a certain threshold. */
data class RevealOnThreshold(val minSize: Dp = Defaults.MinSize) : Effect.PlaceableBetween {
    init {
        require(minSize >= 0.dp)
    }

    override fun EffectApplyScope.createSpec(
        minLimit: Float,
        minLimitKey: BreakpointKey,
        maxLimit: Float,
        maxLimitKey: BreakpointKey,
        placement: EffectPlacement,
    ) {
        val maxSize = maxLimit - minLimit
        val minSize = minSize.toPx().fastCoerceAtMost(maxSize)

        unidirectional(initialMapping = Mapping.Zero) {
            before(mapping = Mapping.Zero)

            target(breakpoint = minLimit + minSize, from = minSize, to = maxSize)

            after(mapping = Mapping.Fixed(maxSize))
        }
    }

    object Defaults {
        val MinSize: Dp = 8.dp
    }
}
+92 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64
  ],
  "features": [
    {
      "name": "input",
      "type": "float",
      "data_points": [
        0,
        3,
        6,
        10,
        10
      ]
    },
    {
      "name": "gestureDirection",
      "type": "string",
      "data_points": [
        "Max",
        "Max",
        "Max",
        "Max",
        "Max"
      ]
    },
    {
      "name": "output",
      "type": "float",
      "data_points": [
        0,
        0,
        0,
        0,
        0
      ]
    },
    {
      "name": "outputTarget",
      "type": "float",
      "data_points": [
        0,
        0,
        0,
        0,
        0
      ]
    },
    {
      "name": "outputSpring",
      "type": "springParameters",
      "data_points": [
        {
          "stiffness": 100000,
          "dampingRatio": 1
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        },
        {
          "stiffness": 700,
          "dampingRatio": 0.9
        }
      ]
    },
    {
      "name": "isStable",
      "type": "boolean",
      "data_points": [
        true,
        true,
        true,
        true,
        true
      ]
    }
  ]
}
 No newline at end of file
Loading