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.drawscope
18
19 import androidx.annotation.FloatRange
20 import androidx.compose.ui.geometry.CornerRadius
21 import androidx.compose.ui.geometry.Offset
22 import androidx.compose.ui.geometry.Size
23 import androidx.compose.ui.geometry.center
24 import androidx.compose.ui.graphics.BlendMode
25 import androidx.compose.ui.graphics.Brush
26 import androidx.compose.ui.graphics.Canvas
27 import androidx.compose.ui.graphics.ClipOp
28 import androidx.compose.ui.graphics.Color
29 import androidx.compose.ui.graphics.ColorFilter
30 import androidx.compose.ui.graphics.FilterQuality
31 import androidx.compose.ui.graphics.ImageBitmap
32 import androidx.compose.ui.graphics.Paint
33 import androidx.compose.ui.graphics.Path
34 import androidx.compose.ui.graphics.PathEffect
35 import androidx.compose.ui.graphics.PointMode
36 import androidx.compose.ui.graphics.StrokeCap
37 import androidx.compose.ui.graphics.StrokeJoin
38 import androidx.compose.ui.graphics.degrees
39 import androidx.compose.ui.graphics.internal.JvmDefaultWithCompatibility
40 import androidx.compose.ui.graphics.layer.GraphicsLayer
41 import androidx.compose.ui.unit.Density
42 import androidx.compose.ui.unit.IntOffset
43 import androidx.compose.ui.unit.IntSize
44 import androidx.compose.ui.unit.LayoutDirection
45 import androidx.compose.ui.unit.toIntSize
46
47 /**
48 * Simultaneously translate the [DrawScope] coordinate space by [left] and [top] as well as modify
49 * the dimensions of the current painting area. This provides a callback to issue more drawing
50 * instructions within the modified coordinate space. This method modifies the width of the
51 * [DrawScope] to be equivalent to width - (left + right) as well as height to height - (top +
52 * bottom). After this method is invoked, the coordinate space is returned to the state before the
53 * inset was applied.
54 *
55 * @param left number of pixels to inset the left drawing bound
56 * @param top number of pixels to inset the top drawing bound
57 * @param right number of pixels to inset the right drawing bound
58 * @param bottom number of pixels to inset the bottom drawing bound
59 * @param block lambda that is called to issue drawing commands within the inset coordinate space
60 */
insetnull61 inline fun DrawScope.inset(
62 left: Float,
63 top: Float,
64 right: Float,
65 bottom: Float,
66 block: DrawScope.() -> Unit
67 ) {
68 drawContext.transform.inset(left, top, right, bottom)
69 try {
70 block()
71 } finally {
72 drawContext.transform.inset(-left, -top, -right, -bottom)
73 }
74 }
75
76 /**
77 * Convenience method modifies the [DrawScope] bounds to inset both left, top, right and bottom
78 * bounds by [inset]. After this method is invoked, the coordinate space is returned to the state
79 * before this inset was applied.
80 *
81 * @param inset number of pixels to inset left, top, right, and bottom bounds.
82 * @param block lambda that is called to issue additional drawing commands within the modified
83 * coordinate space
84 */
insetnull85 inline fun DrawScope.inset(inset: Float, block: DrawScope.() -> Unit) {
86 drawContext.transform.inset(inset, inset, inset, inset)
87 try {
88 block()
89 } finally {
90 drawContext.transform.inset(-inset, -inset, -inset, -inset)
91 }
92 }
93
94 /**
95 * Convenience method modifies the [DrawScope] bounds to inset both left and right bounds by
96 * [horizontal] as well as the top and bottom by [vertical]. After this method is invoked, the
97 * coordinate space is returned to the state before this inset was applied.
98 *
99 * @param horizontal number of pixels to inset both left and right bounds. Zero by default
100 * @param vertical Optional number of pixels to inset both top and bottom bounds. Zero by default
101 * @param block lambda that is called to issue additional drawing commands within the modified
102 * coordinate space
103 */
insetnull104 inline fun DrawScope.inset(
105 horizontal: Float = 0.0f,
106 vertical: Float = 0.0f,
107 block: DrawScope.() -> Unit
108 ) = inset(horizontal, vertical, horizontal, vertical, block)
109
110 /**
111 * Translate the coordinate space by the given delta in pixels in both the x and y coordinates
112 * respectively
113 *
114 * @param left Pixels to translate the coordinate space in the x-axis
115 * @param top Pixels to translate the coordinate space in the y-axis
116 * @param block lambda that is called to issue drawing commands within the translated coordinate
117 * space
118 */
119 inline fun DrawScope.translate(left: Float = 0.0f, top: Float = 0.0f, block: DrawScope.() -> Unit) {
120 drawContext.transform.translate(left, top)
121 try {
122 block()
123 } finally {
124 drawContext.transform.translate(-left, -top)
125 }
126 }
127
128 /**
129 * Add a rotation (in degrees clockwise) to the current transform at the given pivot point. The
130 * pivot coordinate remains unchanged by the rotation transformation. After the provided lambda is
131 * invoked, the rotation transformation is undone.
132 *
133 * @param degrees to rotate clockwise
134 * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
135 * @param block lambda that is called to issue drawing commands within the rotated coordinate space
136 */
rotatenull137 inline fun DrawScope.rotate(degrees: Float, pivot: Offset = center, block: DrawScope.() -> Unit) =
138 withTransform({ rotate(degrees, pivot) }, block)
139
140 /**
141 * Add a rotation (in radians clockwise) to the current transform at the given pivot point. The
142 * pivot coordinate remains unchanged by the rotation transformation
143 *
144 * @param radians to rotate clockwise
145 * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
146 * @param block lambda that is called to issue drawing commands within the rotated coordinate space
147 */
rotateRadnull148 inline fun DrawScope.rotateRad(
149 radians: Float,
150 pivot: Offset = center,
151 block: DrawScope.() -> Unit
152 ) {
153 withTransform({ rotate(degrees(radians), pivot) }, block)
154 }
155
156 /**
157 * Add an axis-aligned scale to the current transform, scaling by the first argument in the
158 * horizontal direction and the second in the vertical direction at the given pivot coordinate. The
159 * pivot coordinate remains unchanged by the scale transformation. After this method is invoked, the
160 * coordinate space is returned to the state before the scale was applied.
161 *
162 * @param scaleX The amount to scale in X
163 * @param scaleY The amount to scale in Y
164 * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
165 * @param block lambda used to issue drawing commands within the scaled coordinate space
166 */
scalenull167 inline fun DrawScope.scale(
168 scaleX: Float,
169 scaleY: Float,
170 pivot: Offset = center,
171 block: DrawScope.() -> Unit
172 ) = withTransform({ scale(scaleX, scaleY, pivot) }, block)
173
174 /**
175 * Add an axis-aligned scale to the current transform, scaling both the horizontal direction and the
176 * vertical direction at the given pivot coordinate. The pivot coordinate remains unchanged by the
177 * scale transformation. After this method is invoked, the coordinate space is returned to the state
178 * before the scale was applied.
179 *
180 * @param scale The amount to scale uniformly in both directions
181 * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
182 * @param block lambda used to issue drawing commands within the scaled coordinate space
183 */
scalenull184 inline fun DrawScope.scale(scale: Float, pivot: Offset = center, block: DrawScope.() -> Unit) =
185 withTransform({ scale(scale, scale, pivot) }, block)
186
187 /**
188 * Reduces the clip region to the intersection of the current clip and the given rectangle indicated
189 * by the given left, top, right and bottom bounds. This provides a callback to issue drawing
190 * commands within the clipped region. After this method is invoked, this clip is no longer applied.
191 *
192 * Use [ClipOp.Difference] to subtract the provided rectangle from the current clip.
193 *
194 * @param left Left bound of the rectangle to clip
195 * @param top Top bound of the rectangle to clip
196 * @param right Right bound of the rectangle to clip
197 * @param bottom Bottom bound of the rectangle to clip
198 * @param clipOp Clipping operation to conduct on the given bounds, defaults to [ClipOp.Intersect]
199 * @param block Lambda callback with this CanvasScope as a receiver scope to issue drawing commands
200 * within the provided clip
201 */
clipRectnull202 inline fun DrawScope.clipRect(
203 left: Float = 0.0f,
204 top: Float = 0.0f,
205 right: Float = size.width,
206 bottom: Float = size.height,
207 clipOp: ClipOp = ClipOp.Intersect,
208 block: DrawScope.() -> Unit
209 ) = withTransform({ clipRect(left, top, right, bottom, clipOp) }, block)
210
211 /**
212 * Reduces the clip region to the intersection of the current clip and the given path. This method
213 * provides a callback to issue drawing commands within the region defined by the clipped path.
214 * After this method is invoked, this clip is no longer applied.
215 *
216 * @param path Shape to clip drawing content within
217 * @param clipOp Clipping operation to conduct on the given bounds, defaults to [ClipOp.Intersect]
218 * @param block Lambda callback with this CanvasScope as a receiver scope to issue drawing commands
219 * within the provided clip
220 */
clipPathnull221 inline fun DrawScope.clipPath(
222 path: Path,
223 clipOp: ClipOp = ClipOp.Intersect,
224 block: DrawScope.() -> Unit
225 ) = withTransform({ clipPath(path, clipOp) }, block)
226
227 /**
228 * Provides access to draw directly with the underlying [Canvas]. This is helpful for situations to
229 * re-use alternative drawing logic in combination with [DrawScope]
230 *
231 * @param block Lambda callback to issue drawing commands on the provided [Canvas]
232 */
drawIntoCanvasnull233 inline fun DrawScope.drawIntoCanvas(block: (Canvas) -> Unit) = block(drawContext.canvas)
234
235 /**
236 * Perform 1 or more transformations and execute drawing commands with the specified transformations
237 * applied. After this call is complete, the transformation before this call was made is restored
238 *
239 * @sample androidx.compose.ui.graphics.samples.DrawScopeBatchedTransformSample
240 * @param transformBlock Callback invoked to issue transformations to be made before the drawing
241 * operations are issued
242 * @param drawBlock Callback invoked to issue drawing operations after the transformations are
243 * applied
244 */
245 inline fun DrawScope.withTransform(
246 transformBlock: DrawTransform.() -> Unit,
247 drawBlock: DrawScope.() -> Unit
248 ) =
249 with(drawContext) {
250 // Transformation can include inset calls which change the drawing area
251 // so cache the previous size before the transformation is done
252 // and reset it afterwards
253 val previousSize = size
254 canvas.save()
255 try {
256 transformBlock(transform)
257 drawBlock()
258 } finally {
259 canvas.restore()
260 size = previousSize
261 }
262 }
263
264 @Deprecated(
265 message = "Please use a new overload accepting nullable GraphicsLayer",
266 level = DeprecationLevel.HIDDEN
267 )
drawnull268 inline fun DrawScope.draw(
269 density: Density,
270 layoutDirection: LayoutDirection,
271 canvas: Canvas,
272 size: Size,
273 block: DrawScope.() -> Unit
274 ) {
275 draw(density, layoutDirection, canvas, size, null, block)
276 }
277
278 /**
279 * Draws into the provided [Canvas] with the commands specified in the lambda with this [DrawScope]
280 * as a receiver
281 *
282 * @sample androidx.compose.ui.graphics.samples.DrawScopeRetargetingSample
283 * @param density [Density] used to assist in conversions of density independent pixels to raw
284 * pixels to draw
285 * @param layoutDirection [LayoutDirection] of the layout being drawn in.
286 * @param canvas target canvas to render into
287 * @param size bounds relative to the current canvas translation in which the [DrawScope] should
288 * draw within
289 * @param graphicsLayer Current [GraphicsLayer] we are drawing into. Might be null if the [canvas]
290 * is not provided by a [GraphicsLayer], for example in the case of a software-accelerated drawing
291 * @param block lambda that is called to issue drawing commands on this [DrawScope]
292 */
drawnull293 inline fun DrawScope.draw(
294 density: Density,
295 layoutDirection: LayoutDirection,
296 canvas: Canvas,
297 size: Size,
298 graphicsLayer: GraphicsLayer? = null,
299 block: DrawScope.() -> Unit
300 ) {
301 // Remember the previous drawing parameters in case we are temporarily re-directing our
302 // drawing to a separate Layer/RenderNode only to draw that content back into the original
303 // Canvas. If there is no previous canvas that was being drawing into, this ends up
304 // resetting these parameters back to defaults defensively
305 val prevDensity = drawContext.density
306 val prevLayoutDirection = drawContext.layoutDirection
307 val prevCanvas = drawContext.canvas
308 val prevSize = drawContext.size
309 val prevLayer = drawContext.graphicsLayer
310 drawContext.apply {
311 this.density = density
312 this.layoutDirection = layoutDirection
313 this.canvas = canvas
314 this.size = size
315 this.graphicsLayer = graphicsLayer
316 }
317 canvas.save()
318 try {
319 this.block()
320 } finally {
321 canvas.restore()
322 drawContext.apply {
323 this.density = prevDensity
324 this.layoutDirection = prevLayoutDirection
325 this.canvas = prevCanvas
326 this.size = prevSize
327 this.graphicsLayer = prevLayer
328 }
329 }
330 }
331
332 /**
333 * Creates a scoped drawing environment with the provided [Canvas]. This provides a declarative,
334 * stateless API to draw shapes and paths without requiring consumers to maintain underlying
335 * [Canvas] state information. [DrawScope] implementations are also provided sizing information and
336 * transformations are done relative to the local translation. That is left and top coordinates are
337 * always the origin and the right and bottom coordinates are always the specified width and height
338 * respectively. Drawing content is not clipped, so it is possible to draw outside of the specified
339 * bounds.
340 *
341 * @sample androidx.compose.ui.graphics.samples.DrawScopeSample
342 */
343 @DrawScopeMarker
344 @JvmDefaultWithCompatibility
345 interface DrawScope : Density {
346
347 /**
348 * The current [DrawContext] that contains the dependencies needed to create the drawing
349 * environment
350 */
351 val drawContext: DrawContext
352
353 /** Center of the current bounds of the drawing environment */
354 val center: Offset
355 get() = drawContext.size.center
356
357 /** Provides the dimensions of the current drawing environment */
358 val size: Size
359 get() = drawContext.size
360
361 /** The layout direction of the layout being drawn in. */
362 val layoutDirection: LayoutDirection
363
364 /**
365 * Draws a line between the given points using the given paint. The line is stroked.
366 *
367 * @param brush the color or fill to be applied to the line
368 * @param start first point of the line to be drawn
369 * @param end second point of the line to be drawn
370 * @param strokeWidth stroke width to apply to the line
371 * @param cap treatment applied to the ends of the line segment
372 * @param pathEffect optional effect or pattern to apply to the line
373 * @param alpha opacity to be applied to the [brush] from 0.0f to 1.0f representing fully
374 * transparent to fully opaque respectively
375 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
376 * @param blendMode the blending algorithm to apply to the [brush]
377 */
drawLinenull378 fun drawLine(
379 brush: Brush,
380 start: Offset,
381 end: Offset,
382 strokeWidth: Float = Stroke.HairlineWidth,
383 cap: StrokeCap = Stroke.DefaultCap,
384 pathEffect: PathEffect? = null,
385 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
386 colorFilter: ColorFilter? = null,
387 blendMode: BlendMode = DefaultBlendMode
388 )
389
390 /**
391 * Draws a line between the given points using the given paint. The line is stroked.
392 *
393 * @param color the color to be applied to the line
394 * @param start first point of the line to be drawn
395 * @param end second point of the line to be drawn
396 * @param strokeWidth The stroke width to apply to the line
397 * @param cap treatment applied to the ends of the line segment
398 * @param pathEffect optional effect or pattern to apply to the line
399 * @param alpha opacity to be applied to the [color] from 0.0f to 1.0f representing fully
400 * transparent to fully opaque respectively
401 * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
402 * @param blendMode the blending algorithm to apply to the [color]
403 */
404 fun drawLine(
405 color: Color,
406 start: Offset,
407 end: Offset,
408 strokeWidth: Float = Stroke.HairlineWidth,
409 cap: StrokeCap = Stroke.DefaultCap,
410 pathEffect: PathEffect? = null,
411 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
412 colorFilter: ColorFilter? = null,
413 blendMode: BlendMode = DefaultBlendMode
414 )
415
416 /**
417 * Draws a rectangle with the given offset and size. If no offset from the top left is provided,
418 * it is drawn starting from the origin of the current translation. If no size is provided, the
419 * size of the current environment is used.
420 *
421 * @param brush The color or fill to be applied to the rectangle
422 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
423 * @param size Dimensions of the rectangle to draw
424 * @param alpha Opacity to be applied to the [brush] from 0.0f to 1.0f representing fully
425 * transparent to fully opaque respectively
426 * @param style Whether or not the rectangle is stroked or filled in
427 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
428 * @param blendMode Blending algorithm to apply to destination
429 */
430 fun drawRect(
431 brush: Brush,
432 topLeft: Offset = Offset.Zero,
433 size: Size = this.size.offsetSize(topLeft),
434 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
435 style: DrawStyle = Fill,
436 colorFilter: ColorFilter? = null,
437 blendMode: BlendMode = DefaultBlendMode
438 )
439
440 /**
441 * Draws a rectangle with the given offset and size. If no offset from the top left is provided,
442 * it is drawn starting from the origin of the current translation. If no size is provided, the
443 * size of the current environment is used.
444 *
445 * @param color The color to be applied to the rectangle
446 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
447 * @param size Dimensions of the rectangle to draw
448 * @param alpha Opacity to be applied to the [color] from 0.0f to 1.0f representing fully
449 * transparent to fully opaque respectively
450 * @param style Whether or not the rectangle is stroked or filled in
451 * @param colorFilter ColorFilter to apply to the [color] source pixels
452 * @param blendMode Blending algorithm to apply to destination
453 */
454 fun drawRect(
455 color: Color,
456 topLeft: Offset = Offset.Zero,
457 size: Size = this.size.offsetSize(topLeft),
458 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
459 style: DrawStyle = Fill,
460 colorFilter: ColorFilter? = null,
461 blendMode: BlendMode = DefaultBlendMode
462 )
463
464 /**
465 * Draws the given [ImageBitmap] into the canvas with its top-left corner at the given [Offset].
466 * The image is composited into the canvas using the given [Paint].
467 *
468 * @param image The [ImageBitmap] to draw
469 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
470 * @param alpha Opacity to be applied to [image] from 0.0f to 1.0f representing fully
471 * transparent to fully opaque respectively
472 * @param style Specifies whether the image is to be drawn filled in or as a rectangular stroke
473 * @param colorFilter ColorFilter to apply to the [image] when drawn into the destination
474 * @param blendMode Blending algorithm to apply to destination
475 */
476 fun drawImage(
477 image: ImageBitmap,
478 topLeft: Offset = Offset.Zero,
479 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
480 style: DrawStyle = Fill,
481 colorFilter: ColorFilter? = null,
482 blendMode: BlendMode = DefaultBlendMode
483 )
484
485 /**
486 * Draws the subset of the given image described by the `src` argument into the canvas in the
487 * axis-aligned rectangle given by the `dst` argument.
488 *
489 * If no src rect is provided, the entire image is scaled into the corresponding destination
490 * bounds
491 *
492 * @param image The source image to draw
493 * @param srcOffset Optional offset representing the top left offset of the source image to
494 * draw, this defaults to the origin of [image]
495 * @param srcSize Optional dimensions of the source image to draw relative to [srcOffset], this
496 * defaults the width and height of [image]
497 * @param dstOffset Optional offset representing the top left offset of the destination to draw
498 * the given image, this defaults to the origin of the current translation tarting top left
499 * offset in the destination to draw the image
500 * @param dstSize Optional dimensions of the destination to draw, this defaults to [srcSize]
501 * @param alpha Opacity to be applied to [image] from 0.0f to 1.0f representing fully
502 * transparent to fully opaque respectively
503 * @param style Specifies whether the image is to be drawn filled in or as a rectangular stroke
504 * @param colorFilter ColorFilter to apply to the [image] when drawn into the destination
505 * @param blendMode Blending algorithm to apply to destination
506 */
507 @Deprecated(
508 "Prefer usage of drawImage that consumes an optional FilterQuality parameter",
509 level = DeprecationLevel.HIDDEN,
510 replaceWith =
511 ReplaceWith(
512 "drawImage(image, srcOffset, srcSize, dstOffset, dstSize, alpha, style, " +
513 "colorFilter, blendMode, FilterQuality.Low)",
514 "androidx.compose.ui.graphics.drawscope",
515 "androidx.compose.ui.graphics.FilterQuality"
516 )
517 ) // Binary API compatibility.
518 fun drawImage(
519 image: ImageBitmap,
520 srcOffset: IntOffset = IntOffset.Zero,
521 srcSize: IntSize = IntSize(image.width, image.height),
522 dstOffset: IntOffset = IntOffset.Zero,
523 dstSize: IntSize = srcSize,
524 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
525 style: DrawStyle = Fill,
526 colorFilter: ColorFilter? = null,
527 blendMode: BlendMode = DefaultBlendMode
528 )
529
530 /**
531 * Draws the subset of the given image described by the `src` argument into the canvas in the
532 * axis-aligned rectangle given by the `dst` argument.
533 *
534 * If no src rect is provided, the entire image is scaled into the corresponding destination
535 * bounds
536 *
537 * @param image The source image to draw
538 * @param srcOffset Optional offset representing the top left offset of the source image to
539 * draw, this defaults to the origin of [image]
540 * @param srcSize Optional dimensions of the source image to draw relative to [srcOffset], this
541 * defaults the width and height of [image]
542 * @param dstOffset Optional offset representing the top left offset of the destination to draw
543 * the given image, this defaults to the origin of the current translation tarting top left
544 * offset in the destination to draw the image
545 * @param dstSize Optional dimensions of the destination to draw, this defaults to [srcSize]
546 * @param alpha Opacity to be applied to [image] from 0.0f to 1.0f representing fully
547 * transparent to fully opaque respectively
548 * @param style Specifies whether the image is to be drawn filled in or as a rectangular stroke
549 * @param colorFilter ColorFilter to apply to the [image] when drawn into the destination
550 * @param blendMode Blending algorithm to apply to destination
551 * @param filterQuality Sampling algorithm applied to the [image] when it is scaled and drawn
552 * into the destination. The default is [FilterQuality.Low] which scales using a bilinear
553 * sampling algorithm
554 */
555 fun drawImage(
556 image: ImageBitmap,
557 srcOffset: IntOffset = IntOffset.Zero,
558 srcSize: IntSize = IntSize(image.width, image.height),
559 dstOffset: IntOffset = IntOffset.Zero,
560 dstSize: IntSize = srcSize,
561 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
562 style: DrawStyle = Fill,
563 colorFilter: ColorFilter? = null,
564 blendMode: BlendMode = DefaultBlendMode,
565 filterQuality: FilterQuality = DefaultFilterQuality
566 ) {
567 drawImage(
568 image = image,
569 srcOffset = srcOffset,
570 srcSize = srcSize,
571 dstOffset = dstOffset,
572 dstSize = dstSize,
573 alpha = alpha,
574 style = style,
575 colorFilter = colorFilter,
576 blendMode = blendMode
577 )
578 }
579
580 /**
581 * Draws a rounded rectangle with the provided size, offset and radii for the x and y axis
582 * respectively. This rectangle is drawn with the provided [Brush] parameter and is filled or
583 * stroked based on the given [DrawStyle]
584 *
585 * @param brush The color or fill to be applied to the rounded rectangle
586 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
587 * @param size Dimensions of the rectangle to draw
588 * @param cornerRadius Corner radius of the rounded rectangle, negative radii values are clamped
589 * to 0
590 * @param alpha Opacity to be applied to rounded rectangle from 0.0f to 1.0f representing fully
591 * transparent to fully opaque respectively
592 * @param style Specifies whether the rounded rectangle is stroked or filled in
593 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
594 * @param blendMode Blending algorithm to be applied to the brush
595 */
drawRoundRectnull596 fun drawRoundRect(
597 brush: Brush,
598 topLeft: Offset = Offset.Zero,
599 size: Size = this.size.offsetSize(topLeft),
600 cornerRadius: CornerRadius = CornerRadius.Zero,
601 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
602 style: DrawStyle = Fill,
603 colorFilter: ColorFilter? = null,
604 blendMode: BlendMode = DefaultBlendMode
605 )
606
607 /**
608 * Draws a rounded rectangle with the given [Paint]. Whether the rectangle is filled or stroked
609 * (or both) is controlled by [Paint.style].
610 *
611 * @param color The color to be applied to the rounded rectangle
612 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
613 * @param size Dimensions of the rectangle to draw
614 * @param cornerRadius Corner radius of the rounded rectangle, negative radii values are clamped
615 * to 0
616 * @param alpha Opacity to be applied to rounded rectangle from 0.0f to 1.0f representing fully
617 * transparent to fully opaque respectively
618 * @param style Specifies whether the rounded rectangle is stroked or filled in
619 * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
620 * @param blendMode Blending algorithm to be applied to the color
621 */
622 fun drawRoundRect(
623 color: Color,
624 topLeft: Offset = Offset.Zero,
625 size: Size = this.size.offsetSize(topLeft),
626 cornerRadius: CornerRadius = CornerRadius.Zero,
627 style: DrawStyle = Fill,
628 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
629 colorFilter: ColorFilter? = null,
630 blendMode: BlendMode = DefaultBlendMode
631 )
632
633 /**
634 * Draws a circle at the provided center coordinate and radius. If no center point is provided
635 * the center of the bounds is used.
636 *
637 * @param brush The color or fill to be applied to the circle
638 * @param radius The radius of the circle
639 * @param center The center coordinate where the circle is to be drawn
640 * @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing fully
641 * transparent to fully opaque respectively
642 * @param style Whether or not the circle is stroked or filled in
643 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
644 * @param blendMode Blending algorithm to be applied to the brush
645 */
646 fun drawCircle(
647 brush: Brush,
648 radius: Float = size.minDimension / 2.0f,
649 center: Offset = this.center,
650 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
651 style: DrawStyle = Fill,
652 colorFilter: ColorFilter? = null,
653 blendMode: BlendMode = DefaultBlendMode
654 )
655
656 /**
657 * Draws a circle at the provided center coordinate and radius. If no center point is provided
658 * the center of the bounds is used.
659 *
660 * @param color The color or fill to be applied to the circle
661 * @param radius The radius of the circle
662 * @param center The center coordinate where the circle is to be drawn
663 * @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing fully
664 * transparent to fully opaque respectively
665 * @param style Whether or not the circle is stroked or filled in
666 * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
667 * @param blendMode Blending algorithm to be applied to the brush
668 */
669 fun drawCircle(
670 color: Color,
671 radius: Float = size.minDimension / 2.0f,
672 center: Offset = this.center,
673 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
674 style: DrawStyle = Fill,
675 colorFilter: ColorFilter? = null,
676 blendMode: BlendMode = DefaultBlendMode
677 )
678
679 /**
680 * Draws an oval with the given offset and size. If no offset from the top left is provided, it
681 * is drawn starting from the origin of the current translation. If no size is provided, the
682 * size of the current environment is used.
683 *
684 * @param brush Color or fill to be applied to the oval
685 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
686 * @param size Dimensions of the rectangle to draw
687 * @param alpha Opacity to be applied to the oval from 0.0f to 1.0f representing fully
688 * transparent to fully opaque respectively
689 * @param style Whether or not the oval is stroked or filled in
690 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
691 * @param blendMode Blending algorithm to be applied to the brush
692 * @sample androidx.compose.ui.graphics.samples.DrawScopeOvalBrushSample
693 */
694 fun drawOval(
695 brush: Brush,
696 topLeft: Offset = Offset.Zero,
697 size: Size = this.size.offsetSize(topLeft),
698 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
699 style: DrawStyle = Fill,
700 colorFilter: ColorFilter? = null,
701 blendMode: BlendMode = DefaultBlendMode
702 )
703
704 /**
705 * Draws an oval with the given offset and size. If no offset from the top left is provided, it
706 * is drawn starting from the origin of the current translation. If no size is provided, the
707 * size of the current environment is used.
708 *
709 * @param color Color to be applied to the oval
710 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
711 * @param size Dimensions of the rectangle to draw
712 * @param alpha Opacity to be applied to the oval from 0.0f to 1.0f representing fully
713 * transparent to fully opaque respectively
714 * @param style Whether or not the oval is stroked or filled in
715 * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
716 * @param blendMode Blending algorithm to be applied to the brush
717 * @sample androidx.compose.ui.graphics.samples.DrawScopeOvalColorSample
718 */
719 fun drawOval(
720 color: Color,
721 topLeft: Offset = Offset.Zero,
722 size: Size = this.size.offsetSize(topLeft),
723 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
724 style: DrawStyle = Fill,
725 colorFilter: ColorFilter? = null,
726 blendMode: BlendMode = DefaultBlendMode
727 )
728
729 /**
730 * Draw an arc scaled to fit inside the given rectangle. It starts from startAngle degrees
731 * around the oval up to startAngle + sweepAngle degrees around the oval, with zero degrees
732 * being the point on the right hand side of the oval that crosses the horizontal line that
733 * intersects the center of the rectangle and with positive angles going clockwise around the
734 * oval. If useCenter is true, the arc is closed back to the center, forming a circle sector.
735 * Otherwise, the arc is not closed, forming a circle segment.
736 *
737 * @param brush Color or fill to be applied to the arc
738 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
739 * @param size Dimensions of the arc to draw
740 * @param startAngle Starting angle in degrees. 0 represents 3 o'clock
741 * @param sweepAngle Size of the arc in degrees that is drawn clockwise relative to [startAngle]
742 * @param useCenter Flag indicating if the arc is to close the center of the bounds
743 * @param alpha Opacity to be applied to the arc from 0.0f to 1.0f representing fully
744 * transparent to fully opaque respectively
745 * @param style Whether or not the arc is stroked or filled in
746 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
747 * @param blendMode Blending algorithm to be applied to the arc when it is drawn
748 */
749 fun drawArc(
750 brush: Brush,
751 startAngle: Float,
752 sweepAngle: Float,
753 useCenter: Boolean,
754 topLeft: Offset = Offset.Zero,
755 size: Size = this.size.offsetSize(topLeft),
756 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
757 style: DrawStyle = Fill,
758 colorFilter: ColorFilter? = null,
759 blendMode: BlendMode = DefaultBlendMode
760 )
761
762 /**
763 * Draw an arc scaled to fit inside the given rectangle. It starts from startAngle degrees
764 * around the oval up to startAngle + sweepAngle degrees around the oval, with zero degrees
765 * being the point on the right hand side of the oval that crosses the horizontal line that
766 * intersects the center of the rectangle and with positive angles going clockwise around the
767 * oval. If useCenter is true, the arc is closed back to the center, forming a circle sector.
768 * Otherwise, the arc is not closed, forming a circle segment.
769 *
770 * @param color Color to be applied to the arc
771 * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
772 * @param size Dimensions of the arc to draw
773 * @param startAngle Starting angle in degrees. 0 represents 3 o'clock
774 * @param sweepAngle Size of the arc in degrees that is drawn clockwise relative to [startAngle]
775 * @param useCenter Flag indicating if the arc is to close the center of the bounds
776 * @param alpha Opacity to be applied to the arc from 0.0f to 1.0f representing fully
777 * transparent to fully opaque respectively
778 * @param style Whether or not the arc is stroked or filled in
779 * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
780 * @param blendMode Blending algorithm to be applied to the arc when it is drawn
781 */
782 fun drawArc(
783 color: Color,
784 startAngle: Float,
785 sweepAngle: Float,
786 useCenter: Boolean,
787 topLeft: Offset = Offset.Zero,
788 size: Size = this.size.offsetSize(topLeft),
789 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
790 style: DrawStyle = Fill,
791 colorFilter: ColorFilter? = null,
792 blendMode: BlendMode = DefaultBlendMode
793 )
794
795 /**
796 * Draws the given [Path] with the given [Color]. Whether this shape is filled or stroked (or
797 * both) is controlled by [DrawStyle]. If the path is filled, then subpaths within it are
798 * implicitly closed (see [Path.close]).
799 *
800 * @param path Path to draw
801 * @param color Color to be applied to the path
802 * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
803 * transparent to fully opaque respectively
804 * @param style Whether or not the path is stroked or filled in
805 * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
806 * @param blendMode Blending algorithm to be applied to the path when it is drawn
807 */
808 fun drawPath(
809 path: Path,
810 color: Color,
811 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
812 style: DrawStyle = Fill,
813 colorFilter: ColorFilter? = null,
814 blendMode: BlendMode = DefaultBlendMode
815 )
816
817 /**
818 * Draws the given [Path] with the given [Color]. Whether this shape is filled or stroked (or
819 * both) is controlled by [DrawStyle]. If the path is filled, then subpaths within it are
820 * implicitly closed (see [Path.close]).
821 *
822 * @param path Path to draw
823 * @param brush Brush to be applied to the path
824 * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
825 * transparent to fully opaque respectively
826 * @param style Whether or not the path is stroked or filled in
827 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
828 * @param blendMode Blending algorithm to be applied to the path when it is drawn
829 */
830 fun drawPath(
831 path: Path,
832 brush: Brush,
833 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
834 style: DrawStyle = Fill,
835 colorFilter: ColorFilter? = null,
836 blendMode: BlendMode = DefaultBlendMode
837 )
838
839 /**
840 * Draws a sequence of points according to the given [PointMode].
841 *
842 * The `points` argument is interpreted as offsets from the origin.
843 *
844 * @param points List of points to draw with the specified [PointMode]
845 * @param pointMode [PointMode] used to indicate how the points are to be drawn
846 * @param color Color to be applied to the points
847 * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
848 * transparent to fully opaque respectively
849 * @param strokeWidth The stroke width to apply to the line
850 * @param cap Treatment applied to the ends of the line segment
851 * @param pathEffect optional effect or pattern to apply to the point
852 * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
853 * @param blendMode Blending algorithm to be applied to the path when it is drawn
854 */
855 fun drawPoints(
856 points: List<Offset>,
857 pointMode: PointMode,
858 color: Color,
859 strokeWidth: Float = Stroke.HairlineWidth,
860 cap: StrokeCap = StrokeCap.Butt,
861 pathEffect: PathEffect? = null,
862 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
863 colorFilter: ColorFilter? = null,
864 blendMode: BlendMode = DefaultBlendMode
865 )
866
867 /**
868 * Draws a sequence of points according to the given [PointMode].
869 *
870 * The `points` argument is interpreted as offsets from the origin.
871 *
872 * @param points List of points to draw with the specified [PointMode]
873 * @param pointMode [PointMode] used to indicate how the points are to be drawn
874 * @param brush Brush to be applied to the points
875 * @param strokeWidth The stroke width to apply to the line
876 * @param cap Treatment applied to the ends of the line segment
877 * @param pathEffect optional effect or pattern to apply to the points
878 * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
879 * transparent to fully opaque respectively.
880 * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
881 * @param blendMode Blending algorithm to be applied to the path when it is drawn
882 */
883 fun drawPoints(
884 points: List<Offset>,
885 pointMode: PointMode,
886 brush: Brush,
887 strokeWidth: Float = Stroke.HairlineWidth,
888 cap: StrokeCap = StrokeCap.Butt,
889 pathEffect: PathEffect? = null,
890 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
891 colorFilter: ColorFilter? = null,
892 blendMode: BlendMode = DefaultBlendMode
893 )
894
895 /**
896 * Record the corresponding drawing commands for this [GraphicsLayer] instance using the
897 * [Density], [LayoutDirection] and [IntSize] from the provided [DrawScope] as defaults. This
898 * will retarget the underlying canvas of the provided DrawScope to draw within the layer itself
899 * and reset it to the original canvas on the conclusion of this method call.
900 */
901 fun GraphicsLayer.record(
902 size: IntSize = this@DrawScope.size.toIntSize(),
903 block: DrawScope.() -> Unit
904 ) =
905 record(this@DrawScope, this@DrawScope.layoutDirection, size) {
906 this@DrawScope.draw(
907 // we can use this@record.drawContext directly as the values in this@DrawScope
908 // and this@record are the same
909 drawContext.density,
910 drawContext.layoutDirection,
911 drawContext.canvas,
912 drawContext.size,
913 drawContext.graphicsLayer,
914 block
915 )
916 }
917
918 /** Helper method to offset the provided size with the offset in box width and height */
offsetSizenull919 private fun Size.offsetSize(offset: Offset): Size =
920 Size(this.width - offset.x, this.height - offset.y)
921
922 companion object {
923
924 /**
925 * Default blending mode used for each drawing operation. This ensures that content is drawn
926 * on top of the pixels in the destination
927 */
928 val DefaultBlendMode: BlendMode = BlendMode.SrcOver
929
930 /**
931 * Default FilterQuality used for determining the filtering algorithm to apply when scaling
932 * [ImageBitmap] objects. Maps to the default behavior of bilinear filtering
933 */
934 val DefaultFilterQuality: FilterQuality = FilterQuality.Low
935 }
936 }
937
938 /** Represents how the shapes should be drawn within a [DrawScope] */
939 sealed class DrawStyle
940
941 /**
942 * Default [DrawStyle] indicating shapes should be drawn completely filled in with the provided
943 * color or pattern
944 */
945 object Fill : DrawStyle()
946
947 /**
948 * [DrawStyle] that provides information for drawing content with a stroke
949 *
950 * @param width Configure the width of the stroke in pixels
951 * @param miter Set the stroke miter value. This is used to control the behavior of miter joins when
952 * the joins angle is sharp. This value must be >= 0
953 * @param cap Return the paint's Cap, controlling how the start and end of stroked lines and paths
954 * are treated. The default is [StrokeCap.Butt]
955 * @param join Set's the treatment where lines and curve segments join on a stroked path. The
956 * default is [StrokeJoin.Miter]
957 * @param pathEffect Effect to apply to the stroke, null indicates a solid stroke line is to be
958 * drawn
959 */
960 class Stroke(
961 val width: Float = 0.0f,
962 val miter: Float = DefaultMiter,
963 val cap: StrokeCap = DefaultCap,
964 val join: StrokeJoin = DefaultJoin,
965 val pathEffect: PathEffect? = null
966 ) : DrawStyle() {
967 companion object {
968
969 /** Width to indicate a hairline stroke of 1 pixel */
970 const val HairlineWidth = 0.0f
971
972 /** Default miter length used in combination with joins */
973 const val DefaultMiter: Float = 4.0f
974
975 /** Default cap used for line endings */
976 val DefaultCap = StrokeCap.Butt
977
978 /** Default join style used for connections between line and curve segments */
979 val DefaultJoin = StrokeJoin.Miter
980 }
981
equalsnull982 override fun equals(other: Any?): Boolean {
983 if (this === other) return true
984 if (other !is Stroke) return false
985
986 if (width != other.width) return false
987 if (miter != other.miter) return false
988 if (cap != other.cap) return false
989 if (join != other.join) return false
990 if (pathEffect != other.pathEffect) return false
991
992 return true
993 }
994
hashCodenull995 override fun hashCode(): Int {
996 var result = width.hashCode()
997 result = 31 * result + miter.hashCode()
998 result = 31 * result + cap.hashCode()
999 result = 31 * result + join.hashCode()
1000 result = 31 * result + (pathEffect?.hashCode() ?: 0)
1001 return result
1002 }
1003
toStringnull1004 override fun toString(): String {
1005 return "Stroke(width=$width, miter=$miter, cap=$cap, join=$join, pathEffect=$pathEffect)"
1006 }
1007 }
1008