1 /*
2  * 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.geometry
18 
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.ui.util.fastIsFinite
21 import androidx.compose.ui.util.lerp
22 import kotlin.math.absoluteValue
23 import kotlin.math.max
24 import kotlin.math.min
25 
26 /** An immutable rounded rectangle with custom radii for all four corners. */
27 @Immutable
28 @Suppress("DataClassDefinition")
29 data class RoundRect(
30     /** The offset of the left edge of this rectangle from the x axis */
31     val left: Float,
32     /** The offset of the top edge of this rectangle from the y axis */
33     val top: Float,
34     /** The offset of the right edge of this rectangle from the x axis */
35     val right: Float,
36     /** The offset of the bottom edge of this rectangle from the y axis */
37     val bottom: Float,
38     /** The top-left radius */
39     val topLeftCornerRadius: CornerRadius = CornerRadius.Zero,
40 
41     /** The top-right radius */
42     val topRightCornerRadius: CornerRadius = CornerRadius.Zero,
43 
44     /** The bottom-right radius */
45     val bottomRightCornerRadius: CornerRadius = CornerRadius.Zero,
46 
47     /** The bottom-left radius */
48     val bottomLeftCornerRadius: CornerRadius = CornerRadius.Zero
49 ) {
50     /** The distance between the left and right edges of this rectangle. */
51     val width: Float
52         get() = right - left
53 
54     /** The distance between the top and bottom edges of this rectangle. */
55     val height: Float
56         get() = bottom - top
57 
58     /**
59      * Same RoundRect with scaled radii per side. If you need this call [scaledRadiiRect] instead.
60      * Not @Volatile since the computed result will always be the same even if we race and duplicate
61      * creation/computation in [scaledRadiiRect].
62      */
63     private var _scaledRadiiRect: RoundRect? = null
64 
65     /**
66      * Scales all radii so that on each side their sum will not pass the size of the width/height.
67      */
scaledRadiiRectnull68     private fun scaledRadiiRect(): RoundRect =
69         _scaledRadiiRect
70             ?: run {
71                     var scale = 1.0f
72                     scale =
73                         minRadius(scale, bottomLeftCornerRadius.y, topLeftCornerRadius.y, height)
74                     scale = minRadius(scale, topLeftCornerRadius.x, topRightCornerRadius.x, width)
75                     scale =
76                         minRadius(scale, topRightCornerRadius.y, bottomRightCornerRadius.y, height)
77                     scale =
78                         minRadius(scale, bottomRightCornerRadius.x, bottomLeftCornerRadius.x, width)
79 
80                     RoundRect(
81                         left = left * scale,
82                         top = top * scale,
83                         right = right * scale,
84                         bottom = bottom * scale,
85                         topLeftCornerRadius =
86                             CornerRadius(
87                                 topLeftCornerRadius.x * scale,
88                                 topLeftCornerRadius.y * scale
89                             ),
90                         topRightCornerRadius =
91                             CornerRadius(
92                                 topRightCornerRadius.x * scale,
93                                 topRightCornerRadius.y * scale
94                             ),
95                         bottomRightCornerRadius =
96                             CornerRadius(
97                                 bottomRightCornerRadius.x * scale,
98                                 bottomRightCornerRadius.y * scale
99                             ),
100                         bottomLeftCornerRadius =
101                             CornerRadius(
102                                 bottomLeftCornerRadius.x * scale,
103                                 bottomLeftCornerRadius.y * scale
104                             )
105                     )
106                 }
<lambda>null107                 .also {
108                     // This might happen racey on different threads, we don't care, it'll be the
109                     // same results.
110                     _scaledRadiiRect = it
111                 }
112 
113     /**
114      * Returns the minimum between min and scale to which radius1 and radius2 should be scaled with
115      * in order not to exceed the limit.
116      */
minRadiusnull117     private fun minRadius(min: Float, radius1: Float, radius2: Float, limit: Float): Float {
118         val sum = radius1 + radius2
119         return if (sum > limit && sum != 0.0f) {
120             min(min, limit / sum)
121         } else {
122             min
123         }
124     }
125 
126     /**
127      * Whether the point specified by the given offset (which is assumed to be relative to the
128      * origin) lies inside the rounded rectangle.
129      *
130      * This method may allocate (and cache) a copy of the object with normalized radii the first
131      * time it is called on a particular [RoundRect] instance. When using this method, prefer to
132      * reuse existing [RoundRect]s rather than recreating the object each time.
133      */
containsnull134     operator fun contains(point: Offset): Boolean {
135         if (point.x < left || point.x >= right || point.y < top || point.y >= bottom) {
136             return false
137             // outside bounding box
138         }
139 
140         val scaled = scaledRadiiRect()
141 
142         val x: Float
143         val y: Float
144         val radiusX: Float
145         val radiusY: Float
146         // check whether point is in one of the rounded corner areas
147         // x, y -> translate to ellipse center
148         if (
149             point.x < left + scaled.topLeftCornerRadius.x &&
150                 point.y < top + scaled.topLeftCornerRadius.y
151         ) {
152             x = point.x - left - scaled.topLeftCornerRadius.x
153             y = point.y - top - scaled.topLeftCornerRadius.y
154             radiusX = scaled.topLeftCornerRadius.x
155             radiusY = scaled.topLeftCornerRadius.y
156         } else if (
157             point.x > right - scaled.topRightCornerRadius.x &&
158                 point.y < top + scaled.topRightCornerRadius.y
159         ) {
160             x = point.x - right + scaled.topRightCornerRadius.x
161             y = point.y - top - scaled.topRightCornerRadius.y
162             radiusX = scaled.topRightCornerRadius.x
163             radiusY = scaled.topRightCornerRadius.y
164         } else if (
165             point.x > right - scaled.bottomRightCornerRadius.x &&
166                 point.y > bottom - scaled.bottomRightCornerRadius.y
167         ) {
168             x = point.x - right + scaled.bottomRightCornerRadius.x
169             y = point.y - bottom + scaled.bottomRightCornerRadius.y
170             radiusX = scaled.bottomRightCornerRadius.x
171             radiusY = scaled.bottomRightCornerRadius.y
172         } else if (
173             point.x < left + scaled.bottomLeftCornerRadius.x &&
174                 point.y > bottom - scaled.bottomLeftCornerRadius.y
175         ) {
176             x = point.x - left - scaled.bottomLeftCornerRadius.x
177             y = point.y - bottom + scaled.bottomLeftCornerRadius.y
178             radiusX = scaled.bottomLeftCornerRadius.x
179             radiusY = scaled.bottomLeftCornerRadius.y
180         } else {
181             return true
182             // inside and not within the rounded corner area
183         }
184 
185         val newX = x / radiusX
186         val newY = y / radiusY
187 
188         // check if the point is inside the unit circle
189         return newX * newX + newY * newY <= 1.0f
190     }
191 
toStringnull192     override fun toString(): String {
193         val tlRadius = topLeftCornerRadius
194         val trRadius = topRightCornerRadius
195         val brRadius = bottomRightCornerRadius
196         val blRadius = bottomLeftCornerRadius
197         val rect =
198             "${left.toStringAsFixed(1)}, " +
199                 "${top.toStringAsFixed(1)}, " +
200                 "${right.toStringAsFixed(1)}, " +
201                 bottom.toStringAsFixed(1)
202         if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) {
203             if (tlRadius.x == tlRadius.y) {
204                 return "RoundRect(rect=$rect, radius=${tlRadius.x.toStringAsFixed(1)})"
205             }
206             return "RoundRect(rect=$rect, x=${tlRadius.x.toStringAsFixed(1)}, " +
207                 "y=${tlRadius.y.toStringAsFixed(1)})"
208         }
209         return "RoundRect(" +
210             "rect=$rect, " +
211             "topLeft=$tlRadius, " +
212             "topRight=$trRadius, " +
213             "bottomRight=$brRadius, " +
214             "bottomLeft=$blRadius)"
215     }
216 
217     companion object {
218         /** A rounded rectangle with all the values set to zero. */
219         @kotlin.jvm.JvmStatic val Zero = RoundRect(0.0f, 0.0f, 0.0f, 0.0f, CornerRadius.Zero)
220     }
221 }
222 
223 /**
224  * Construct a rounded rectangle from its left, top, right, and bottom edges, and the same radii
225  * along its horizontal axis and its vertical axis.
226  */
RoundRectnull227 fun RoundRect(
228     left: Float,
229     top: Float,
230     right: Float,
231     bottom: Float,
232     radiusX: Float,
233     radiusY: Float
234 ): RoundRect {
235     val radius = CornerRadius(radiusX, radiusY)
236     return RoundRect(
237         left = left,
238         top = top,
239         right = right,
240         bottom = bottom,
241         topLeftCornerRadius = radius,
242         topRightCornerRadius = radius,
243         bottomRightCornerRadius = radius,
244         bottomLeftCornerRadius = radius
245     )
246 }
247 
248 /**
249  * Construct a rounded rectangle from its left, top, right, and bottom edges, and the same radius in
250  * each corner.
251  */
RoundRectnull252 fun RoundRect(left: Float, top: Float, right: Float, bottom: Float, cornerRadius: CornerRadius) =
253     RoundRect(left, top, right, bottom, cornerRadius.x, cornerRadius.y)
254 
255 /**
256  * Construct a rounded rectangle from its bounding box and the same radii along its horizontal axis
257  * and its vertical axis.
258  */
259 fun RoundRect(rect: Rect, radiusX: Float, radiusY: Float): RoundRect =
260     RoundRect(
261         left = rect.left,
262         top = rect.top,
263         right = rect.right,
264         bottom = rect.bottom,
265         radiusX = radiusX,
266         radiusY = radiusY
267     )
268 
269 /**
270  * Construct a rounded rectangle from its bounding box and a radius that is the same in each corner.
271  */
272 fun RoundRect(rect: Rect, cornerRadius: CornerRadius): RoundRect =
273     RoundRect(rect = rect, radiusX = cornerRadius.x, radiusY = cornerRadius.y)
274 
275 /**
276  * Construct a rounded rectangle from its bounding box and topLeft, topRight, bottomRight, and
277  * bottomLeft radii.
278  *
279  * The corner radii default to [CornerRadius.Zero], i.e. right-angled corners
280  */
281 fun RoundRect(
282     rect: Rect,
283     topLeft: CornerRadius = CornerRadius.Zero,
284     topRight: CornerRadius = CornerRadius.Zero,
285     bottomRight: CornerRadius = CornerRadius.Zero,
286     bottomLeft: CornerRadius = CornerRadius.Zero
287 ): RoundRect =
288     RoundRect(
289         left = rect.left,
290         top = rect.top,
291         right = rect.right,
292         bottom = rect.bottom,
293         topLeftCornerRadius = topLeft,
294         topRightCornerRadius = topRight,
295         bottomRightCornerRadius = bottomRight,
296         bottomLeftCornerRadius = bottomLeft
297     )
298 
299 /** Returns a new [RoundRect] translated by the given offset. */
300 fun RoundRect.translate(offset: Offset): RoundRect =
301     RoundRect(
302         left = left + offset.x,
303         top = top + offset.y,
304         right = right + offset.x,
305         bottom = bottom + offset.y,
306         topLeftCornerRadius = topLeftCornerRadius,
307         topRightCornerRadius = topRightCornerRadius,
308         bottomRightCornerRadius = bottomRightCornerRadius,
309         bottomLeftCornerRadius = bottomLeftCornerRadius
310     )
311 
312 /** The bounding box of this rounded rectangle (the rectangle with no rounded corners). */
313 val RoundRect.boundingRect: Rect
314     get() = Rect(left, top, right, bottom)
315 
316 /**
317  * The non-rounded rectangle that is constrained by the smaller of the two diagonals, with each
318  * diagonal traveling through the middle of the curve corners. The middle of a corner is the
319  * intersection of the curve with its respective quadrant bisector.
320  */
321 val RoundRect.safeInnerRect: Rect
322     get() {
323         val insetFactor = 0.29289321881f // 1-cos(pi/4)
324 
325         val leftRadius = max(bottomLeftCornerRadius.x, topLeftCornerRadius.x)
326         val topRadius = max(topLeftCornerRadius.y, topRightCornerRadius.y)
327         val rightRadius = max(topRightCornerRadius.x, bottomRightCornerRadius.x)
328         val bottomRadius = max(bottomRightCornerRadius.y, bottomLeftCornerRadius.y)
329 
330         return Rect(
331             left + leftRadius * insetFactor,
332             top + topRadius * insetFactor,
333             right - rightRadius * insetFactor,
334             bottom - bottomRadius * insetFactor
335         )
336     }
337 
338 /** Whether this rounded rectangle encloses a non-zero area. Negative areas are considered empty. */
339 val RoundRect.isEmpty
340     get() = left >= right || top >= bottom
341 
342 /** Whether all coordinates of this rounded rectangle are finite. */
343 val RoundRect.isFinite
344     get() =
345         left.fastIsFinite() && top.fastIsFinite() && right.fastIsFinite() && bottom.fastIsFinite()
346 
347 /** Whether this rounded rectangle is a simple rectangle with zero corner radii. */
348 val RoundRect.isRect
349     get(): Boolean =
350         topLeftCornerRadius.isZero() &&
351             topRightCornerRadius.isZero() &&
352             bottomLeftCornerRadius.isZero() &&
353             bottomRightCornerRadius.isZero()
354 
355 /** Whether this rounded rectangle has no side with a straight section. */
356 val RoundRect.isEllipse
357     get(): Boolean =
358         topLeftCornerRadius.packedValue == topRightCornerRadius.packedValue &&
359             topRightCornerRadius.packedValue == bottomRightCornerRadius.packedValue &&
360             bottomRightCornerRadius.packedValue == bottomLeftCornerRadius.packedValue &&
361             width <= 2.0 * topLeftCornerRadius.x &&
362             height <= 2.0 * topLeftCornerRadius.y
363 
364 /** Whether this rounded rectangle would draw as a circle. */
365 val RoundRect.isCircle
366     get() = width == height && isEllipse
367 
368 /**
369  * The lesser of the magnitudes of the [RoundRect.width] and the [RoundRect.height] of this rounded
370  * rectangle.
371  */
372 val RoundRect.minDimension
373     get(): Float = min(width.absoluteValue, height.absoluteValue)
374 
375 val RoundRect.maxDimension
376     get(): Float = max(width.absoluteValue, height.absoluteValue)
377 
378 /**
379  * The offset to the point halfway between the left and right and the top and bottom edges of this
380  * rectangle.
381  */
382 val RoundRect.center: Offset
383     get() = Offset((left + width / 2.0f), (top + height / 2.0f))
384 
385 /**
386  * Returns `true` if the rounded rectangle have the same radii in both the horizontal and vertical
387  * direction for all corners.
388  */
389 val RoundRect.isSimple: Boolean
390     get() =
391         topLeftCornerRadius.isCircular() &&
392             topLeftCornerRadius.packedValue == topRightCornerRadius.packedValue &&
393             topLeftCornerRadius.packedValue == bottomRightCornerRadius.packedValue &&
394             topLeftCornerRadius.packedValue == bottomLeftCornerRadius.packedValue
395 
396 /**
397  * Linearly interpolate between two rounded rectangles.
398  *
399  * The [fraction] argument represents position on the timeline, with 0.0 meaning that the
400  * interpolation has not started, returning [start] (or something equivalent to [start]), 1.0
401  * meaning that the interpolation has finished, returning [stop] (or something equivalent to
402  * [stop]), and values in between meaning that the interpolation is at the relevant point on the
403  * timeline between [start] and [stop]. The interpolation can be extrapolated beyond 0.0 and 1.0, so
404  * negative values and values greater than 1.0 are valid (and can easily be generated by curves).
405  *
406  * Values for [fraction] are usually obtained from an [Animation<Float>], such as an
407  * `AnimationController`.
408  */
lerpnull409 fun lerp(start: RoundRect, stop: RoundRect, fraction: Float): RoundRect =
410     RoundRect(
411         left = lerp(start.left, stop.left, fraction),
412         top = lerp(start.top, stop.top, fraction),
413         right = lerp(start.right, stop.right, fraction),
414         bottom = lerp(start.bottom, stop.bottom, fraction),
415         topLeftCornerRadius = lerp(start.topLeftCornerRadius, stop.topLeftCornerRadius, fraction),
416         topRightCornerRadius =
417             lerp(start.topRightCornerRadius, stop.topRightCornerRadius, fraction),
418         bottomRightCornerRadius =
419             lerp(start.bottomRightCornerRadius, stop.bottomRightCornerRadius, fraction),
420         bottomLeftCornerRadius =
421             lerp(start.bottomLeftCornerRadius, stop.bottomLeftCornerRadius, fraction)
422     )
423