1 /*
2  * Copyright 2020 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.graphics
18 
19 import androidx.annotation.VisibleForTesting
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.ComposableOpenTarget
22 import androidx.compose.runtime.RememberObserver
23 import androidx.compose.runtime.remember
24 import androidx.compose.ui.geometry.Size
25 import androidx.compose.ui.graphics.layer.GraphicsLayer
26 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
27 import androidx.compose.ui.layout.PlacementScopeMarker
28 import androidx.compose.ui.platform.LocalGraphicsContext
29 import androidx.compose.ui.unit.Density
30 import androidx.compose.ui.unit.LayoutDirection
31 import kotlin.js.JsName
32 
33 /** Default camera distance for all layers */
34 const val DefaultCameraDistance = 8.0f
35 
36 /** Default ambient shadow color for all layers. */
37 val DefaultShadowColor = Color.Black
38 
39 /**
40  * A scope which can be used to define the effects to apply for the content, such as scaling
41  * ([scaleX], [scaleY]), rotation ([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow
42  * ([shadowElevation], [shape]), and clipping ([clip], [shape]).
43  */
44 @JvmDefaultWithCompatibility
45 @PlacementScopeMarker
46 interface GraphicsLayerScope : Density {
47     /** The horizontal scale of the drawn area. Default value is `1`. */
48     var scaleX: Float
49 
50     /** The vertical scale of the drawn area. Default value is `1`. */
51     var scaleY: Float
52 
53     /**
54      * The alpha of the drawn area. Setting this to something other than `1` will cause the drawn
55      * contents to be translucent and setting it to `0` will cause it to be fully invisible. Default
56      * value is `1` and the range is between `0` and `1`.
57      */
58     /*@setparam:FloatRange(from = 0.0, to = 1.0)*/
59     var alpha: Float
60 
61     /** Horizontal pixel offset of the layer relative to its left bound. Default value is `0`. */
62     var translationX: Float
63 
64     /** Vertical pixel offset of the layer relative to its top bound. Default value is `0` */
65     var translationY: Float
66 
67     /**
68      * Sets the elevation for the shadow in pixels. With the [shadowElevation] > 0f and [shape] set,
69      * a shadow is produced. Default value is `0` and the value must not be negative.
70      *
71      * Note that if you provide a non-zero [shadowElevation] and if the passed [shape] is concave
72      * the shadow will not be drawn on Android versions less than 10.
73      */
74     /*@setparam:FloatRange(from = 0.0)*/
75     var shadowElevation: Float
76 
77     /**
78      * Sets the color of the ambient shadow that is drawn when [shadowElevation] > 0f.
79      *
80      * By default the shadow color is black. Generally, this color will be opaque so the intensity
81      * of the shadow is consistent between different graphics layers with different colors.
82      *
83      * The opacity of the final ambient shadow is a function of the shadow caster height, the alpha
84      * channel of the [ambientShadowColor] (typically opaque), and the
85      * [android.R.attr.ambientShadowAlpha] theme attribute.
86      *
87      * Note that this parameter is only supported on Android 9 (Pie) and above. On older versions,
88      * this property always returns [Color.Black] and setting new values is ignored.
89      */
90     // Add default getter/setter implementation to avoid breaking api changes due to abstract
91     // method additions. ReusableGraphicsLayer is the only implementation anyway.
92     var ambientShadowColor: Color
93         get() = DefaultShadowColor
94         // Keep the parameter name so current.txt maintains it for named parameter usage
95         @Suppress("UNUSED_PARAMETER") set(ambientShadowColor) {}
96 
97     /**
98      * Sets the color of the spot shadow that is drawn when [shadowElevation] > 0f.
99      *
100      * By default the shadow color is black. Generally, this color will be opaque so the intensity
101      * of the shadow is consistent between different graphics layers with different colors.
102      *
103      * The opacity of the final spot shadow is a function of the shadow caster height, the alpha
104      * channel of the [spotShadowColor] (typically opaque), and the [android.R.attr.spotShadowAlpha]
105      * theme attribute.
106      *
107      * Note that this parameter is only supported on Android 9 (Pie) and above. On older versions,
108      * this property always returns [Color.Black] and setting new values is ignored.
109      */
110     // Add default getter/setter implementation to avoid breaking api changes due to abstract
111     // method additions. ReusableGraphicsLayer is the only implementation anyway.
112     var spotShadowColor: Color
113         get() = DefaultShadowColor
114         // Keep the parameter name so current.txt maintains it for named parameter usage
115         @Suppress("UNUSED_PARAMETER") set(spotShadowColor) {}
116 
117     /**
118      * The rotation, in degrees, of the contents around the horizontal axis in degrees. Default
119      * value is `0`.
120      */
121     var rotationX: Float
122 
123     /**
124      * The rotation, in degrees, of the contents around the vertical axis in degrees. Default value
125      * is `0`.
126      */
127     var rotationY: Float
128 
129     /**
130      * The rotation, in degrees, of the contents around the Z axis in degrees. Default value is `0`.
131      */
132     var rotationZ: Float
133 
134     /**
135      * Sets the distance along the Z axis (orthogonal to the X/Y plane on which layers are drawn)
136      * from the camera to this layer. The camera's distance affects 3D transformations, for instance
137      * rotations around the X and Y axis. If the rotationX or rotationY properties are changed and
138      * this view is large (more than half the size of the screen), it is recommended to always use a
139      * camera distance that's greater than the height (X axis rotation) or the width (Y axis
140      * rotation) of this view.
141      *
142      * The distance of the camera from the drawing plane can have an affect on the perspective
143      * distortion of the layer when it is rotated around the x or y axis. For example, a large
144      * distance will result in a large viewing angle, and there will not be much perspective
145      * distortion of the view as it rotates. A short distance may cause much more perspective
146      * distortion upon rotation, and can also result in some drawing artifacts if the rotated view
147      * ends up partially behind the camera (which is why the recommendation is to use a distance at
148      * least as far as the size of the view, if the view is to be rotated.)
149      *
150      * The distance is expressed in pixels and must always be positive. Default value is
151      * [DefaultCameraDistance]
152      */
153     /*@setparam:FloatRange(from = 0.0)*/
154     var cameraDistance: Float
155 
156     /**
157      * Offset percentage along the x and y axis for which contents are rotated and scaled. The
158      * default value of 0.5f, 0.5f indicates the pivot point will be at the midpoint of the left and
159      * right as well as the top and bottom bounds of the layer. Default value is
160      * [TransformOrigin.Center]
161      */
162     var transformOrigin: TransformOrigin
163 
164     /**
165      * The [Shape] of the layer. When [shadowElevation] is non-zero a shadow is produced using this
166      * [shape]. When [clip] is `true` contents will be clipped to this [shape]. When clipping, the
167      * content will be redrawn when the [shape] changes. Default value is [RectangleShape]
168      */
169     var shape: Shape
170 
171     /** Set to `true` to clip the content to the [shape]. Default value is `false` */
172     @Suppress("GetterSetterNames") @get:Suppress("GetterSetterNames") var clip: Boolean
173 
174     /**
175      * Configure the [RenderEffect] to apply to this [GraphicsLayerScope]. This will apply a visual
176      * effect to the results of the [GraphicsLayerScope] before it is drawn. For example if
177      * [BlurEffect] is provided, the contents will be drawn in a separate layer, then this layer
178      * will be blurred when this [GraphicsLayerScope] is drawn.
179      *
180      * Note this parameter is only supported on Android 12 and above. Attempts to use this Modifier
181      * on older Android versions will be ignored.
182      */
183     var renderEffect: RenderEffect?
184         get() = null
185         set(_) {}
186 
187     /**
188      * BlendMode to use when drawing this layer to the destination. The default is
189      * [BlendMode.SrcOver]. Any value other than [BlendMode.SrcOver] will force this
190      * [GraphicsLayerScope] to use an offscreen compositing layer for rendering and is equivalent to
191      * using [CompositingStrategy.Offscreen].
192      */
193     var blendMode: BlendMode
194         get() = BlendMode.SrcOver
195         set(_) {}
196 
197     /**
198      * ColorFilter applied when drawing this layer to the destination. Setting of this to any
199      * non-null will force this [GraphicsLayer] to use an offscreen compositing layer for rendering
200      * and is equivalent to using [CompositingStrategy.Offscreen]
201      */
202     var colorFilter: ColorFilter?
203         get() = null
204         set(_) {}
205 
206     /**
207      * Determines the [CompositingStrategy] used to render the contents of this graphicsLayer into
208      * an offscreen buffer first before rendering to the destination
209      */
210     var compositingStrategy: CompositingStrategy
211         get() = CompositingStrategy.Auto
212         // Keep the parameter name so current.txt maintains it for named parameter usage
213         @Suppress("UNUSED_PARAMETER") set(compositingStrategy) {}
214 
215     /**
216      * [Size] of the graphicsLayer represented in pixels. Drawing commands can extend beyond the
217      * size specified, however, if the graphicsLayer is promoted to an offscreen rasterization
218      * layer, any content rendered outside of the specified size will be clipped.
219      */
220     val size: Size
221         get() = Size.Unspecified
222 }
223 
224 private class GraphicsContextObserver(private val graphicsContext: GraphicsContext) :
225     RememberObserver {
226 
227     val graphicsLayer = graphicsContext.createGraphicsLayer()
228 
onRememberednull229     override fun onRemembered() {
230         // NO-OP
231     }
232 
onForgottennull233     override fun onForgotten() {
234         graphicsContext.releaseGraphicsLayer(graphicsLayer)
235     }
236 
onAbandonednull237     override fun onAbandoned() {
238         graphicsContext.releaseGraphicsLayer(graphicsLayer)
239     }
240 }
241 
242 /**
243  * Create a new [GraphicsLayer] instance that will automatically be released when the Composable is
244  * disposed.
245  *
246  * @return a GraphicsLayer instance
247  */
248 @Composable
249 @ComposableOpenTarget(-1)
rememberGraphicsLayernull250 fun rememberGraphicsLayer(): GraphicsLayer {
251     val graphicsContext = LocalGraphicsContext.current
252     return remember { GraphicsContextObserver(graphicsContext) }.graphicsLayer
253 }
254 
255 /** Creates simple [GraphicsLayerScope]. */
256 @JsName("funGraphicsLayerScope")
GraphicsLayerScopenull257 fun GraphicsLayerScope(): GraphicsLayerScope = ReusableGraphicsLayerScope()
258 
259 internal object Fields {
260     const val ScaleX: Int = 0b1 shl 0
261     const val ScaleY: Int = 0b1 shl 1
262     const val Alpha: Int = 0b1 shl 2
263     const val TranslationX: Int = 0b1 shl 3
264     const val TranslationY: Int = 0b1 shl 4
265     const val ShadowElevation: Int = 0b1 shl 5
266     const val AmbientShadowColor: Int = 0b1 shl 6
267     const val SpotShadowColor: Int = 0b1 shl 7
268     const val RotationX: Int = 0b1 shl 8
269     const val RotationY: Int = 0b1 shl 9
270     const val RotationZ: Int = 0b1 shl 10
271     const val CameraDistance: Int = 0b1 shl 11
272     const val TransformOrigin: Int = 0b1 shl 12
273     const val Shape: Int = 0b1 shl 13
274     const val Clip: Int = 0b1 shl 14
275     const val CompositingStrategy: Int = 0b1 shl 15
276     const val RenderEffect: Int = 0b1 shl 17
277     const val ColorFilter: Int = 0b1 shl 18
278     const val BlendMode: Int = 0b1 shl 19
279 
280     const val MatrixAffectingFields =
281         ScaleX or
282             ScaleY or
283             TranslationX or
284             TranslationY or
285             TransformOrigin or
286             RotationX or
287             RotationY or
288             RotationZ or
289             CameraDistance
290 }
291 
292 internal class ReusableGraphicsLayerScope : GraphicsLayerScope {
293     internal var mutatedFields: Int = 0
294 
295     override var scaleX: Float = 1f
296         set(value) {
297             if (field != value) {
298                 mutatedFields = mutatedFields or Fields.ScaleX
299                 field = value
300             }
301         }
302 
303     override var scaleY: Float = 1f
304         set(value) {
305             if (field != value) {
306                 mutatedFields = mutatedFields or Fields.ScaleY
307                 field = value
308             }
309         }
310 
311     override var alpha: Float = 1f
312         set(value) {
313             if (field != value) {
314                 mutatedFields = mutatedFields or Fields.Alpha
315                 field = value
316             }
317         }
318 
319     override var translationX: Float = 0f
320         set(value) {
321             if (field != value) {
322                 mutatedFields = mutatedFields or Fields.TranslationX
323                 field = value
324             }
325         }
326 
327     override var translationY: Float = 0f
328         set(value) {
329             if (field != value) {
330                 mutatedFields = mutatedFields or Fields.TranslationY
331                 field = value
332             }
333         }
334 
335     override var shadowElevation: Float = 0f
336         set(value) {
337             if (field != value) {
338                 mutatedFields = mutatedFields or Fields.ShadowElevation
339                 field = value
340             }
341         }
342 
343     override var ambientShadowColor: Color = DefaultShadowColor
344         set(value) {
345             if (field != value) {
346                 mutatedFields = mutatedFields or Fields.AmbientShadowColor
347                 field = value
348             }
349         }
350 
351     override var spotShadowColor: Color = DefaultShadowColor
352         set(value) {
353             if (field != value) {
354                 mutatedFields = mutatedFields or Fields.SpotShadowColor
355                 field = value
356             }
357         }
358 
359     override var rotationX: Float = 0f
360         set(value) {
361             if (field != value) {
362                 mutatedFields = mutatedFields or Fields.RotationX
363                 field = value
364             }
365         }
366 
367     override var rotationY: Float = 0f
368         set(value) {
369             if (field != value) {
370                 mutatedFields = mutatedFields or Fields.RotationY
371                 field = value
372             }
373         }
374 
375     override var rotationZ: Float = 0f
376         set(value) {
377             if (field != value) {
378                 mutatedFields = mutatedFields or Fields.RotationZ
379                 field = value
380             }
381         }
382 
383     override var cameraDistance: Float = DefaultCameraDistance
384         set(value) {
385             if (field != value) {
386                 mutatedFields = mutatedFields or Fields.CameraDistance
387                 field = value
388             }
389         }
390 
391     override var transformOrigin: TransformOrigin = TransformOrigin.Center
392         set(value) {
393             if (field != value) {
394                 mutatedFields = mutatedFields or Fields.TransformOrigin
395                 field = value
396             }
397         }
398 
399     override var shape: Shape = RectangleShape
400         set(value) {
401             if (field != value) {
402                 mutatedFields = mutatedFields or Fields.Shape
403                 field = value
404             }
405         }
406 
407     override var clip: Boolean = false
408         set(value) {
409             if (field != value) {
410                 mutatedFields = mutatedFields or Fields.Clip
411                 field = value
412             }
413         }
414 
415     override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
416         set(value) {
417             if (field != value) {
418                 mutatedFields = mutatedFields or Fields.CompositingStrategy
419                 field = value
420             }
421         }
422 
423     override var size: Size = Size.Unspecified
424 
425     internal var graphicsDensity: Density = Density(1.0f)
426 
427     internal var layoutDirection: LayoutDirection = LayoutDirection.Ltr
428 
429     override val density: Float
430         get() = graphicsDensity.density
431 
432     override val fontScale: Float
433         get() = graphicsDensity.fontScale
434 
435     override var renderEffect: RenderEffect? = null
436         set(value) {
437             if (field != value) {
438                 mutatedFields = mutatedFields or Fields.RenderEffect
439                 field = value
440             }
441         }
442 
443     override var colorFilter: ColorFilter? = null
444         set(value) {
445             if (field != value) {
446                 mutatedFields = mutatedFields or Fields.ColorFilter
447                 field = value
448             }
449         }
450 
451     override var blendMode: BlendMode = BlendMode.SrcOver
452         set(value) {
453             if (field != value) {
454                 mutatedFields = mutatedFields or Fields.BlendMode
455                 field = value
456             }
457         }
458 
459     internal var outline: Outline? = null
460         @VisibleForTesting internal set
461 
resetnull462     fun reset() {
463         scaleX = 1f
464         scaleY = 1f
465         alpha = 1f
466         translationX = 0f
467         translationY = 0f
468         shadowElevation = 0f
469         ambientShadowColor = DefaultShadowColor
470         spotShadowColor = DefaultShadowColor
471         rotationX = 0f
472         rotationY = 0f
473         rotationZ = 0f
474         cameraDistance = DefaultCameraDistance
475         transformOrigin = TransformOrigin.Center
476         shape = RectangleShape
477         clip = false
478         renderEffect = null
479         colorFilter = null
480         blendMode = BlendMode.SrcOver
481         compositingStrategy = CompositingStrategy.Auto
482         size = Size.Unspecified
483         outline = null
484         // mutatedFields should be reset last as all the setters above modify it.
485         mutatedFields = 0
486     }
487 
updateOutlinenull488     internal fun updateOutline() {
489         outline = shape.createOutline(size, layoutDirection, graphicsDensity)
490     }
491 }
492