1 /*
<lambda>null2 * Copyright 2019 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 androidx.compose.ui.draw
18
19 import androidx.collection.MutableObjectList
20 import androidx.collection.mutableObjectListOf
21 import androidx.compose.ui.Modifier
22 import androidx.compose.ui.geometry.Size
23 import androidx.compose.ui.graphics.Canvas
24 import androidx.compose.ui.graphics.GraphicsContext
25 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
26 import androidx.compose.ui.graphics.drawscope.DrawScope
27 import androidx.compose.ui.graphics.layer.GraphicsLayer
28 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
29 import androidx.compose.ui.internal.checkPrecondition
30 import androidx.compose.ui.internal.checkPreconditionNotNull
31 import androidx.compose.ui.node.DrawModifierNode
32 import androidx.compose.ui.node.ModifierNodeElement
33 import androidx.compose.ui.node.Nodes
34 import androidx.compose.ui.node.ObserverModifierNode
35 import androidx.compose.ui.node.invalidateDraw
36 import androidx.compose.ui.node.observeReads
37 import androidx.compose.ui.node.requireCoordinator
38 import androidx.compose.ui.node.requireDensity
39 import androidx.compose.ui.node.requireGraphicsContext
40 import androidx.compose.ui.node.requireLayoutDirection
41 import androidx.compose.ui.platform.InspectorInfo
42 import androidx.compose.ui.unit.Density
43 import androidx.compose.ui.unit.IntSize
44 import androidx.compose.ui.unit.LayoutDirection
45 import androidx.compose.ui.unit.toIntSize
46 import androidx.compose.ui.unit.toSize
47
48 /** A [Modifier.Element] that draws into the space of the layout. */
49 @JvmDefaultWithCompatibility
50 interface DrawModifier : Modifier.Element {
51
52 fun ContentDrawScope.draw()
53 }
54
55 /**
56 * [DrawModifier] implementation that supports building a cache of objects to be referenced across
57 * draw calls
58 */
59 @JvmDefaultWithCompatibility
60 interface DrawCacheModifier : DrawModifier {
61
62 /**
63 * Callback invoked to re-build objects to be re-used across draw calls. This is useful to
64 * conditionally recreate objects only if the size of the drawing environment changes, or if
65 * state parameters that are inputs to objects change. This method is guaranteed to be called
66 * before [DrawModifier.draw].
67 *
68 * @param params The params to be used to build the cache.
69 */
onBuildCachenull70 fun onBuildCache(params: BuildDrawCacheParams)
71 }
72
73 /**
74 * The set of parameters which could be used to build the drawing cache.
75 *
76 * @see DrawCacheModifier.onBuildCache
77 */
78 interface BuildDrawCacheParams {
79 /** The current size of the drawing environment */
80 val size: Size
81
82 /** The current layout direction. */
83 val layoutDirection: LayoutDirection
84
85 /** The current screen density to provide the ability to convert between */
86 val density: Density
87 }
88
89 /** Draw into a [Canvas] behind the modified content. */
Modifiernull90 fun Modifier.drawBehind(onDraw: DrawScope.() -> Unit) = this then DrawBehindElement(onDraw)
91
92 private class DrawBehindElement(val onDraw: DrawScope.() -> Unit) :
93 ModifierNodeElement<DrawBackgroundModifier>() {
94 override fun create() = DrawBackgroundModifier(onDraw)
95
96 override fun update(node: DrawBackgroundModifier) {
97 node.onDraw = onDraw
98 }
99
100 override fun InspectorInfo.inspectableProperties() {
101 name = "drawBehind"
102 properties["onDraw"] = onDraw
103 }
104
105 override fun equals(other: Any?): Boolean {
106 if (this === other) return true
107 if (other !is DrawBehindElement) return false
108
109 if (onDraw !== other.onDraw) return false
110
111 return true
112 }
113
114 override fun hashCode(): Int {
115 return onDraw.hashCode()
116 }
117 }
118
119 internal class DrawBackgroundModifier(var onDraw: DrawScope.() -> Unit) :
120 Modifier.Node(), DrawModifierNode {
121
drawnull122 override fun ContentDrawScope.draw() {
123 onDraw()
124 drawContent()
125 }
126 }
127
128 /**
129 * Draw into a [DrawScope] with content that is persisted across draw calls as long as the size of
130 * the drawing area is the same or any state objects that are read have not changed. In the event
131 * that the drawing area changes, or the underlying state values that are being read change, this
132 * method is invoked again to recreate objects to be used during drawing
133 *
134 * For example, a [androidx.compose.ui.graphics.LinearGradient] that is to occupy the full bounds of
135 * the drawing area can be created once the size has been defined and referenced for subsequent draw
136 * calls without having to re-allocate.
137 *
138 * @sample androidx.compose.ui.samples.DrawWithCacheModifierSample
139 * @sample androidx.compose.ui.samples.DrawWithCacheModifierStateParameterSample
140 * @sample androidx.compose.ui.samples.DrawWithCacheContentSample
141 */
drawWithCachenull142 fun Modifier.drawWithCache(onBuildDrawCache: CacheDrawScope.() -> DrawResult) =
143 this then DrawWithCacheElement(onBuildDrawCache)
144
145 private class DrawWithCacheElement(val onBuildDrawCache: CacheDrawScope.() -> DrawResult) :
146 ModifierNodeElement<CacheDrawModifierNodeImpl>() {
147 override fun create(): CacheDrawModifierNodeImpl {
148 return CacheDrawModifierNodeImpl(CacheDrawScope(), onBuildDrawCache)
149 }
150
151 override fun update(node: CacheDrawModifierNodeImpl) {
152 node.block = onBuildDrawCache
153 }
154
155 override fun InspectorInfo.inspectableProperties() {
156 name = "drawWithCache"
157 properties["onBuildDrawCache"] = onBuildDrawCache
158 }
159
160 override fun equals(other: Any?): Boolean {
161 if (this === other) return true
162 if (other !is DrawWithCacheElement) return false
163
164 if (onBuildDrawCache !== other.onBuildDrawCache) return false
165
166 return true
167 }
168
169 override fun hashCode(): Int {
170 return onBuildDrawCache.hashCode()
171 }
172 }
173
CacheDrawModifierNodenull174 fun CacheDrawModifierNode(
175 onBuildDrawCache: CacheDrawScope.() -> DrawResult
176 ): CacheDrawModifierNode {
177 return CacheDrawModifierNodeImpl(CacheDrawScope(), onBuildDrawCache)
178 }
179
180 /**
181 * Expands on the [androidx.compose.ui.node.DrawModifierNode] by adding the ability to invalidate
182 * the draw cache for changes in things like shapes and bitmaps (see Modifier.border for a usage
183 * examples).
184 */
185 sealed interface CacheDrawModifierNode : DrawModifierNode {
invalidateDrawCachenull186 fun invalidateDrawCache()
187 }
188
189 /**
190 * Wrapper [GraphicsContext] implementation that maintains a list of the [GraphicsLayer] instances
191 * that were created through this instance so it can release only those [GraphicsLayer]s when it is
192 * disposed of within the corresponding Modifier is disposed
193 */
194 private class ScopedGraphicsContext : GraphicsContext {
195
196 private var allocatedGraphicsLayers: MutableObjectList<GraphicsLayer>? = null
197
198 var graphicsContext: GraphicsContext? = null
199 set(value) {
200 releaseGraphicsLayers()
201 field = value
202 }
203
204 override fun createGraphicsLayer(): GraphicsLayer {
205 val gContext = graphicsContext
206 checkPrecondition(gContext != null) { "GraphicsContext not provided" }
207 val layer = gContext.createGraphicsLayer()
208 val layers = allocatedGraphicsLayers
209 if (layers == null) {
210 mutableObjectListOf(layer).also { allocatedGraphicsLayers = it }
211 } else {
212 layers.add(layer)
213 }
214
215 return layer
216 }
217
218 override fun releaseGraphicsLayer(layer: GraphicsLayer) {
219 graphicsContext?.releaseGraphicsLayer(layer)
220 }
221
222 fun releaseGraphicsLayers() {
223 allocatedGraphicsLayers?.let { layers ->
224 layers.forEach { layer -> releaseGraphicsLayer(layer) }
225 layers.clear()
226 }
227 }
228 }
229
230 private class CacheDrawModifierNodeImpl(
231 private val cacheDrawScope: CacheDrawScope,
232 block: CacheDrawScope.() -> DrawResult
233 ) : Modifier.Node(), CacheDrawModifierNode, ObserverModifierNode, BuildDrawCacheParams {
234
235 private var isCacheValid = false
236 private var cachedGraphicsContext: ScopedGraphicsContext? = null
237
238 var block: CacheDrawScope.() -> DrawResult = block
239 set(value) {
240 field = value
241 invalidateDrawCache()
242 }
243
244 init {
245 cacheDrawScope.cacheParams = this
<lambda>null246 cacheDrawScope.graphicsContextProvider = { graphicsContext }
247 }
248
249 override val density: Density
250 get() = requireDensity()
251
252 override val layoutDirection: LayoutDirection
253 get() = requireLayoutDirection()
254
255 override val size: Size
256 get() = requireCoordinator(Nodes.LayoutAware).size.toSize()
257
258 val graphicsContext: GraphicsContext
259 get() {
260 var localGraphicsContext = cachedGraphicsContext
261 if (localGraphicsContext == null) {
<lambda>null262 localGraphicsContext = ScopedGraphicsContext().also { cachedGraphicsContext = it }
263 }
264 if (localGraphicsContext.graphicsContext == null) {
265 localGraphicsContext.graphicsContext = requireGraphicsContext()
266 }
267 return localGraphicsContext
268 }
269
onDetachnull270 override fun onDetach() {
271 super.onDetach()
272 cachedGraphicsContext?.releaseGraphicsLayers()
273 }
274
onMeasureResultChangednull275 override fun onMeasureResultChanged() {
276 invalidateDrawCache()
277 }
278
onObservedReadsChangednull279 override fun onObservedReadsChanged() {
280 invalidateDrawCache()
281 }
282
invalidateDrawCachenull283 override fun invalidateDrawCache() {
284 // Release all previously allocated graphics layers to the recycling pool
285 // if a layer is needed in a subsequent draw, it will be obtained from the pool again and
286 // reused
287 cachedGraphicsContext?.releaseGraphicsLayers()
288 isCacheValid = false
289 cacheDrawScope.drawResult = null
290 invalidateDraw()
291 }
292
onDensityChangenull293 override fun onDensityChange() {
294 invalidateDrawCache()
295 }
296
onLayoutDirectionChangenull297 override fun onLayoutDirectionChange() {
298 invalidateDrawCache()
299 }
300
getOrBuildCachedDrawBlocknull301 private fun getOrBuildCachedDrawBlock(contentDrawScope: ContentDrawScope): DrawResult {
302 if (!isCacheValid) {
303 cacheDrawScope.apply {
304 drawResult = null
305 this.contentDrawScope = contentDrawScope
306 observeReads { block() }
307 checkPreconditionNotNull(drawResult) {
308 "DrawResult not defined, did you forget to call onDraw?"
309 }
310 }
311 isCacheValid = true
312 }
313 return cacheDrawScope.drawResult!!
314 }
315
drawnull316 override fun ContentDrawScope.draw() {
317 getOrBuildCachedDrawBlock(this).block(this)
318 }
319 }
320
321 /**
322 * Handle to a drawing environment that enables caching of content based on the resolved size.
323 * Consumers define parameters and refer to them in the captured draw callback provided in
324 * [onDrawBehind] or [onDrawWithContent].
325 *
326 * [onDrawBehind] will draw behind the layout's drawing contents however, [onDrawWithContent] will
327 * provide the ability to draw before or after the layout's contents
328 */
329 class CacheDrawScope internal constructor() : Density {
330 internal var cacheParams: BuildDrawCacheParams = EmptyBuildDrawCacheParams
331 internal var drawResult: DrawResult? = null
332 internal var contentDrawScope: ContentDrawScope? = null
333 internal var graphicsContextProvider: (() -> GraphicsContext)? = null
334
335 /** Provides the dimensions of the current drawing environment */
336 val size: Size
337 get() = cacheParams.size
338
339 /** Provides the [LayoutDirection]. */
340 val layoutDirection: LayoutDirection
341 get() = cacheParams.layoutDirection
342
343 /**
344 * Returns a managed [GraphicsLayer] instance. This [GraphicsLayer] maybe newly created or
345 * return a previously allocated instance. Consumers are not expected to release this instance
346 * as it is automatically recycled upon invalidation of the CacheDrawScope and released when the
347 * [DrawCacheModifier] is detached.
348 */
obtainGraphicsLayernull349 fun obtainGraphicsLayer(): GraphicsLayer =
350 graphicsContextProvider!!.invoke().createGraphicsLayer()
351
352 /**
353 * Record the drawing commands into the [GraphicsLayer] with the [Density], [LayoutDirection]
354 * and [Size] are given from the provided [CacheDrawScope]
355 */
356 fun GraphicsLayer.record(
357 density: Density = this@CacheDrawScope,
358 layoutDirection: LayoutDirection = this@CacheDrawScope.layoutDirection,
359 size: IntSize = this@CacheDrawScope.size.toIntSize(),
360 block: ContentDrawScope.() -> Unit
361 ) {
362 val scope = contentDrawScope!!
363 with(scope) {
364 val prevDensity = drawContext.density
365 val prevLayoutDirection = drawContext.layoutDirection
366 record(size) {
367 drawContext.apply {
368 this.density = density
369 this.layoutDirection = layoutDirection
370 }
371 try {
372 block(scope)
373 } finally {
374 drawContext.apply {
375 this.density = prevDensity
376 this.layoutDirection = prevLayoutDirection
377 }
378 }
379 }
380 }
381 }
382
383 /** Issue drawing commands to be executed before the layout content is drawn */
<lambda>null384 fun onDrawBehind(block: DrawScope.() -> Unit): DrawResult = onDrawWithContent {
385 block()
386 drawContent()
387 }
388
389 /** Issue drawing commands before or after the layout's drawing contents */
onDrawWithContentnull390 fun onDrawWithContent(block: ContentDrawScope.() -> Unit): DrawResult {
391 return DrawResult(block).also { drawResult = it }
392 }
393
394 override val density: Float
395 get() = cacheParams.density.density
396
397 override val fontScale: Float
398 get() = cacheParams.density.fontScale
399 }
400
401 private object EmptyBuildDrawCacheParams : BuildDrawCacheParams {
402 override val size: Size = Size.Unspecified
403 override val layoutDirection: LayoutDirection = LayoutDirection.Ltr
404 override val density: Density = Density(1f, 1f)
405 }
406
407 /**
408 * Holder to a callback to be invoked during draw operations. This lambda captures and reuses
409 * parameters defined within the CacheDrawScope receiver scope lambda.
410 */
411 class DrawResult internal constructor(internal var block: ContentDrawScope.() -> Unit)
412
413 /**
414 * Creates a [DrawModifier] that allows the developer to draw before or after the layout's contents.
415 * It also allows the modifier to adjust the layout's canvas.
416 */
drawWithContentnull417 fun Modifier.drawWithContent(onDraw: ContentDrawScope.() -> Unit): Modifier =
418 this then DrawWithContentElement(onDraw)
419
420 private class DrawWithContentElement(val onDraw: ContentDrawScope.() -> Unit) :
421 ModifierNodeElement<DrawWithContentModifier>() {
422 override fun create() = DrawWithContentModifier(onDraw)
423
424 override fun update(node: DrawWithContentModifier) {
425 node.onDraw = onDraw
426 }
427
428 override fun InspectorInfo.inspectableProperties() {
429 name = "drawWithContent"
430 properties["onDraw"] = onDraw
431 }
432
433 override fun equals(other: Any?): Boolean {
434 if (this === other) return true
435 if (other !is DrawWithContentElement) return false
436
437 if (onDraw !== other.onDraw) return false
438
439 return true
440 }
441
442 override fun hashCode(): Int {
443 return onDraw.hashCode()
444 }
445 }
446
447 private class DrawWithContentModifier(var onDraw: ContentDrawScope.() -> Unit) :
448 Modifier.Node(), DrawModifierNode {
449
drawnull450 override fun ContentDrawScope.draw() {
451 onDraw()
452 }
453 }
454