1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'box_border.dart';
6/// @docImport 'box_decoration.dart';
7/// @docImport 'shape_decoration.dart';
8library;
9
10import 'dart:ui' as ui show lerpDouble;
11
12import 'package:flutter/foundation.dart';
13
14import 'basic_types.dart';
15import 'border_radius.dart';
16import 'borders.dart';
17import 'circle_border.dart';
18
19// A common interface for [RoundedRectangleBorder] and [RoundedSuperellipseBorder].
20mixin _RRectLikeBorder on OutlinedBorder {
21 BorderRadiusGeometry get borderRadius;
22}
23
24/// A rectangular border with rounded corners.
25///
26/// Typically used with [ShapeDecoration] to draw a box with a rounded
27/// rectangle.
28///
29/// This shape can interpolate to and from [CircleBorder].
30///
31/// See also:
32///
33/// * [BorderSide], which is used to describe each side of the box.
34/// * [Border], which, when used with [BoxDecoration], can also
35/// describe a rounded rectangle.
36/// * [RoundedSuperellipseBorder], which uses a smoother shape similar to the one
37/// used in iOS design.
38class RoundedRectangleBorder extends OutlinedBorder with _RRectLikeBorder {
39 /// Creates a rounded rectangle border.
40 const RoundedRectangleBorder({super.side, this.borderRadius = BorderRadius.zero});
41
42 /// The radii for each corner.
43 @override
44 final BorderRadiusGeometry borderRadius;
45
46 @override
47 ShapeBorder scale(double t) {
48 return RoundedRectangleBorder(side: side.scale(t), borderRadius: borderRadius * t);
49 }
50
51 @override
52 ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
53 if (a is RoundedRectangleBorder) {
54 return RoundedRectangleBorder(
55 side: BorderSide.lerp(a.side, side, t),
56 borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
57 );
58 }
59 if (a is CircleBorder) {
60 return _RoundedRectangleToCircleBorder(
61 side: BorderSide.lerp(a.side, side, t),
62 borderRadius: borderRadius,
63 circularity: 1.0 - t,
64 eccentricity: a.eccentricity,
65 );
66 }
67 return super.lerpFrom(a, t);
68 }
69
70 @override
71 ShapeBorder? lerpTo(ShapeBorder? b, double t) {
72 if (b is RoundedRectangleBorder) {
73 return RoundedRectangleBorder(
74 side: BorderSide.lerp(side, b.side, t),
75 borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
76 );
77 }
78 if (b is CircleBorder) {
79 return _RoundedRectangleToCircleBorder(
80 side: BorderSide.lerp(side, b.side, t),
81 borderRadius: borderRadius,
82 circularity: t,
83 eccentricity: b.eccentricity,
84 );
85 }
86 return super.lerpTo(b, t);
87 }
88
89 /// Returns a copy of this RoundedRectangleBorder with the given fields
90 /// replaced with the new values.
91 @override
92 RoundedRectangleBorder copyWith({BorderSide? side, BorderRadiusGeometry? borderRadius}) {
93 return RoundedRectangleBorder(
94 side: side ?? this.side,
95 borderRadius: borderRadius ?? this.borderRadius,
96 );
97 }
98
99 @override
100 Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
101 final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
102 final RRect adjustedRect = borderRect.deflate(side.strokeInset);
103 return Path()..addRRect(adjustedRect);
104 }
105
106 @override
107 Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
108 return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
109 }
110
111 @override
112 void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
113 if (borderRadius == BorderRadius.zero) {
114 canvas.drawRect(rect, paint);
115 } else {
116 canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), paint);
117 }
118 }
119
120 @override
121 bool get preferPaintInterior => true;
122
123 @override
124 void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
125 switch (side.style) {
126 case BorderStyle.none:
127 break;
128 case BorderStyle.solid:
129 if (side.width == 0.0) {
130 canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), side.toPaint());
131 } else {
132 final Paint paint = Paint()..color = side.color;
133 final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
134 final RRect inner = borderRect.deflate(side.strokeInset);
135 final RRect outer = borderRect.inflate(side.strokeOutset);
136 canvas.drawDRRect(outer, inner, paint);
137 }
138 }
139 }
140
141 @override
142 bool operator ==(Object other) {
143 if (other.runtimeType != runtimeType) {
144 return false;
145 }
146 return other is RoundedRectangleBorder &&
147 other.side == side &&
148 other.borderRadius == borderRadius;
149 }
150
151 @override
152 int get hashCode => Object.hash(side, borderRadius);
153
154 @override
155 String toString() {
156 return '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)';
157 }
158}
159
160class _RoundedRectangleToCircleBorder extends _ShapeToCircleBorder<RoundedRectangleBorder> {
161 const _RoundedRectangleToCircleBorder({
162 super.side,
163 super.borderRadius = BorderRadius.zero,
164 required super.circularity,
165 required super.eccentricity,
166 });
167
168 @override
169 void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]) {
170 RRect rrect = radius.toRRect(rect);
171 if (inflation != null) {
172 rrect = rrect.inflate(inflation);
173 }
174 canvas.drawRRect(rrect, paint);
175 }
176
177 @override
178 Path buildPath(Rect rect, BorderRadius radius, [double? inflation]) {
179 RRect rrect = radius.toRRect(rect);
180 if (inflation != null) {
181 rrect = rrect.inflate(inflation);
182 }
183 return Path()..addRRect(rrect);
184 }
185
186 @override
187 _RoundedRectangleToCircleBorder copyWith({
188 BorderSide? side,
189 BorderRadiusGeometry? borderRadius,
190 double? circularity,
191 double? eccentricity,
192 }) {
193 return _RoundedRectangleToCircleBorder(
194 side: side ?? this.side,
195 borderRadius: borderRadius ?? this.borderRadius,
196 circularity: circularity ?? this.circularity,
197 eccentricity: eccentricity ?? this.eccentricity,
198 );
199 }
200}
201
202/// A rectangular border with rounded corners following the shape of an
203/// [RSuperellipse].
204///
205/// Typically used with [ShapeDecoration] to draw a box that mimics the rounded
206/// rectangle style commonly seen in iOS design.
207///
208/// See also:
209///
210/// * [RSuperellipse], which defines the shape.
211/// * [RoundedRectangleBorder], which uses the traditional [RRect] shape.
212class RoundedSuperellipseBorder extends OutlinedBorder with _RRectLikeBorder {
213 /// Creates a rounded rectangle border.
214 const RoundedSuperellipseBorder({super.side, this.borderRadius = BorderRadius.zero});
215
216 /// The radii for each corner.
217 @override
218 final BorderRadiusGeometry borderRadius;
219
220 @override
221 ShapeBorder scale(double t) {
222 return RoundedSuperellipseBorder(side: side.scale(t), borderRadius: borderRadius * t);
223 }
224
225 @override
226 ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
227 if (a is RoundedSuperellipseBorder) {
228 return RoundedSuperellipseBorder(
229 side: BorderSide.lerp(a.side, side, t),
230 borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
231 );
232 }
233 if (a is CircleBorder) {
234 return _RoundedSuperellipseToCircleBorder(
235 side: BorderSide.lerp(a.side, side, t),
236 borderRadius: borderRadius,
237 circularity: 1.0 - t,
238 eccentricity: a.eccentricity,
239 );
240 }
241 return super.lerpFrom(a, t);
242 }
243
244 @override
245 ShapeBorder? lerpTo(ShapeBorder? b, double t) {
246 if (b is RoundedSuperellipseBorder) {
247 return RoundedSuperellipseBorder(
248 side: BorderSide.lerp(side, b.side, t),
249 borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
250 );
251 }
252 if (b is CircleBorder) {
253 return _RoundedSuperellipseToCircleBorder(
254 side: BorderSide.lerp(side, b.side, t),
255 borderRadius: borderRadius,
256 circularity: t,
257 eccentricity: b.eccentricity,
258 );
259 }
260 return super.lerpTo(b, t);
261 }
262
263 /// Returns a copy of this RoundedSuperellipseBorder with the given fields
264 /// replaced with the new values.
265 @override
266 RoundedSuperellipseBorder copyWith({BorderSide? side, BorderRadiusGeometry? borderRadius}) {
267 return RoundedSuperellipseBorder(
268 side: side ?? this.side,
269 borderRadius: borderRadius ?? this.borderRadius,
270 );
271 }
272
273 @override
274 Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
275 final RSuperellipse borderRect = borderRadius.resolve(textDirection).toRSuperellipse(rect);
276 final RSuperellipse adjustedRect = borderRect.deflate(side.strokeInset);
277 return Path()..addRSuperellipse(adjustedRect);
278 }
279
280 @override
281 Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
282 return Path()..addRSuperellipse(borderRadius.resolve(textDirection).toRSuperellipse(rect));
283 }
284
285 @override
286 void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
287 if (borderRadius == BorderRadius.zero) {
288 canvas.drawRect(rect, paint);
289 } else {
290 canvas.drawRSuperellipse(borderRadius.resolve(textDirection).toRSuperellipse(rect), paint);
291 }
292 }
293
294 @override
295 bool get preferPaintInterior => true;
296
297 @override
298 void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
299 switch (side.style) {
300 case BorderStyle.none:
301 break;
302 case BorderStyle.solid:
303 if (side.width == 0.0) {
304 canvas.drawRSuperellipse(
305 borderRadius.resolve(textDirection).toRSuperellipse(rect),
306 side.toPaint(),
307 );
308 } else {
309 final double strokeOffset = (side.strokeOutset - side.strokeInset) / 2;
310 final RSuperellipse base = borderRadius
311 .resolve(textDirection)
312 .toRSuperellipse(rect)
313 .inflate(strokeOffset);
314 canvas.drawRSuperellipse(base, side.toPaint());
315 }
316 }
317 }
318
319 @override
320 bool operator ==(Object other) {
321 if (other.runtimeType != runtimeType) {
322 return false;
323 }
324 return other is RoundedSuperellipseBorder &&
325 other.side == side &&
326 other.borderRadius == borderRadius;
327 }
328
329 @override
330 int get hashCode => Object.hash(side, borderRadius);
331
332 @override
333 String toString() {
334 return '${objectRuntimeType(this, 'RoundedSuperellipseBorder')}($side, $borderRadius)';
335 }
336}
337
338class _RoundedSuperellipseToCircleBorder extends _ShapeToCircleBorder<RoundedSuperellipseBorder> {
339 const _RoundedSuperellipseToCircleBorder({
340 super.side,
341 super.borderRadius = BorderRadius.zero,
342 required super.circularity,
343 required super.eccentricity,
344 });
345
346 @override
347 void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]) {
348 RSuperellipse rsuperellipse = radius.toRSuperellipse(rect);
349 if (inflation != null) {
350 rsuperellipse = rsuperellipse.inflate(inflation);
351 }
352 canvas.drawRSuperellipse(rsuperellipse, paint);
353 }
354
355 @override
356 Path buildPath(Rect rect, BorderRadius radius, [double? inflation]) {
357 RSuperellipse rsuperellipse = radius.toRSuperellipse(rect);
358 if (inflation != null) {
359 rsuperellipse = rsuperellipse.inflate(inflation);
360 }
361 return Path()..addRSuperellipse(rsuperellipse);
362 }
363
364 @override
365 _RoundedSuperellipseToCircleBorder copyWith({
366 BorderSide? side,
367 BorderRadiusGeometry? borderRadius,
368 double? circularity,
369 double? eccentricity,
370 }) {
371 return _RoundedSuperellipseToCircleBorder(
372 side: side ?? this.side,
373 borderRadius: borderRadius ?? this.borderRadius,
374 circularity: circularity ?? this.circularity,
375 eccentricity: eccentricity ?? this.eccentricity,
376 );
377 }
378}
379
380abstract class _ShapeToCircleBorder<T extends _RRectLikeBorder> extends OutlinedBorder {
381 const _ShapeToCircleBorder({
382 super.side,
383 this.borderRadius = BorderRadius.zero,
384 required this.circularity,
385 required this.eccentricity,
386 });
387
388 void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]);
389 Path buildPath(Rect rect, BorderRadius radius, [double? inflation]);
390
391 final BorderRadiusGeometry borderRadius;
392 final double circularity;
393 final double eccentricity;
394
395 @override
396 ShapeBorder scale(double t) {
397 return copyWith(
398 side: side.scale(t),
399 borderRadius: borderRadius * t,
400 circularity: t,
401 eccentricity: eccentricity,
402 );
403 }
404
405 @override
406 ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
407 if (a is T) {
408 return copyWith(
409 side: BorderSide.lerp(a.side, side, t),
410 borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t),
411 circularity: circularity * t,
412 eccentricity: eccentricity,
413 );
414 }
415 if (a is CircleBorder) {
416 return copyWith(
417 side: BorderSide.lerp(a.side, side, t),
418 borderRadius: borderRadius,
419 circularity: circularity + (1.0 - circularity) * (1.0 - t),
420 eccentricity: a.eccentricity,
421 );
422 }
423 if (a is _ShapeToCircleBorder<T>) {
424 return copyWith(
425 side: BorderSide.lerp(a.side, side, t),
426 borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t),
427 circularity: ui.lerpDouble(a.circularity, circularity, t),
428 eccentricity: eccentricity,
429 );
430 }
431 return super.lerpFrom(a, t);
432 }
433
434 @override
435 ShapeBorder? lerpTo(ShapeBorder? b, double t) {
436 if (b is T) {
437 return copyWith(
438 side: BorderSide.lerp(side, b.side, t),
439 borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t),
440 circularity: circularity * (1.0 - t),
441 eccentricity: eccentricity,
442 );
443 }
444 if (b is CircleBorder) {
445 return copyWith(
446 side: BorderSide.lerp(side, b.side, t),
447 borderRadius: borderRadius,
448 circularity: circularity + (1.0 - circularity) * t,
449 eccentricity: b.eccentricity,
450 );
451 }
452 if (b is _ShapeToCircleBorder<T>) {
453 return copyWith(
454 side: BorderSide.lerp(side, b.side, t),
455 borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t),
456 circularity: ui.lerpDouble(circularity, b.circularity, t),
457 eccentricity: eccentricity,
458 );
459 }
460 return super.lerpTo(b, t);
461 }
462
463 Rect _adjustRect(Rect rect) {
464 if (circularity == 0.0 || rect.width == rect.height) {
465 return rect;
466 }
467 if (rect.width < rect.height) {
468 final double partialDelta = (rect.height - rect.width) / 2;
469 final double delta = circularity * partialDelta * (1.0 - eccentricity);
470 return Rect.fromLTRB(rect.left, rect.top + delta, rect.right, rect.bottom - delta);
471 } else {
472 final double partialDelta = (rect.width - rect.height) / 2;
473 final double delta = circularity * partialDelta * (1.0 - eccentricity);
474 return Rect.fromLTRB(rect.left + delta, rect.top, rect.right - delta, rect.bottom);
475 }
476 }
477
478 BorderRadius _adjustBorderRadius(Rect rect, TextDirection? textDirection) {
479 final BorderRadius resolvedRadius = borderRadius.resolve(textDirection);
480 if (circularity == 0.0) {
481 return resolvedRadius;
482 }
483 if (eccentricity != 0.0) {
484 if (rect.width < rect.height) {
485 return BorderRadius.lerp(
486 resolvedRadius,
487 BorderRadius.all(
488 Radius.elliptical(rect.width / 2, (0.5 + eccentricity / 2) * rect.height / 2),
489 ),
490 circularity,
491 )!;
492 } else {
493 return BorderRadius.lerp(
494 resolvedRadius,
495 BorderRadius.all(
496 Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2),
497 ),
498 circularity,
499 )!;
500 }
501 }
502 return BorderRadius.lerp(
503 resolvedRadius,
504 BorderRadius.circular(rect.shortestSide / 2),
505 circularity,
506 )!;
507 }
508
509 @override
510 Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
511 return buildPath(
512 _adjustRect(rect),
513 _adjustBorderRadius(rect, textDirection),
514 -ui.lerpDouble(side.width, 0, side.strokeAlign)!,
515 );
516 }
517
518 @override
519 Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
520 return buildPath(_adjustRect(rect), _adjustBorderRadius(rect, textDirection));
521 }
522
523 @override
524 void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
525 final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection);
526 if (adjustedBorderRadius == BorderRadius.zero) {
527 canvas.drawRect(_adjustRect(rect), paint);
528 } else {
529 drawShape(canvas, _adjustRect(rect), adjustedBorderRadius, paint);
530 }
531 }
532
533 @override
534 bool get preferPaintInterior => true;
535 @override
536 _ShapeToCircleBorder<T> copyWith({
537 BorderSide? side,
538 BorderRadiusGeometry? borderRadius,
539 double? circularity,
540 double? eccentricity,
541 });
542
543 @override
544 void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
545 switch (side.style) {
546 case BorderStyle.none:
547 break;
548 case BorderStyle.solid:
549 drawShape(
550 canvas,
551 _adjustRect(rect),
552 _adjustBorderRadius(rect, textDirection),
553 side.toPaint(),
554 side.strokeOffset / 2,
555 );
556 }
557 }
558
559 @override
560 bool operator ==(Object other) {
561 if (other.runtimeType != runtimeType) {
562 return false;
563 }
564 return other is _ShapeToCircleBorder<T> &&
565 other.side == side &&
566 other.borderRadius == borderRadius &&
567 other.circularity == circularity;
568 }
569
570 @override
571 int get hashCode => Object.hash(side, borderRadius, circularity);
572
573 @override
574 String toString() {
575 if (eccentricity != 0.0) {
576 return '$T($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
577 }
578 return '$T($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
579 }
580}
581

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com