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.graphics
18
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.Stable
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.geometry.isFinite
25 import androidx.compose.ui.geometry.isSpecified
26 import androidx.compose.ui.geometry.isUnspecified
27 import androidx.compose.ui.util.fastIsFinite
28 import kotlin.math.abs
29
30 @Immutable
31 sealed class Brush {
32
33 /**
34 * Return the intrinsic size of the [Brush]. If the there is no intrinsic size (i.e. filling
35 * bounds with an arbitrary color) return [Size.Unspecified]. If there is no intrinsic size in a
36 * single dimension, return [Size] with [Float.NaN] in the desired dimension.
37 */
38 open val intrinsicSize: Size = Size.Unspecified
39
40 abstract fun applyTo(size: Size, p: Paint, alpha: Float)
41
42 companion object {
43
44 /**
45 * Creates a linear gradient with the provided colors along the given start and end
46 * coordinates. The colors are dispersed at the provided offset defined in the colorstop
47 * pair.
48 *
49 * ```
50 * Brush.linearGradient(
51 * 0.0f to Color.Red,
52 * 0.3f to Color.Green,
53 * 1.0f to Color.Blue,
54 * start = Offset(0.0f, 50.0f),
55 * end = Offset(0.0f, 100.0f)
56 * )
57 * ```
58 *
59 * @sample androidx.compose.ui.graphics.samples.LinearGradientColorStopSample
60 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
61 * @param colorStops Colors and their offset in the gradient area
62 * @param start Starting position of the linear gradient. This can be set to [Offset.Zero]
63 * to position at the far left and top of the drawing area
64 * @param end Ending position of the linear gradient. This can be set to [Offset.Infinite]
65 * to position at the far right and bottom of the drawing area
66 * @param tileMode Determines the behavior for how the shader is to fill a region outside
67 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
68 */
69 @Stable
70 fun linearGradient(
71 vararg colorStops: Pair<Float, Color>,
72 start: Offset = Offset.Zero,
73 end: Offset = Offset.Infinite,
74 tileMode: TileMode = TileMode.Clamp
75 ): Brush =
76 LinearGradient(
77 colors = List<Color>(colorStops.size) { i -> colorStops[i].second },
78 stops = List<Float>(colorStops.size) { i -> colorStops[i].first },
79 start = start,
80 end = end,
81 tileMode = tileMode
82 )
83
84 /**
85 * Creates a linear gradient with the provided colors along the given start and end
86 * coordinates. The colors are
87 *
88 * ```
89 * Brush.linearGradient(
90 * listOf(Color.Red, Color.Green, Color.Blue),
91 * start = Offset(0.0f, 50.0f),
92 * end = Offset(0.0f, 100.0f)
93 * )
94 * ```
95 *
96 * @sample androidx.compose.ui.graphics.samples.LinearGradientSample
97 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
98 * @param colors Colors to be rendered as part of the gradient
99 * @param start Starting position of the linear gradient. This can be set to [Offset.Zero]
100 * to position at the far left and top of the drawing area
101 * @param end Ending position of the linear gradient. This can be set to [Offset.Infinite]
102 * to position at the far right and bottom of the drawing area
103 * @param tileMode Determines the behavior for how the shader is to fill a region outside
104 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
105 */
106 @Stable
107 fun linearGradient(
108 colors: List<Color>,
109 start: Offset = Offset.Zero,
110 end: Offset = Offset.Infinite,
111 tileMode: TileMode = TileMode.Clamp
112 ): Brush =
113 LinearGradient(
114 colors = colors,
115 stops = null,
116 start = start,
117 end = end,
118 tileMode = tileMode
119 )
120
121 /**
122 * Creates a horizontal gradient with the given colors evenly dispersed within the gradient
123 *
124 * Ex:
125 * ```
126 * Brush.horizontalGradient(
127 * listOf(Color.Red, Color.Green, Color.Blue),
128 * startX = 10.0f,
129 * endX = 20.0f
130 * )
131 * ```
132 *
133 * @sample androidx.compose.ui.graphics.samples.HorizontalGradientSample
134 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
135 * @param colors colors Colors to be rendered as part of the gradient
136 * @param startX Starting x position of the horizontal gradient. Defaults to 0 which
137 * represents the left of the drawing area
138 * @param endX Ending x position of the horizontal gradient. Defaults to
139 * [Float.POSITIVE_INFINITY] which indicates the right of the specified drawing area
140 * @param tileMode Determines the behavior for how the shader is to fill a region outside
141 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
142 */
143 @Stable
144 fun horizontalGradient(
145 colors: List<Color>,
146 startX: Float = 0.0f,
147 endX: Float = Float.POSITIVE_INFINITY,
148 tileMode: TileMode = TileMode.Clamp
149 ): Brush = linearGradient(colors, Offset(startX, 0.0f), Offset(endX, 0.0f), tileMode)
150
151 /**
152 * Creates a horizontal gradient with the given colors dispersed at the provided offset
153 * defined in the colorstop pair.
154 *
155 * Ex:
156 * ```
157 * Brush.horizontalGradient(
158 * 0.0f to Color.Red,
159 * 0.3f to Color.Green,
160 * 1.0f to Color.Blue,
161 * startX = 0.0f,
162 * endX = 100.0f
163 * )
164 * ```
165 *
166 * @sample androidx.compose.ui.graphics.samples.HorizontalGradientColorStopSample
167 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
168 * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
169 * the vertical gradient
170 * @param startX Starting x position of the horizontal gradient. Defaults to 0 which
171 * represents the left of the drawing area
172 * @param endX Ending x position of the horizontal gradient. Defaults to
173 * [Float.POSITIVE_INFINITY] which indicates the right of the specified drawing area
174 * @param tileMode Determines the behavior for how the shader is to fill a region outside
175 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
176 */
177 @Stable
178 fun horizontalGradient(
179 vararg colorStops: Pair<Float, Color>,
180 startX: Float = 0.0f,
181 endX: Float = Float.POSITIVE_INFINITY,
182 tileMode: TileMode = TileMode.Clamp
183 ): Brush =
184 linearGradient(
185 *colorStops,
186 start = Offset(startX, 0.0f),
187 end = Offset(endX, 0.0f),
188 tileMode = tileMode
189 )
190
191 /**
192 * Creates a vertical gradient with the given colors evenly dispersed within the gradient
193 * Ex:
194 * ```
195 * Brush.verticalGradient(
196 * listOf(Color.Red, Color.Green, Color.Blue),
197 * startY = 0.0f,
198 * endY = 100.0f
199 * )
200 * ```
201 *
202 * @sample androidx.compose.ui.graphics.samples.VerticalGradientSample
203 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
204 * @param colors colors Colors to be rendered as part of the gradient
205 * @param startY Starting y position of the vertical gradient. Defaults to 0 which
206 * represents the top of the drawing area
207 * @param endY Ending y position of the vertical gradient. Defaults to
208 * [Float.POSITIVE_INFINITY] which indicates the bottom of the specified drawing area
209 * @param tileMode Determines the behavior for how the shader is to fill a region outside
210 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
211 */
212 @Stable
213 fun verticalGradient(
214 colors: List<Color>,
215 startY: Float = 0.0f,
216 endY: Float = Float.POSITIVE_INFINITY,
217 tileMode: TileMode = TileMode.Clamp
218 ): Brush = linearGradient(colors, Offset(0.0f, startY), Offset(0.0f, endY), tileMode)
219
220 /**
221 * Creates a vertical gradient with the given colors at the provided offset defined in the
222 * [Pair<Float, Color>]
223 *
224 * Ex:
225 * ```
226 * Brush.verticalGradient(
227 * 0.1f to Color.Red,
228 * 0.3f to Color.Green,
229 * 0.5f to Color.Blue,
230 * startY = 0.0f,
231 * endY = 100.0f
232 * )
233 * ```
234 *
235 * @sample androidx.compose.ui.graphics.samples.VerticalGradientColorStopSample
236 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
237 * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
238 * the vertical gradient
239 * @param startY Starting y position of the vertical gradient. Defaults to 0 which
240 * represents the top of the drawing area
241 * @param endY Ending y position of the vertical gradient. Defaults to
242 * [Float.POSITIVE_INFINITY] which indicates the bottom of the specified drawing area
243 * @param tileMode Determines the behavior for how the shader is to fill a region outside
244 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
245 */
246 @Stable
247 fun verticalGradient(
248 vararg colorStops: Pair<Float, Color>,
249 startY: Float = 0f,
250 endY: Float = Float.POSITIVE_INFINITY,
251 tileMode: TileMode = TileMode.Clamp
252 ): Brush =
253 linearGradient(
254 *colorStops,
255 start = Offset(0.0f, startY),
256 end = Offset(0.0f, endY),
257 tileMode = tileMode
258 )
259
260 /**
261 * Creates a radial gradient with the given colors at the provided offset defined in the
262 * colorstop pair.
263 *
264 * ```
265 * Brush.radialGradient(
266 * 0.0f to Color.Red,
267 * 0.3f to Color.Green,
268 * 1.0f to Color.Blue,
269 * center = Offset(side1 / 2.0f, side2 / 2.0f),
270 * radius = side1 / 2.0f,
271 * tileMode = TileMode.Repeated
272 * )
273 * ```
274 *
275 * @sample androidx.compose.ui.graphics.samples.RadialBrushColorStopSample
276 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
277 * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
278 * the radial gradient
279 * @param center Center position of the radial gradient circle. If this is set to
280 * [Offset.Unspecified] then the center of the drawing area is used as the center for the
281 * radial gradient. [Float.POSITIVE_INFINITY] can be used for either [Offset.x] or
282 * [Offset.y] to indicate the far right or far bottom of the drawing area respectively.
283 * @param radius Radius for the radial gradient. Defaults to positive infinity to indicate
284 * the largest radius that can fit within the bounds of the drawing area
285 * @param tileMode Determines the behavior for how the shader is to fill a region outside
286 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
287 */
288 @Stable
289 fun radialGradient(
290 vararg colorStops: Pair<Float, Color>,
291 center: Offset = Offset.Unspecified,
292 radius: Float = Float.POSITIVE_INFINITY,
293 tileMode: TileMode = TileMode.Clamp
294 ): Brush =
295 RadialGradient(
296 colors = List<Color>(colorStops.size) { i -> colorStops[i].second },
297 stops = List<Float>(colorStops.size) { i -> colorStops[i].first },
298 center = center,
299 radius = radius,
300 tileMode = tileMode
301 )
302
303 /**
304 * Creates a radial gradient with the given colors evenly dispersed within the gradient
305 *
306 * ```
307 * Brush.radialGradient(
308 * listOf(Color.Red, Color.Green, Color.Blue),
309 * center = Offset(side1 / 2.0f, side2 / 2.0f),
310 * radius = side1 / 2.0f,
311 * tileMode = TileMode.Repeated
312 * )
313 * ```
314 *
315 * @sample androidx.compose.ui.graphics.samples.RadialBrushSample
316 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
317 * @param colors Colors to be rendered as part of the gradient
318 * @param center Center position of the radial gradient circle. If this is set to
319 * [Offset.Unspecified] then the center of the drawing area is used as the center for the
320 * radial gradient. [Float.POSITIVE_INFINITY] can be used for either [Offset.x] or
321 * [Offset.y] to indicate the far right or far bottom of the drawing area respectively.
322 * @param radius Radius for the radial gradient. Defaults to positive infinity to indicate
323 * the largest radius that can fit within the bounds of the drawing area
324 * @param tileMode Determines the behavior for how the shader is to fill a region outside
325 * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
326 */
327 @Stable
328 fun radialGradient(
329 colors: List<Color>,
330 center: Offset = Offset.Unspecified,
331 radius: Float = Float.POSITIVE_INFINITY,
332 tileMode: TileMode = TileMode.Clamp
333 ): Brush =
334 RadialGradient(
335 colors = colors,
336 stops = null,
337 center = center,
338 radius = radius,
339 tileMode = tileMode
340 )
341
342 /**
343 * Creates a sweep gradient with the given colors dispersed around the center with offsets
344 * defined in each colorstop pair. The sweep begins relative to 3 o'clock and continues
345 * clockwise until it reaches the starting position again.
346 *
347 * Ex:
348 * ```
349 * Brush.sweepGradient(
350 * 0.0f to Color.Red,
351 * 0.3f to Color.Green,
352 * 1.0f to Color.Blue,
353 * center = Offset(0.0f, 100.0f)
354 * )
355 * ```
356 *
357 * @sample androidx.compose.ui.graphics.samples.SweepGradientColorStopSample
358 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
359 * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
360 * the sweep gradient
361 * @param center Center position of the sweep gradient circle. If this is set to
362 * [Offset.Unspecified] then the center of the drawing area is used as the center for the
363 * sweep gradient
364 */
365 @Stable
366 fun sweepGradient(
367 vararg colorStops: Pair<Float, Color>,
368 center: Offset = Offset.Unspecified
369 ): Brush =
370 SweepGradient(
371 colors = List<Color>(colorStops.size) { i -> colorStops[i].second },
372 stops = List<Float>(colorStops.size) { i -> colorStops[i].first },
373 center = center
374 )
375
376 /**
377 * Creates a sweep gradient with the given colors dispersed evenly around the center. The
378 * sweep begins relative to 3 o'clock and continues clockwise until it reaches the starting
379 * position again.
380 *
381 * Ex:
382 * ```
383 * Brush.sweepGradient(
384 * listOf(Color.Red, Color.Green, Color.Blue),
385 * center = Offset(10.0f, 20.0f)
386 * )
387 * ```
388 *
389 * @sample androidx.compose.ui.graphics.samples.SweepGradientSample
390 * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
391 * @param colors List of colors to fill the sweep gradient
392 * @param center Center position of the sweep gradient circle. If this is set to
393 * [Offset.Unspecified] then the center of the drawing area is used as the center for the
394 * sweep gradient
395 */
396 @Stable
397 fun sweepGradient(colors: List<Color>, center: Offset = Offset.Unspecified): Brush =
398 SweepGradient(colors = colors, stops = null, center = center)
399 }
400 }
401
402 @Immutable
403 class SolidColor(val value: Color) : Brush() {
applyTonull404 override fun applyTo(size: Size, p: Paint, alpha: Float) {
405 p.alpha = DefaultAlpha
406 p.color =
407 if (alpha != DefaultAlpha) {
408 value.copy(alpha = value.alpha * alpha)
409 } else {
410 value
411 }
412 if (p.shader != null) p.shader = null
413 }
414
equalsnull415 override fun equals(other: Any?): Boolean {
416 if (this === other) return true
417 if (other !is SolidColor) return false
418 if (value != other.value) return false
419
420 return true
421 }
422
hashCodenull423 override fun hashCode(): Int {
424 return value.hashCode()
425 }
426
toStringnull427 override fun toString(): String {
428 return "SolidColor(value=$value)"
429 }
430 }
431
432 /** Brush implementation used to apply a linear gradient on a given [Paint] */
433 @Immutable
434 class LinearGradient
435 internal constructor(
436 private val colors: List<Color>,
437 private val stops: List<Float>? = null,
438 private val start: Offset,
439 private val end: Offset,
440 private val tileMode: TileMode = TileMode.Clamp
441 ) : ShaderBrush() {
442
443 override val intrinsicSize: Size
444 get() =
445 Size(
446 if (start.x.isFinite() && end.x.isFinite()) abs(start.x - end.x) else Float.NaN,
447 if (start.y.isFinite() && end.y.isFinite()) abs(start.y - end.y) else Float.NaN
448 )
449
createShadernull450 override fun createShader(size: Size): Shader {
451 val startX = if (start.x == Float.POSITIVE_INFINITY) size.width else start.x
452 val startY = if (start.y == Float.POSITIVE_INFINITY) size.height else start.y
453 val endX = if (end.x == Float.POSITIVE_INFINITY) size.width else end.x
454 val endY = if (end.y == Float.POSITIVE_INFINITY) size.height else end.y
455 return LinearGradientShader(
456 colors = colors,
457 colorStops = stops,
458 from = Offset(startX, startY),
459 to = Offset(endX, endY),
460 tileMode = tileMode
461 )
462 }
463
equalsnull464 override fun equals(other: Any?): Boolean {
465 if (this === other) return true
466 if (other !is LinearGradient) return false
467
468 if (colors != other.colors) return false
469 if (stops != other.stops) return false
470 if (start != other.start) return false
471 if (end != other.end) return false
472 if (tileMode != other.tileMode) return false
473
474 return true
475 }
476
hashCodenull477 override fun hashCode(): Int {
478 var result = colors.hashCode()
479 result = 31 * result + (stops?.hashCode() ?: 0)
480 result = 31 * result + start.hashCode()
481 result = 31 * result + end.hashCode()
482 result = 31 * result + tileMode.hashCode()
483 return result
484 }
485
toStringnull486 override fun toString(): String {
487 val startValue = if (start.isFinite) "start=$start, " else ""
488 val endValue = if (end.isFinite) "end=$end, " else ""
489 return "LinearGradient(colors=$colors, " +
490 "stops=$stops, " +
491 startValue +
492 endValue +
493 "tileMode=$tileMode)"
494 }
495 }
496
497 /** Brush implementation used to apply a radial gradient on a given [Paint] */
498 @Immutable
499 class RadialGradient
500 internal constructor(
501 private val colors: List<Color>,
502 private val stops: List<Float>? = null,
503 private val center: Offset,
504 private val radius: Float,
505 private val tileMode: TileMode = TileMode.Clamp
506 ) : ShaderBrush() {
507
508 override val intrinsicSize: Size
509 get() =
510 if (radius.fastIsFinite()) {
511 Size(radius * 2, radius * 2)
512 } else {
513 Size.Unspecified
514 }
515
createShadernull516 override fun createShader(size: Size): Shader {
517 val centerX: Float
518 val centerY: Float
519 if (center.isUnspecified) {
520 val drawCenter = size.center
521 centerX = drawCenter.x
522 centerY = drawCenter.y
523 } else {
524 centerX = if (center.x == Float.POSITIVE_INFINITY) size.width else center.x
525 centerY = if (center.y == Float.POSITIVE_INFINITY) size.height else center.y
526 }
527
528 return RadialGradientShader(
529 colors = colors,
530 colorStops = stops,
531 center = Offset(centerX, centerY),
532 radius = if (radius == Float.POSITIVE_INFINITY) size.minDimension / 2 else radius,
533 tileMode = tileMode
534 )
535 }
536
equalsnull537 override fun equals(other: Any?): Boolean {
538 if (this === other) return true
539 if (other !is RadialGradient) return false
540
541 if (colors != other.colors) return false
542 if (stops != other.stops) return false
543 if (center != other.center) return false
544 if (radius != other.radius) return false
545 if (tileMode != other.tileMode) return false
546
547 return true
548 }
549
hashCodenull550 override fun hashCode(): Int {
551 var result = colors.hashCode()
552 result = 31 * result + (stops?.hashCode() ?: 0)
553 result = 31 * result + center.hashCode()
554 result = 31 * result + radius.hashCode()
555 result = 31 * result + tileMode.hashCode()
556 return result
557 }
558
toStringnull559 override fun toString(): String {
560 val centerValue = if (center.isSpecified) "center=$center, " else ""
561 val radiusValue = if (radius.fastIsFinite()) "radius=$radius, " else ""
562 return "RadialGradient(" +
563 "colors=$colors, " +
564 "stops=$stops, " +
565 centerValue +
566 radiusValue +
567 "tileMode=$tileMode)"
568 }
569 }
570
571 /** Brush implementation used to apply a sweep gradient on a given [Paint] */
572 @Immutable
573 class SweepGradient
574 internal constructor(
575 private val center: Offset,
576 private val colors: List<Color>,
577 private val stops: List<Float>? = null
578 ) : ShaderBrush() {
579
createShadernull580 override fun createShader(size: Size): Shader =
581 SweepGradientShader(
582 if (center.isUnspecified) {
583 size.center
584 } else {
585 Offset(
586 if (center.x == Float.POSITIVE_INFINITY) size.width else center.x,
587 if (center.y == Float.POSITIVE_INFINITY) size.height else center.y
588 )
589 },
590 colors,
591 stops
592 )
593
equalsnull594 override fun equals(other: Any?): Boolean {
595 if (this === other) return true
596 if (other !is SweepGradient) return false
597
598 if (center != other.center) return false
599 if (colors != other.colors) return false
600 if (stops != other.stops) return false
601
602 return true
603 }
604
hashCodenull605 override fun hashCode(): Int {
606 var result = center.hashCode()
607 result = 31 * result + colors.hashCode()
608 result = 31 * result + (stops?.hashCode() ?: 0)
609 return result
610 }
611
toStringnull612 override fun toString(): String {
613 val centerValue = if (center.isSpecified) "center=$center, " else ""
614 return "SweepGradient(" + centerValue + "colors=$colors, stops=$stops)"
615 }
616 }
617
618 /**
619 * Convenience method to create a ShaderBrush that always returns the same shader instance
620 * regardless of size
621 */
ShaderBrushnull622 fun ShaderBrush(shader: Shader) =
623 object : ShaderBrush() {
624
625 /** Create a shader based on the given size that represents the current drawing area */
626 override fun createShader(size: Size): Shader = shader
627 }
628
629 /**
630 * Brush implementation that wraps and applies a the provided shader to a [Paint] The shader can be
631 * lazily created based on a given size, or provided directly as a parameter
632 */
633 @Immutable
634 abstract class ShaderBrush() : Brush() {
635
636 private var internalShader: Shader? = null
637 private var createdSize = Size.Unspecified
638
createShadernull639 abstract fun createShader(size: Size): Shader
640
641 final override fun applyTo(size: Size, p: Paint, alpha: Float) {
642 var shader = internalShader
643 if (shader == null || createdSize != size) {
644 if (size.isEmpty()) {
645 shader = null
646 internalShader = null
647 createdSize = Size.Unspecified
648 } else {
649 shader = createShader(size).also { internalShader = it }
650 createdSize = size
651 }
652 }
653 if (p.color != Color.Black) p.color = Color.Black
654 if (p.shader != shader) p.shader = shader
655 if (p.alpha != alpha) p.alpha = alpha
656 }
657 }
658