1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.compose.ui.graphics
18
19 import androidx.compose.runtime.getValue
20 import androidx.compose.runtime.mutableFloatStateOf
21 import androidx.compose.runtime.mutableStateListOf
22 import androidx.compose.runtime.mutableStateOf
23 import androidx.compose.runtime.setValue
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.geometry.Offset
26 import androidx.compose.ui.graphics.Path
27 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
28 import androidx.compose.ui.graphics.drawscope.DrawScope
29 import androidx.compose.ui.graphics.drawscope.clipPath
30 import androidx.compose.ui.graphics.drawscope.translate
31 import androidx.compose.ui.graphics.layer.GraphicsLayer
32 import androidx.compose.ui.graphics.layer.drawLayer
33 import androidx.compose.ui.layout.LayoutCoordinates
34 import androidx.compose.ui.layout.positionInWindow
35 import androidx.compose.ui.modifier.ModifierLocalModifierNode
36 import androidx.compose.ui.node.DrawModifierNode
37 import androidx.compose.ui.node.LayoutAwareModifierNode
38 import androidx.compose.ui.node.ModifierNodeElement
39 import androidx.compose.ui.node.requireDensity
40 import androidx.compose.ui.node.requireGraphicsContext
41 import androidx.compose.ui.unit.Density
42 import androidx.compose.ui.unit.LayoutDirection
43 import androidx.compose.ui.util.fastForEach
44
45 /**
46 * Define this as a container into which other composables can be drawn using [drawInContainer].
47 *
48 * The elements redirected to this container will be drawn above the content of this composable.
49 */
50 fun Modifier.container(state: ContainerState): Modifier {
51 return this then ContainerElement(state)
52 }
53
54 /**
55 * Draw this composable into the container associated to [state].
56 *
57 * @param state the state of the container into which we should draw this composable.
58 * @param enabled whether the redirection of the drawing to the container is enabled.
59 * @param zIndex the z-index of the composable in the container.
60 * @param clipPath the clip path applied when drawing this composable into the container.
61 */
drawInContainernull62 fun Modifier.drawInContainer(
63 state: ContainerState,
64 enabled: () -> Boolean = { true },
65 zIndex: Float = 0f,
_null66 clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null },
67 ): Modifier {
68 return this.then(
69 DrawInContainerElement(
70 state = state,
71 enabled = enabled,
72 zIndex = zIndex,
73 clipPath = clipPath,
74 )
75 )
76 }
77
78 class ContainerState {
79 private var renderers = mutableStateListOf<LayerRenderer>()
80 internal var lastOffsetInWindow by mutableStateOf(Offset.Zero)
81
onLayerRendererAttachednull82 internal fun onLayerRendererAttached(renderer: LayerRenderer) {
83 renderers.add(renderer)
84 renderers.sortBy { it.zIndex }
85 }
86
onLayerRendererDetachednull87 internal fun onLayerRendererDetached(renderer: LayerRenderer) {
88 renderers.remove(renderer)
89 }
90
drawInOverlaynull91 internal fun drawInOverlay(drawScope: DrawScope) {
92 renderers.fastForEach { it.drawInOverlay(drawScope) }
93 }
94 }
95
96 internal interface LayerRenderer {
97 val zIndex: Float
98
drawInOverlaynull99 fun drawInOverlay(drawScope: DrawScope)
100 }
101
102 private data class ContainerElement(private val state: ContainerState) :
103 ModifierNodeElement<ContainerNode>() {
104 override fun create(): ContainerNode {
105 return ContainerNode(state)
106 }
107
108 override fun update(node: ContainerNode) {
109 node.state = state
110 }
111 }
112
113 /** A node implementing [container] that can be delegated to. */
114 class ContainerNode(var state: ContainerState) :
115 Modifier.Node(), LayoutAwareModifierNode, DrawModifierNode {
onPlacednull116 override fun onPlaced(coordinates: LayoutCoordinates) {
117 state.lastOffsetInWindow = coordinates.positionInWindow()
118 }
119
drawnull120 override fun ContentDrawScope.draw() {
121 drawContent()
122 state.drawInOverlay(this)
123 }
124 }
125
126 private data class DrawInContainerElement(
127 var state: ContainerState,
128 var enabled: () -> Boolean,
129 val zIndex: Float,
130 val clipPath: (LayoutDirection, Density) -> Path?,
131 ) : ModifierNodeElement<DrawInContainerNode>() {
createnull132 override fun create(): DrawInContainerNode {
133 return DrawInContainerNode(state, enabled, zIndex, clipPath)
134 }
135
updatenull136 override fun update(node: DrawInContainerNode) {
137 node.state = state
138 node.enabled = enabled
139 node.zIndex = zIndex
140 node.clipPath = clipPath
141 }
142 }
143
144 /**
145 * The implementation of [drawInContainer].
146 *
147 * Note: this was forked from AndroidX RenderInTransitionOverlayNodeElement.kt
148 * (http://shortn/_3dfSFPbm8f).
149 */
150 internal class DrawInContainerNode(
151 var state: ContainerState,
<lambda>null152 var enabled: () -> Boolean = { true },
153 zIndex: Float = 0f,
_null154 var clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null },
155 ) : Modifier.Node(), LayoutAwareModifierNode, DrawModifierNode, ModifierLocalModifierNode {
156 private var lastOffsetInWindow by mutableStateOf(Offset.Zero)
157 var zIndex by mutableFloatStateOf(zIndex)
158
159 private inner class LayerWithRenderer(val layer: GraphicsLayer) : LayerRenderer {
160 override val zIndex: Float
161 get() = this@DrawInContainerNode.zIndex
162
drawInOverlaynull163 override fun drawInOverlay(drawScope: DrawScope) {
164 if (enabled()) {
165 with(drawScope) {
166 val (x, y) = lastOffsetInWindow - state.lastOffsetInWindow
167 val clipPath = clipPath(layoutDirection, requireDensity())
168 if (clipPath != null) {
169 clipPath(clipPath) { translate(x, y) { drawLayer(layer) } }
170 } else {
171 translate(x, y) { drawLayer(layer) }
172 }
173 }
174 }
175 }
176 }
177
178 // Render in-place logic. Depending on the result of `renderInOverlay()`, the content will
179 // either render in-place or in the overlay, but never in both places.
drawnull180 override fun ContentDrawScope.draw() {
181 val layer = requireNotNull(layer) { "Error: layer never initialized" }
182 layer.record { this@draw.drawContent() }
183 if (!enabled()) {
184 drawLayer(layer)
185 }
186 }
187
onPlacednull188 override fun onPlaced(coordinates: LayoutCoordinates) {
189 lastOffsetInWindow = coordinates.positionInWindow()
190 }
191
192 val layer: GraphicsLayer?
193 get() = layerWithRenderer?.layer
194
195 private var layerWithRenderer: LayerWithRenderer? = null
196
onAttachnull197 override fun onAttach() {
198 LayerWithRenderer(requireGraphicsContext().createGraphicsLayer()).let {
199 state.onLayerRendererAttached(it)
200 layerWithRenderer = it
201 }
202 }
203
onDetachnull204 override fun onDetach() {
205 layerWithRenderer?.let {
206 state.onLayerRendererDetached(it)
207 requireGraphicsContext().releaseGraphicsLayer(it.layer)
208 }
209 }
210 }
211