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

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

Merge changes from topic "stl-no-modifier-transformation" into main

* changes:
  Migrate Modifier.multiPointerDraggable to the Node Modifier API
  Annotate Stable classes/interfaces to optimize compositions
  Move Element drawing logic in ElementNode
  Remove Modifier transformations (1/2)
  Move PunchHole.kt to the animation/scene/ directory
parents 4c8ea0d6 6f2454eb
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -10,7 +10,6 @@ import com.android.systemui.shade.ui.composable.Shade
fun TransitionBuilder.lockscreenToShadeTransition() {
    spec = tween(durationMillis = 500)

    punchHole(Shade.Elements.QuickSettings, bounds = Shade.Elements.Scrim, Shade.Shapes.Scrim)
    translate(Shade.Elements.Scrim, Edge.Top, startsOutsideLayoutBounds = false)
    fractionRange(end = 0.5f) {
        fade(Shade.Elements.ScrimBackground)
+35 −55
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
@@ -25,16 +26,17 @@ import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.layout.IntermediateMeasureScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Constraints
@@ -46,6 +48,7 @@ import com.android.compose.ui.util.lerp
import kotlinx.coroutines.launch

/** An element on screen, that can be composed in one or more scenes. */
@Stable
internal class Element(val key: ElementKey) {
    /**
     * The last values of this element, coming from any scene. Note that this value will be unstable
@@ -90,6 +93,7 @@ internal class Element(val key: ElementKey) {
    }

    /** The target values of this element in a given scene. */
    @Stable
    class TargetValues(val scene: SceneKey) {
        val lastValues = Values()

@@ -107,6 +111,7 @@ internal class Element(val key: ElementKey) {
    }

    /** A shared value of this element. */
    @Stable
    class SharedValue<T>(val key: ValueKey, initialValue: T) {
        var value by mutableStateOf(initialValue)
    }
@@ -126,6 +131,7 @@ data class Scale(val scaleX: Float, val scaleY: Float, val pivot: Offset = Offse

/** The implementation of [SceneScope.element]. */
@OptIn(ExperimentalComposeUiApi::class)
@Stable
internal fun Modifier.element(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: Scene,
@@ -144,24 +150,9 @@ internal fun Modifier.element(
                ?: Element.TargetValues(scene.key).also { element.sceneValues[scene.key] = it }
    }

    return this.then(ElementModifier(layoutImpl, element, sceneValues))
        .drawWithContent {
            if (shouldDrawElement(layoutImpl, scene, element)) {
                val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
                if (drawScale == Scale.Default) {
                    drawContent()
                } else {
                    scale(
                        drawScale.scaleX,
                        drawScale.scaleY,
                        if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
                    ) {
                        this@drawWithContent.drawContent()
                    }
                }
            }
        }
        .modifierTransformations(layoutImpl, scene, element, sceneValues)
    return this.then(ElementModifier(layoutImpl, scene, element, sceneValues))
        // TODO(b/311132415): Move this into ElementNode once we can create a delegate
        // IntermediateLayoutModifierNode.
        .intermediateLayout { measurable, constraints ->
            val placeable =
                measure(layoutImpl, scene, element, sceneValues, measurable, constraints)
@@ -178,22 +169,25 @@ internal fun Modifier.element(
 */
private data class ElementModifier(
    private val layoutImpl: SceneTransitionLayoutImpl,
    private val scene: Scene,
    private val element: Element,
    private val sceneValues: Element.TargetValues,
) : ModifierNodeElement<ElementNode>() {
    override fun create(): ElementNode = ElementNode(layoutImpl, element, sceneValues)
    override fun create(): ElementNode = ElementNode(layoutImpl, scene, element, sceneValues)

    override fun update(node: ElementNode) {
        node.update(layoutImpl, element, sceneValues)
        node.update(layoutImpl, scene, element, sceneValues)
    }
}

internal class ElementNode(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: Scene,
    element: Element,
    sceneValues: Element.TargetValues,
) : Modifier.Node() {
) : Modifier.Node(), DrawModifierNode {
    private var layoutImpl: SceneTransitionLayoutImpl = layoutImpl
    private var scene: Scene = scene
    private var element: Element = element
    private var sceneValues: Element.TargetValues = sceneValues

@@ -239,15 +233,34 @@ internal class ElementNode(

    fun update(
        layoutImpl: SceneTransitionLayoutImpl,
        scene: Scene,
        element: Element,
        sceneValues: Element.TargetValues,
    ) {
        removeNodeFromSceneValues()
        this.layoutImpl = layoutImpl
        this.scene = scene
        this.element = element
        this.sceneValues = sceneValues
        addNodeToSceneValues()
    }

    override fun ContentDrawScope.draw() {
        if (shouldDrawElement(layoutImpl, scene, element)) {
            val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
            if (drawScale == Scale.Default) {
                drawContent()
            } else {
                scale(
                    drawScale.scaleX,
                    drawScale.scaleY,
                    if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
                ) {
                    this@draw.drawContent()
                }
            }
        }
    }
}

private fun shouldDrawElement(
@@ -331,39 +344,6 @@ internal fun sharedElementTransformation(
    return sharedInFromScene
}

/**
 * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
 * throughout the current transition, if any.
 */
private fun Modifier.modifierTransformations(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: Scene,
    element: Element,
    sceneValues: Element.TargetValues,
): Modifier {
    when (val state = layoutImpl.state.transitionState) {
        is TransitionState.Idle -> return this
        is TransitionState.Transition -> {
            val fromScene = state.fromScene
            val toScene = state.toScene
            if (fromScene == toScene) {
                // Same as idle.
                return this
            }

            return layoutImpl.transitions
                .transitionSpec(fromScene, state.toScene)
                .transformations(element.key, scene.key)
                .modifier
                .fold(this) { modifier, transformation ->
                    with(transformation) {
                        modifier.transform(layoutImpl, scene, element, sceneValues)
                    }
                }
        }
    }
}

/**
 * Whether the element is opaque or not.
 *
+2 −0
Original line number Diff line number Diff line
@@ -17,11 +17,13 @@
package com.android.compose.animation.scene

import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Stable

/**
 * A base class to create unique keys, associated to an [identity] that is used to check the
 * equality of two key instances.
 */
@Stable
sealed class Key(val debugName: String, val identity: Any) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
+94 −17
Original line number Diff line number Diff line
@@ -23,20 +23,25 @@ import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellati
import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.horizontalDrag
import androidx.compose.foundation.gestures.verticalDrag
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
@@ -56,7 +61,7 @@ import androidx.compose.ui.util.fastForEach
 * dragged) and a second pointer is down and dragged. This is an implementation detail that might
 * change in the future.
 */
// TODO(b/291055080): Migrate to the Modifier.Node API.
@Stable
internal fun Modifier.multiPointerDraggable(
    orientation: Orientation,
    enabled: Boolean,
@@ -64,22 +69,88 @@ internal fun Modifier.multiPointerDraggable(
    onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
    onDragDelta: (Float) -> Unit,
    onDragStopped: (velocity: Float) -> Unit,
): Modifier = composed {
    val onDragStarted by rememberUpdatedState(onDragStarted)
    val onDragStopped by rememberUpdatedState(onDragStopped)
    val onDragDelta by rememberUpdatedState(onDragDelta)
    val startDragImmediately by rememberUpdatedState(startDragImmediately)
): Modifier =
    this.then(
        MultiPointerDraggableElement(
            orientation,
            enabled,
            startDragImmediately,
            onDragStarted,
            onDragDelta,
            onDragStopped,
        )
    )

    val velocityTracker = remember { VelocityTracker() }
    val maxFlingVelocity =
        LocalViewConfiguration.current.maximumFlingVelocity.let { max ->
            val maxF = max.toFloat()
            Velocity(maxF, maxF)
private data class MultiPointerDraggableElement(
    private val orientation: Orientation,
    private val enabled: Boolean,
    private val startDragImmediately: Boolean,
    private val onDragStarted:
        (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
    private val onDragDelta: (Float) -> Unit,
    private val onDragStopped: (velocity: Float) -> Unit,
) : ModifierNodeElement<MultiPointerDraggableNode>() {
    override fun create(): MultiPointerDraggableNode =
        MultiPointerDraggableNode(
            orientation = orientation,
            enabled = enabled,
            startDragImmediately = startDragImmediately,
            onDragStarted = onDragStarted,
            onDragDelta = onDragDelta,
            onDragStopped = onDragStopped,
        )

    override fun update(node: MultiPointerDraggableNode) {
        node.orientation = orientation
        node.enabled = enabled
        node.startDragImmediately = startDragImmediately
        node.onDragStarted = onDragStarted
        node.onDragDelta = onDragDelta
        node.onDragStopped = onDragStopped
    }
}

    pointerInput(enabled, orientation, maxFlingVelocity) {
private class MultiPointerDraggableNode(
    orientation: Orientation,
    enabled: Boolean,
    var startDragImmediately: Boolean,
    var onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
    var onDragDelta: (Float) -> Unit,
    var onDragStopped: (velocity: Float) -> Unit,
) : PointerInputModifierNode, DelegatingNode(), CompositionLocalConsumerModifierNode {
    private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
    private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
    private val velocityTracker = VelocityTracker()

    var enabled: Boolean = enabled
        set(value) {
            // Reset the pointer input whenever enabled changed.
            if (value != field) {
                field = value
                delegate.resetPointerInputHandler()
            }
        }

    var orientation: Orientation = orientation
        set(value) {
            // Reset the pointer input whenever enabled orientation.
            if (value != field) {
                field = value
                delegate.resetPointerInputHandler()
            }
        }

    override fun onCancelPointerInput() = delegate.onCancelPointerInput()

    override fun onPointerEvent(
        pointerEvent: PointerEvent,
        pass: PointerEventPass,
        bounds: IntSize
    ) = delegate.onPointerEvent(pointerEvent, pass, bounds)

    private suspend fun PointerInputScope.pointerInput() {
        if (!enabled) {
            return@pointerInput
            return
        }

        val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
@@ -90,6 +161,12 @@ internal fun Modifier.multiPointerDraggable(
        val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }

        val onDragEnd: () -> Unit = {
            val maxFlingVelocity =
                currentValueOf(LocalViewConfiguration).maximumFlingVelocity.let { max ->
                    val maxF = max.toFloat()
                    Velocity(maxF, maxF)
                }

            val velocity = velocityTracker.calculateVelocity(maxFlingVelocity)
            onDragStopped(
                when (orientation) {
+48 −33
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 * 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.
@@ -14,10 +14,9 @@
 * limitations under the License.
 */

package com.android.compose.animation.scene.transformation
package com.android.compose.animation.scene

import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect
@@ -28,45 +27,62 @@ import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.withSaveLayer
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.Scene
import com.android.compose.animation.scene.SceneTransitionLayoutImpl

/** Punch a hole in an element using the bounds of another element and a given [shape]. */
internal class PunchHole(
    override val matcher: ElementMatcher,
internal fun Modifier.punchHole(
    layoutImpl: SceneTransitionLayoutImpl,
    element: ElementKey,
    bounds: ElementKey,
    shape: Shape,
): Modifier = this.then(PunchHoleElement(layoutImpl, element, bounds, shape))

private data class PunchHoleElement(
    private val layoutImpl: SceneTransitionLayoutImpl,
    private val element: ElementKey,
    private val bounds: ElementKey,
    private val shape: Shape,
) : ModifierTransformation {
) : ModifierNodeElement<PunchHoleNode>() {
    override fun create(): PunchHoleNode = PunchHoleNode(layoutImpl, element, bounds, shape)

    override fun update(node: PunchHoleNode) {
        node.layoutImpl = layoutImpl
        node.element = element
        node.bounds = bounds
        node.shape = shape
    }
}

private class PunchHoleNode(
    var layoutImpl: SceneTransitionLayoutImpl,
    var element: ElementKey,
    var bounds: ElementKey,
    var shape: Shape,
) : Modifier.Node(), DrawModifierNode {
    private var lastSize: Size = Size.Unspecified
    private var lastLayoutDirection: LayoutDirection = LayoutDirection.Ltr
    private var lastOutline: Outline? = null

    override fun Modifier.transform(
        layoutImpl: SceneTransitionLayoutImpl,
        scene: Scene,
        element: Element,
        sceneValues: Element.TargetValues,
    ): Modifier {
        return drawWithContent {
    override fun ContentDrawScope.draw() {
        val bounds = layoutImpl.elements[bounds]

        if (
            bounds == null ||
                bounds.lastSharedValues.size == Element.SizeUnspecified ||
                bounds.lastSharedValues.offset == Offset.Unspecified
        ) {
            drawContent()
                return@drawWithContent
            return
        }

        val element = layoutImpl.elements.getValue(element)
        drawIntoCanvas { canvas ->
            canvas.withSaveLayer(size.toRect(), Paint()) {
                drawContent()
@@ -76,7 +92,6 @@ internal class PunchHole(
            }
        }
    }
    }

    private fun DrawScope.drawHole(bounds: Element) {
        val boundsSize = bounds.lastSharedValues.size.toSize()
Loading