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