From d9c1b4504b335d737565532d1d87eb88e4cb58b3 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 15 Nov 2023 13:22:16 +0100 Subject: Move PunchHole.kt to the animation/scene/ directory This CL moves PunchHole.kt out of the transformation/ directory. This is a pure move to improve the diff of the parent CL http://ag/25382493. Test: PunchHoleTest Bug: 291071158 Flag: NA Change-Id: Ia70d7d8df72a14685ab4c46b4615c91edaf54641 --- .../android/compose/animation/scene/PunchHole.kt | 101 ++++++++++++++++++++ .../compose/animation/scene/TransitionDslImpl.kt | 1 - .../animation/scene/transformation/PunchHole.kt | 105 --------------------- 3 files changed, 101 insertions(+), 106 deletions(-) create mode 100644 packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt delete mode 100644 packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/PunchHole.kt 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 000000000000..a8d364060e88 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt @@ -0,0 +1,101 @@ +/* + * 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 + +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.transformation.ModifierTransformation + +/** 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/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index d4909892f492..bcb20913e554 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 @@ -30,7 +30,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.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 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 984086b7792a..000000000000 --- 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, - ) - } -} -- cgit v1.2.3-59-g8ed1b From 7201c5606e4ba1cd7a25397a0df96eada5859d46 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 15 Nov 2023 11:49:26 +0100 Subject: Remove Modifier transformations (1/2) This CL removes the ModifierTransformation interface. This interface was only implement by PunchHole, which is now replaced by Modifier.punchHole that can be called directly from user code. There were 2 main reasons for this removal, which are both performance reasons: - This removes the modifierTransformations() chain applied in http://shortn/_9vgJ2a7Yx6. With this removed and once b/311132415 is fixed, we will be able to make Modifier.element() be a single ModifierNodeElement. - modifierTransformations() was reading the current transition state in http://shortn/_EAjP93YKeQ, which means that *all elements* always recompose whenever the transition state has changed, just because of that PunchHole transformation. Test: PunchHoleTest Test: Manual, in the gallery app by going from Lockscreen <=> Shade Bug: 291071158 Flag: NA Change-Id: I45e71dce8acb98318ac61127bd5294f0a7053dda --- .../transitions/FromLockscreenToShadeTransition.kt | 1 - .../com/android/compose/animation/scene/Element.kt | 34 ---------- .../android/compose/animation/scene/PunchHole.kt | 75 ++++++++++++++-------- .../com/android/compose/animation/scene/Scene.kt | 7 ++ .../animation/scene/SceneTransitionLayout.kt | 13 ++++ .../animation/scene/SceneTransitionLayoutState.kt | 6 ++ .../compose/animation/scene/SceneTransitions.kt | 6 +- .../compose/animation/scene/TransitionDsl.kt | 15 ----- .../compose/animation/scene/TransitionDslImpl.kt | 5 -- .../scene/transformation/Transformation.kt | 14 ---- 10 files changed, 74 insertions(+), 102 deletions(-) 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 fadbdce80cbf..22dc0aee45b4 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 @@ 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) 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 3b999e304491..cf4aa625628a 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 @@ -161,7 +161,6 @@ internal fun Modifier.element( } } } - .modifierTransformations(layoutImpl, scene, element, sceneValues) .intermediateLayout { measurable, constraints -> val placeable = measure(layoutImpl, scene, element, sceneValues, measurable, constraints) @@ -331,39 +330,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. * 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 index a8d364060e88..560e92becba5 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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. @@ -17,7 +17,6 @@ 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,48 +27,68 @@ 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.transformation.ModifierTransformation -/** 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() { + 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 { - val bounds = layoutImpl.elements[bounds] - if ( - bounds == null || - bounds.lastSharedValues.size == Element.SizeUnspecified || - bounds.lastSharedValues.offset == Offset.Unspecified - ) { + 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() - 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) } - } + val offset = bounds.lastSharedValues.offset - element.lastSharedValues.offset + translate(offset.x, offset.y) { drawHole(bounds) } } } } 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 857a596a1404..f5b271019748 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 @@ -27,6 +27,7 @@ import androidx.compose.runtime.setValue 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 @@ -130,4 +131,10 @@ private class SceneScopeImpl( ) { 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 30d13dfa3f70..1f698ff00141 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 @@ -23,6 +23,7 @@ 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 @@ -177,6 +178,18 @@ interface SceneScope { lerp: (start: T, stop: T, fraction: Float) -> T, canOverflow: Boolean, ): State + + /** + * 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/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 64c9775a386e..6a87662135de 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 @@ -46,6 +46,12 @@ class SceneTransitionLayoutState(initialScene: SceneKey) { 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) + } } sealed interface TransitionState { 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 2172ed300a33..828dff27637b 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 @@ -27,7 +27,6 @@ import com.android.compose.animation.scene.transformation.AnchoredTranslate 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 @@ -122,7 +121,6 @@ data class TransitionSpec( scene: SceneKey, ): ElementTransformations { var shared: SharedElementTransformation? = null - val modifier = mutableListOf() var offset: PropertyTransformation? = null var size: PropertyTransformation? = null var drawScale: PropertyTransformation? = null @@ -166,12 +164,11 @@ data class TransitionSpec( 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 +185,6 @@ data class TransitionSpec( /** The transformations of an element during a transition. */ internal class ElementTransformations( val shared: SharedElementTransformation?, - val modifier: List, val offset: PropertyTransformation?, val size: PropertyTransformation?, val drawScale: PropertyTransformation?, 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 ca66dff5e231..f820074ec3d1 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 @@ package com.android.compose.animation.scene 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 @@ -130,19 +128,6 @@ interface TransitionBuilder : PropertyTransformationBuilder { scenePicker: SharedElementScenePicker = DefaultSharedElementScenePicker, ) - /** - * 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 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 bcb20913e554..8c0a5a394331 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.Spring 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 @@ -92,10 +91,6 @@ internal class TransitionBuilderImpl : TransitionBuilder { 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/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index 0db8469466ef..206935558179 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 class SharedElementTransformation( 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 : Transformation { /** -- cgit v1.2.3-59-g8ed1b From 2205c0aafc664c5c0b48bd52b376fad774c81718 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 15 Nov 2023 15:30:52 +0100 Subject: Move Element drawing logic in ElementNode Bug: 291566282 Test: atest PlatformComposeSceneTransitionLayoutTests Flag: NA Change-Id: I2fd249e2bc003258dcea259b9150cf314d930981 --- .../com/android/compose/animation/scene/Element.kt | 51 +++++++++++++--------- 1 file changed, 30 insertions(+), 21 deletions(-) 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 cf4aa625628a..1d2c053b3c76 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 @@ -25,16 +25,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 @@ -144,23 +145,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() - } - } - } - } + 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) @@ -177,22 +164,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() { - 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 @@ -238,15 +228,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( -- cgit v1.2.3-59-g8ed1b From 6ecab7cfba3045f3e8bd8f13fc9beedfe869ef03 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Tue, 14 Nov 2023 17:43:23 +0100 Subject: Annotate Stable classes/interfaces to optimize compositions This CL annotates most of the public STL classes as @Stable (which they are) so that we don't recompose when it's not necessary. This CL adds a small test to ensure that existing elements don't recompose when starting or finishing a transition. Bug: 291071158 Flag: NA Test: ElementTest Change-Id: Idccc990000d7678261c6a14c7a86aedbc353221d --- .../com/android/compose/animation/scene/Element.kt | 4 ++++ .../src/com/android/compose/animation/scene/Key.kt | 2 ++ .../com/android/compose/animation/scene/Scene.kt | 15 ++++++++---- .../animation/scene/SceneTransitionLayout.kt | 2 ++ .../animation/scene/SceneTransitionLayoutImpl.kt | 2 ++ .../animation/scene/SceneTransitionLayoutState.kt | 4 +++- .../compose/animation/scene/SceneTransitions.kt | 2 ++ .../android/compose/animation/scene/ElementTest.kt | 27 ++++++++++++++++++++++ 8 files changed, 52 insertions(+), 6 deletions(-) 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 1d2c053b3c76..b264a7ef0da3 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 @@ -47,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 @@ -91,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() @@ -108,6 +111,7 @@ internal class Element(val key: ElementKey) { } /** A shared value of this element. */ + @Stable class SharedValue(val key: ValueKey, initialValue: T) { var value by mutableStateOf(initialValue) } 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 5b752eb4e900..84d3b8647d6c 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/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index f5b271019748..f5561cb404b6 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,11 +19,13 @@ package com.android.compose.animation.scene 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 @@ -34,6 +36,7 @@ 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, @@ -105,11 +108,13 @@ private class SceneScopeImpl( ): State { 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( 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 1f698ff00141..07add77eccd4 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,6 +19,7 @@ package com.android.compose.animation.scene 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 @@ -95,6 +96,7 @@ interface SceneTransitionLayoutScope { @DslMarker annotation class ElementDsl @ElementDsl +@Stable interface SceneScope { /** The state of the [SceneTransitionLayout] in which this scene is contained. */ val layoutState: SceneTransitionLayoutState 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 60f385aede02..02ddccbc051b 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.Composable 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 com.android.compose.ui.util.lerp 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 6a87662135de..f48e9147eef4 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 @@ class SceneTransitionLayoutState(initialScene: SceneKey) { * [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 @@ -54,6 +55,7 @@ class SceneTransitionLayoutState(initialScene: SceneKey) { } } +@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 828dff27637b..f91895bb0e05 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 @@ package com.android.compose.animation.scene 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 @@ -92,6 +93,7 @@ class SceneTransitions( } /** The definition of a transition between [from] and [to]. */ +@Stable data class TransitionSpec( val from: SceneKey?, val to: SceneKey?, 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 cc7a0b8e33b2..ce3e1db2c3d0 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.Row 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 @@ class ElementTest { 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) } + } + } } -- cgit v1.2.3-59-g8ed1b From 6f2454ebb752af80e47b944605d3e75401e9fa1b Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 15 Nov 2023 17:56:10 +0100 Subject: Migrate Modifier.multiPointerDraggable to the Node Modifier API Bug: b/291566282 Test: atest SwipeToSceneTest Flag: NA Change-Id: Ia6423577c9ddd56f24c6c2c5516b496ea4258773 --- .../com/android/compose/animation/scene/Element.kt | 1 + .../animation/scene/MultiPointerDraggable.kt | 111 +++++++++++++++++---- 2 files changed, 95 insertions(+), 17 deletions(-) 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 b264a7ef0da3..2b1195229c76 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 @@ -131,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, 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 d48781a4529b..a0fba8076517 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.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) - - val velocityTracker = remember { VelocityTracker() } - val maxFlingVelocity = - LocalViewConfiguration.current.maximumFlingVelocity.let { max -> - val maxF = max.toFloat() - Velocity(maxF, maxF) +): Modifier = + this.then( + MultiPointerDraggableElement( + orientation, + enabled, + startDragImmediately, + onDragStarted, + onDragDelta, + onDragStopped, + ) + ) + +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() { + 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() + } + } + + var orientation: Orientation = orientation + set(value) { + // Reset the pointer input whenever enabled orientation. + if (value != field) { + field = value + delegate.resetPointerInputHandler() + } } - pointerInput(enabled, orientation, maxFlingVelocity) { + 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) { -- cgit v1.2.3-59-g8ed1b