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