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