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
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
index fadbdce..22dc0ae 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
@@ -10,7 +10,6 @@
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)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 3b999e30..2b11952 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -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.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 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 @@
}
/** The target values of this element in a given scene. */
+ @Stable
class TargetValues(val scene: SceneKey) {
val lastValues = Values()
@@ -107,6 +111,7 @@
}
/** A shared value of this element. */
+ @Stable
class SharedValue<T>(val key: ValueKey, initialValue: T) {
var value by mutableStateOf(initialValue)
}
@@ -126,6 +131,7 @@
/** The implementation of [SceneScope.element]. */
@OptIn(ExperimentalComposeUiApi::class)
+@Stable
internal fun Modifier.element(
layoutImpl: SceneTransitionLayoutImpl,
scene: Scene,
@@ -144,24 +150,9 @@
?: 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 @@
*/
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 @@
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(
@@ -332,39 +345,6 @@
}
/**
- * 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.
*
* Important: The logic here should closely match the logic in [elementAlpha]. Note that we don't
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 5b752eb..84d3b86 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -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
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index d48781a..a0fba80 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -23,20 +23,25 @@
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 @@
* 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 @@
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
+ }
+}
+
+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()
+ }
}
- pointerInput(enabled, orientation, maxFlingVelocity) {
+ 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 @@
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) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
new file mode 100644
index 0000000..560e92b
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
@@ -0,0 +1,120 @@
+/*
+ * 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 androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Outline
+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
+
+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,
+) : 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 ContentDrawScope.draw() {
+ val bounds = layoutImpl.elements[bounds]
+
+ if (
+ bounds == null ||
+ bounds.lastSharedValues.size == Element.SizeUnspecified ||
+ bounds.lastSharedValues.offset == Offset.Unspecified
+ ) {
+ drawContent()
+ return
+ }
+
+ val element = layoutImpl.elements.getValue(element)
+ drawIntoCanvas { canvas ->
+ canvas.withSaveLayer(size.toRect(), Paint()) {
+ drawContent()
+
+ val offset = bounds.lastSharedValues.offset - element.lastSharedValues.offset
+ translate(offset.x, offset.y) { drawHole(bounds) }
+ }
+ }
+ }
+
+ private fun DrawScope.drawHole(bounds: Element) {
+ val boundsSize = bounds.lastSharedValues.size.toSize()
+ if (shape == RectangleShape) {
+ drawRect(Color.Black, size = boundsSize, blendMode = BlendMode.DstOut)
+ return
+ }
+
+ val outline =
+ if (boundsSize == lastSize && layoutDirection == lastLayoutDirection) {
+ lastOutline!!
+ } else {
+ val newOutline = shape.createOutline(boundsSize, layoutDirection, this)
+ lastSize = boundsSize
+ lastLayoutDirection = layoutDirection
+ lastOutline = newOutline
+ newOutline
+ }
+
+ drawOutline(
+ outline,
+ Color.Black,
+ blendMode = BlendMode.DstOut,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 857a596..f5561cb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -19,20 +19,24 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+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.graphics.Shape
import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.zIndex
/** A scene in a [SceneTransitionLayout]. */
+@Stable
internal class Scene(
val key: SceneKey,
layoutImpl: SceneTransitionLayoutImpl,
@@ -104,11 +108,13 @@
): State<T> {
val element =
element?.let { key ->
- layoutImpl.elements[key]
- ?: error(
- "Element $key is not composed. Make sure to call animateSharedXAsState " +
- "*after* Modifier.element(key)."
- )
+ Snapshot.withoutReadObservation {
+ layoutImpl.elements[key]
+ ?: error(
+ "Element $key is not composed. Make sure to call " +
+ "animateSharedXAsState *after* Modifier.element(key)."
+ )
+ }
}
return animateSharedValueAsState(
@@ -130,4 +136,10 @@
) {
MovableElement(layoutImpl, scene, key, modifier, content)
}
+
+ override fun Modifier.punchHole(
+ element: ElementKey,
+ bounds: ElementKey,
+ shape: Shape
+ ): Modifier = punchHole(layoutImpl, element, bounds, shape)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 30d13df..07add77 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -19,10 +19,12 @@
import androidx.annotation.FloatRange
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.LocalDensity
@@ -94,6 +96,7 @@
@DslMarker annotation class ElementDsl
@ElementDsl
+@Stable
interface SceneScope {
/** The state of the [SceneTransitionLayout] in which this scene is contained. */
val layoutState: SceneTransitionLayoutState
@@ -177,6 +180,18 @@
lerp: (start: T, stop: T, fraction: Float) -> T,
canOverflow: Boolean,
): State<T>
+
+ /**
+ * Punch a hole in this [element] using the bounds of [bounds] in [scene] and the given [shape].
+ *
+ * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
+ * This can be used to make content drawn below an opaque element visible. For example, if we
+ * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
+ * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big
+ * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be
+ * the result.
+ */
+ fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier
}
// TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 60f385a..02ddccb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -23,6 +23,7 @@
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
@@ -41,6 +42,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
+@Stable
internal class SceneTransitionLayoutImpl(
onChangeScene: (SceneKey) -> Unit,
builder: SceneTransitionLayoutScope.() -> Unit,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 64c9775..f48e914 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -16,11 +16,13 @@
package com.android.compose.animation.scene
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
/** The state of a [SceneTransitionLayout]. */
+@Stable
class SceneTransitionLayoutState(initialScene: SceneKey) {
/**
* The current [TransitionState]. All values read here are backed by the Snapshot system.
@@ -29,7 +31,6 @@
* [SceneTransitionLayoutState.observableTransitionState] instead.
*/
var transitionState: TransitionState by mutableStateOf(TransitionState.Idle(initialScene))
- internal set
/**
* Whether we are transitioning, optionally restricting the check to the transition between
@@ -46,8 +47,15 @@
return (from == null || transition.fromScene == from) &&
(to == null || transition.toScene == to)
}
+
+ /** Whether we are transitioning from [scene] to [other], or from [other] to [scene]. */
+ fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean {
+ return isTransitioning(from = scene, to = other) ||
+ isTransitioning(from = other, to = scene)
+ }
}
+@Stable
sealed interface TransitionState {
/**
* The current effective scene. If a new transition was triggered, it would start from this
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 2172ed3..f91895b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.snap
+import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
@@ -27,7 +28,6 @@
import com.android.compose.animation.scene.transformation.DrawScale
import com.android.compose.animation.scene.transformation.EdgeTranslate
import com.android.compose.animation.scene.transformation.Fade
-import com.android.compose.animation.scene.transformation.ModifierTransformation
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
import com.android.compose.animation.scene.transformation.ScaleSize
@@ -93,6 +93,7 @@
}
/** The definition of a transition between [from] and [to]. */
+@Stable
data class TransitionSpec(
val from: SceneKey?,
val to: SceneKey?,
@@ -122,7 +123,6 @@
scene: SceneKey,
): ElementTransformations {
var shared: SharedElementTransformation? = null
- val modifier = mutableListOf<ModifierTransformation>()
var offset: PropertyTransformation<Offset>? = null
var size: PropertyTransformation<IntSize>? = null
var drawScale: PropertyTransformation<Scale>? = null
@@ -166,12 +166,11 @@
throwIfNotNull(shared, element, name = "shared")
shared = transformation
}
- is ModifierTransformation -> modifier.add(transformation)
is PropertyTransformation<*> -> onPropertyTransformation(transformation)
}
}
- return ElementTransformations(shared, modifier, offset, size, drawScale, alpha)
+ return ElementTransformations(shared, offset, size, drawScale, alpha)
}
private fun throwIfNotNull(
@@ -188,7 +187,6 @@
/** The transformations of an element during a transition. */
internal class ElementTransformations(
val shared: SharedElementTransformation?,
- val modifier: List<ModifierTransformation>,
val offset: PropertyTransformation<Offset>?,
val size: PropertyTransformation<IntSize>?,
val drawScale: PropertyTransformation<Scale>?,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index ca66dff5..f820074 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -18,8 +18,6 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -131,19 +129,6 @@
)
/**
- * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and
- * using the given [shape].
- *
- * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
- * This can be used to make content drawn below an opaque element visible. For example, if we
- * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
- * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big
- * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be
- * the result.
- */
- fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape)
-
- /**
* Adds the transformations in [builder] but in reversed order. This allows you to partially
* reuse the definition of the transition from scene `Foo` to scene `Bar` inside the definition
* of the transition from scene `Bar` to scene `Foo`.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index d490989..8c0a5a3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -22,7 +22,6 @@
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import com.android.compose.animation.scene.transformation.AnchoredSize
import com.android.compose.animation.scene.transformation.AnchoredTranslate
@@ -30,7 +29,6 @@
import com.android.compose.animation.scene.transformation.EdgeTranslate
import com.android.compose.animation.scene.transformation.Fade
import com.android.compose.animation.scene.transformation.PropertyTransformation
-import com.android.compose.animation.scene.transformation.PunchHole
import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
import com.android.compose.animation.scene.transformation.ScaleSize
import com.android.compose.animation.scene.transformation.SharedElementTransformation
@@ -93,10 +91,6 @@
spec.vectorize(Float.VectorConverter).durationMillis
}
- override fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape) {
- transformations.add(PunchHole(matcher, bounds, shape))
- }
-
override fun reversed(builder: TransitionBuilder.() -> Unit) {
reversed = true
builder()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/PunchHole.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/PunchHole.kt
deleted file mode 100644
index 984086b..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/PunchHole.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 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.transformation
-
-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
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Outline
-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.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.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,
- private val bounds: ElementKey,
- private val shape: Shape,
-) : ModifierTransformation {
-
- 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 {
- val bounds = layoutImpl.elements[bounds]
- if (
- bounds == null ||
- bounds.lastSharedValues.size == Element.SizeUnspecified ||
- bounds.lastSharedValues.offset == Offset.Unspecified
- ) {
- drawContent()
- return@drawWithContent
- }
- drawIntoCanvas { canvas ->
- canvas.withSaveLayer(size.toRect(), Paint()) {
- drawContent()
-
- val offset = bounds.lastSharedValues.offset - element.lastSharedValues.offset
- translate(offset.x, offset.y) { drawHole(bounds) }
- }
- }
- }
- }
-
- private fun DrawScope.drawHole(bounds: Element) {
- val boundsSize = bounds.lastSharedValues.size.toSize()
- if (shape == RectangleShape) {
- drawRect(Color.Black, size = boundsSize, blendMode = BlendMode.DstOut)
- return
- }
-
- val outline =
- if (boundsSize == lastSize && layoutDirection == lastLayoutDirection) {
- lastOutline!!
- } else {
- val newOutline = shape.createOutline(boundsSize, layoutDirection, this)
- lastSize = boundsSize
- lastLayoutDirection = layoutDirection
- lastOutline = newOutline
- newOutline
- }
-
- drawOutline(
- outline,
- Color.Black,
- blendMode = BlendMode.DstOut,
- )
- }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 0db8469..2069355 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -16,7 +16,6 @@
package com.android.compose.animation.scene.transformation
-import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.Scene
@@ -52,19 +51,6 @@
internal val scenePicker: SharedElementScenePicker,
) : Transformation
-/** A transformation that is applied on the element during the whole transition. */
-internal interface ModifierTransformation : Transformation {
- /** Apply the transformation to [element]. */
- // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
- // to these internal classes.
- fun Modifier.transform(
- layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
- element: Element,
- sceneValues: Element.TargetValues,
- ): Modifier
-}
-
/** A transformation that changes the value of an element property, like its size or offset. */
internal sealed interface PropertyTransformation<T> : Transformation {
/**
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index cc7a0b8..ce3e1db 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -427,4 +428,30 @@
assertThat(barElement.sceneValues.keys).containsExactly(TestScenes.SceneA)
assertThat(fooElement.sceneValues).isEmpty()
}
+
+ @Test
+ fun existingElementsDontRecomposeWhenTransitionStateChanges() {
+ var fooCompositions = 0
+
+ rule.testTransition(
+ fromSceneContent = {
+ SideEffect { fooCompositions++ }
+ Box(Modifier.element(TestElements.Foo))
+ },
+ toSceneContent = {},
+ transition = {
+ spec = tween(4 * 16)
+
+ scaleSize(TestElements.Foo, width = 2f, height = 0.5f)
+ translate(TestElements.Foo, x = 10.dp, y = 10.dp)
+ fade(TestElements.Foo)
+ }
+ ) {
+ before { assertThat(fooCompositions).isEqualTo(1) }
+ at(16) { assertThat(fooCompositions).isEqualTo(1) }
+ at(32) { assertThat(fooCompositions).isEqualTo(1) }
+ at(48) { assertThat(fooCompositions).isEqualTo(1) }
+ after { assertThat(fooCompositions).isEqualTo(1) }
+ }
+ }
}