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