1 /*
<lambda>null2 * 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.FloatRange
20 import androidx.compose.runtime.Immutable
21 import androidx.compose.ui.geometry.CornerRadius
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.geometry.Rect
24 import androidx.compose.ui.geometry.RoundRect
25 import androidx.compose.ui.geometry.Size
26 import androidx.compose.ui.geometry.boundingRect
27 import androidx.compose.ui.geometry.isSimple
28 import androidx.compose.ui.graphics.drawscope.DrawScope
29 import androidx.compose.ui.graphics.drawscope.DrawStyle
30 import androidx.compose.ui.graphics.drawscope.Fill
31
32 /**
33 * Defines a simple shape, used for bounding graphical regions.
34 *
35 * Can be used for defining a shape of the component background, a shape of shadows cast by the
36 * component, or to clip the contents.
37 */
38 sealed class Outline {
39 /** Rectangular area. */
40 @Immutable
41 class Rectangle(val rect: Rect) : Outline() {
42
43 override val bounds: Rect
44 get() = rect
45
46 override fun equals(other: Any?): Boolean {
47 if (this === other) return true
48 if (other !is Rectangle) return false
49
50 if (rect != other.rect) return false
51
52 return true
53 }
54
55 override fun hashCode(): Int {
56 return rect.hashCode()
57 }
58 }
59
60 /** Rectangular area with rounded corners. */
61 @Immutable
62 class Rounded(val roundRect: RoundRect) : Outline() {
63
64 /**
65 * Optional Path to be created for the RoundRect if the corner radii are not identical This
66 * is because Canvas has a built in API for drawing round rectangles with the same corner
67 * radii in all 4 corners. However, if each corner has a different corner radii, a path must
68 * be drawn instead
69 */
70 internal val roundRectPath: Path?
71
72 init {
73 roundRectPath =
74 if (!roundRect.isSimple) {
75 Path().apply { addRoundRect(roundRect) }
76 } else {
77 null
78 }
79 }
80
81 override val bounds: Rect
82 get() = roundRect.boundingRect
83
84 override fun equals(other: Any?): Boolean {
85 if (this === other) return true
86 if (other !is Rounded) return false
87
88 if (roundRect != other.roundRect) return false
89
90 return true
91 }
92
93 override fun hashCode(): Int {
94 return roundRect.hashCode()
95 }
96 }
97
98 /**
99 * An area defined as a path.
100 *
101 * Note that if you use this path for drawing the shadow on Android versions less than 10 the
102 * shadow will not be drawn for the concave paths. See [Path.isConvex].
103 */
104 class Generic(val path: Path) : Outline() {
105 override val bounds: Rect
106 get() = path.getBounds()
107
108 // No equals or hashcode, two different outlines using the same path shouldn't be considered
109 // equal as the path may have changed since the previous outline was rendered
110 }
111
112 /** Return the bounds of the outline */
113 abstract val bounds: Rect
114 }
115
116 /** Adds the [outline] to the [Path]. */
Pathnull117 fun Path.addOutline(outline: Outline) =
118 when (outline) {
119 is Outline.Rectangle -> addRect(outline.rect)
120 is Outline.Rounded -> addRoundRect(outline.roundRect)
121 is Outline.Generic -> addPath(outline.path)
122 }
123
124 /**
125 * Draws the [Outline] on a [DrawScope].
126 *
127 * @param outline the outline to draw.
128 * @param color Color applied to the outline when it is drawn
129 * @param alpha Opacity to be applied to outline from 0.0f to 1.0f representing fully transparent to
130 * fully opaque respectively
131 * @param style Specifies whether the outline is stroked or filled in
132 * @param colorFilter: ColorFilter to apply to the [color] when drawn into the destination
133 * @param blendMode: Blending algorithm to be applied to the outline
134 */
DrawScopenull135 fun DrawScope.drawOutline(
136 outline: Outline,
137 color: Color,
138 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
139 style: DrawStyle = Fill,
140 colorFilter: ColorFilter? = null,
141 blendMode: BlendMode = DrawScope.DefaultBlendMode
142 ) =
143 drawOutlineHelper(
144 outline,
145 { rect ->
146 drawRect(color, rect.topLeft(), rect.size(), alpha, style, colorFilter, blendMode)
147 },
rrectnull148 { rrect ->
149 val radius = rrect.bottomLeftCornerRadius.x
150 drawRoundRect(
151 color = color,
152 topLeft = rrect.topLeft(),
153 size = rrect.size(),
154 cornerRadius = CornerRadius(radius),
155 alpha = alpha,
156 style = style,
157 colorFilter = colorFilter,
158 blendMode = blendMode
159 )
160 },
pathnull161 { path -> drawPath(path, color, alpha, style, colorFilter, blendMode) }
162 )
163
164 /**
165 * Draws the [Outline] on a [DrawScope].
166 *
167 * @param outline the outline to draw.
168 * @param brush Brush applied to the outline when it is drawn
169 * @param alpha Opacity to be applied to outline from 0.0f to 1.0f representing fully transparent to
170 * fully opaque respectively
171 * @param style Specifies whether the outline is stroked or filled in
172 * @param colorFilter: ColorFilter to apply to the [Brush] when drawn into the destination
173 * @param blendMode: Blending algorithm to be applied to the outline
174 */
DrawScopenull175 fun DrawScope.drawOutline(
176 outline: Outline,
177 brush: Brush,
178 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
179 style: DrawStyle = Fill,
180 colorFilter: ColorFilter? = null,
181 blendMode: BlendMode = DrawScope.DefaultBlendMode
182 ) =
183 drawOutlineHelper(
184 outline,
185 { rect ->
186 drawRect(brush, rect.topLeft(), rect.size(), alpha, style, colorFilter, blendMode)
187 },
rrectnull188 { rrect ->
189 val radius = rrect.bottomLeftCornerRadius.x
190 drawRoundRect(
191 brush = brush,
192 topLeft = rrect.topLeft(),
193 size = rrect.size(),
194 cornerRadius = CornerRadius(radius),
195 alpha = alpha,
196 style = style,
197 colorFilter = colorFilter,
198 blendMode = blendMode
199 )
200 },
pathnull201 { path -> drawPath(path, brush, alpha, style, colorFilter, blendMode) }
202 )
203
204 /** Convenience method to obtain an Offset from the Rect's top and left parameters */
Rectnull205 private fun Rect.topLeft(): Offset = Offset(left, top)
206
207 /** Convenience method to obtain a Size from the Rect's width and height */
208 private fun Rect.size(): Size = Size(width, height)
209
210 /** Convenience method to obtain an Offset from the RoundRect's top and left parameters */
211 private fun RoundRect.topLeft(): Offset = Offset(left, top)
212
213 /** Convenience method to obtain a Size from the RoundRect's width and height parameters */
214 private fun RoundRect.size(): Size = Size(width, height)
215
216 /**
217 * Helper method that allows for delegation of appropriate drawing call based on type of underlying
218 * outline shape
219 */
220 private inline fun DrawScope.drawOutlineHelper(
221 outline: Outline,
222 drawRectBlock: DrawScope.(rect: Rect) -> Unit,
223 drawRoundedRectBlock: DrawScope.(rrect: RoundRect) -> Unit,
224 drawPathBlock: DrawScope.(path: Path) -> Unit
225 ) =
226 when (outline) {
227 is Outline.Rectangle -> drawRectBlock(outline.rect)
228 is Outline.Rounded -> {
229 val path = outline.roundRectPath
230 // If the rounded rect has a path, then the corner radii are not the same across
231 // each of the corners, so we draw the given path.
232 // If there is no path available, then the corner radii are identical so call the
233 // Canvas primitive for drawing a rounded rectangle
234 if (path != null) {
235 drawPathBlock(path)
236 } else {
237 drawRoundedRectBlock(outline.roundRect)
238 }
239 }
240 is Outline.Generic -> drawPathBlock(outline.path)
241 }
242
243 /**
244 * Draws the [Outline] on a [Canvas].
245 *
246 * @param outline the outline to draw.
247 * @param paint the paint used for the drawing.
248 */
drawOutlinenull249 fun Canvas.drawOutline(outline: Outline, paint: Paint) =
250 when (outline) {
251 is Outline.Rectangle -> drawRect(outline.rect, paint)
252 is Outline.Rounded -> {
253 val path = outline.roundRectPath
254 // If the rounded rect has a path, then the corner radii are not the same across
255 // each of the corners, so we draw the given path.
256 // If there is no path available, then the corner radii are identical so call the
257 // Canvas primitive for drawing a rounded rectangle
258 if (path != null) {
259 drawPath(path, paint)
260 } else {
261 drawRoundRect(
262 left = outline.roundRect.left,
263 top = outline.roundRect.top,
264 right = outline.roundRect.right,
265 bottom = outline.roundRect.bottom,
266 radiusX = outline.roundRect.bottomLeftCornerRadius.x,
267 radiusY = outline.roundRect.bottomLeftCornerRadius.y,
268 paint = paint
269 )
270 }
271 }
272 is Outline.Generic -> drawPath(outline.path, paint)
273 }
274
275 /**
276 * Convenience method to determine if the corner radii of the RoundRect are identical in each of the
277 * corners. That is the x radius and the y radius are the same for each corner, however, the x and y
278 * can be different
279 */
RoundRectnull280 private fun RoundRect.hasSameCornerRadius(): Boolean {
281 val sameRadiusX =
282 bottomLeftCornerRadius.x == bottomRightCornerRadius.x &&
283 bottomRightCornerRadius.x == topRightCornerRadius.x &&
284 topRightCornerRadius.x == topLeftCornerRadius.x
285 val sameRadiusY =
286 bottomLeftCornerRadius.y == bottomRightCornerRadius.y &&
287 bottomRightCornerRadius.y == topRightCornerRadius.y &&
288 topRightCornerRadius.y == topLeftCornerRadius.y
289 return sameRadiusX && sameRadiusY
290 }
291