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
5import 'package:flutter/foundation.dart';
6
7import 'basic_types.dart';
8import 'border_radius.dart';
9import 'borders.dart';
10import 'edge_insets.dart';
11
12// Examples can assume:
13// late BuildContext context;
14
15/// The shape to use when rendering a [Border] or [BoxDecoration].
16///
17/// Consider using [ShapeBorder] subclasses directly (with [ShapeDecoration]),
18/// instead of using [BoxShape] and [Border], if the shapes will need to be
19/// interpolated or animated. The [Border] class cannot interpolate between
20/// different shapes.
21enum BoxShape {
22 /// An axis-aligned, 2D rectangle. May have rounded corners (described by a
23 /// [BorderRadius]). The edges of the rectangle will match the edges of the box
24 /// into which the [Border] or [BoxDecoration] is painted.
25 ///
26 /// See also:
27 ///
28 /// * [RoundedRectangleBorder], the equivalent [ShapeBorder].
29 rectangle,
30
31 /// A circle centered in the middle of the box into which the [Border] or
32 /// [BoxDecoration] is painted. The diameter of the circle is the shortest
33 /// dimension of the box, either the width or the height, such that the circle
34 /// touches the edges of the box.
35 ///
36 /// See also:
37 ///
38 /// * [CircleBorder], the equivalent [ShapeBorder].
39 circle,
40
41 // Don't add more, instead create a new ShapeBorder.
42}
43
44/// Base class for box borders that can paint as rectangles, circles, or rounded
45/// rectangles.
46///
47/// This class is extended by [Border] and [BorderDirectional] to provide
48/// concrete versions of four-sided borders using different conventions for
49/// specifying the sides.
50///
51/// The only API difference that this class introduces over [ShapeBorder] is
52/// that its [paint] method takes additional arguments.
53///
54/// See also:
55///
56/// * [BorderSide], which is used to describe each side of the box.
57/// * [RoundedRectangleBorder], another way of describing a box's border.
58/// * [CircleBorder], another way of describing a circle border.
59/// * [BoxDecoration], which uses a [BoxBorder] to describe its borders.
60abstract class BoxBorder extends ShapeBorder {
61 /// Abstract const constructor. This constructor enables subclasses to provide
62 /// const constructors so that they can be used in const expressions.
63 const BoxBorder();
64
65 /// The top side of this border.
66 ///
67 /// This getter is available on both [Border] and [BorderDirectional]. If
68 /// [isUniform] is true, then this is the same style as all the other sides.
69 BorderSide get top;
70
71 /// The bottom side of this border.
72 BorderSide get bottom;
73
74 /// Whether all four sides of the border are identical. Uniform borders are
75 /// typically more efficient to paint.
76 ///
77 /// A uniform border by definition has no text direction dependency and
78 /// therefore could be expressed as a [Border], even if it is currently a
79 /// [BorderDirectional]. A uniform border can also be expressed as a
80 /// [RoundedRectangleBorder].
81 bool get isUniform;
82
83 // We override this to tighten the return value, so that callers can assume
84 // that we'll return a [BoxBorder].
85 @override
86 BoxBorder? add(ShapeBorder other, { bool reversed = false }) => null;
87
88 /// Linearly interpolate between two borders.
89 ///
90 /// If a border is null, it is treated as having four [BorderSide.none]
91 /// borders.
92 ///
93 /// This supports interpolating between [Border] and [BorderDirectional]
94 /// objects. If both objects are different types but both have sides on one or
95 /// both of their lateral edges (the two sides that aren't the top and bottom)
96 /// other than [BorderSide.none], then the sides are interpolated by reducing
97 /// `a`'s lateral edges to [BorderSide.none] over the first half of the
98 /// animation, and then bringing `b`'s lateral edges _from_ [BorderSide.none]
99 /// over the second half of the animation.
100 ///
101 /// For a more flexible approach, consider [ShapeBorder.lerp], which would
102 /// instead [add] the two sets of sides and interpolate them simultaneously.
103 ///
104 /// {@macro dart.ui.shadow.lerp}
105 static BoxBorder? lerp(BoxBorder? a, BoxBorder? b, double t) {
106 if (identical(a, b)) {
107 return a;
108 }
109 if ((a is Border?) && (b is Border?)) {
110 return Border.lerp(a, b, t);
111 }
112 if ((a is BorderDirectional?) && (b is BorderDirectional?)) {
113 return BorderDirectional.lerp(a, b, t);
114 }
115 if (b is Border && a is BorderDirectional) {
116 final BoxBorder c = b;
117 b = a;
118 a = c;
119 t = 1.0 - t;
120 // fall through to next case
121 }
122 if (a is Border && b is BorderDirectional) {
123 if (b.start == BorderSide.none && b.end == BorderSide.none) {
124 // The fact that b is a BorderDirectional really doesn't matter, it turns out.
125 return Border(
126 top: BorderSide.lerp(a.top, b.top, t),
127 right: BorderSide.lerp(a.right, BorderSide.none, t),
128 bottom: BorderSide.lerp(a.bottom, b.bottom, t),
129 left: BorderSide.lerp(a.left, BorderSide.none, t),
130 );
131 }
132 if (a.left == BorderSide.none && a.right == BorderSide.none) {
133 // The fact that a is a Border really doesn't matter, it turns out.
134 return BorderDirectional(
135 top: BorderSide.lerp(a.top, b.top, t),
136 start: BorderSide.lerp(BorderSide.none, b.start, t),
137 end: BorderSide.lerp(BorderSide.none, b.end, t),
138 bottom: BorderSide.lerp(a.bottom, b.bottom, t),
139 );
140 }
141 // Since we have to swap a visual border for a directional one,
142 // we speed up the horizontal sides' transitions and switch from
143 // one mode to the other at t=0.5.
144 if (t < 0.5) {
145 return Border(
146 top: BorderSide.lerp(a.top, b.top, t),
147 right: BorderSide.lerp(a.right, BorderSide.none, t * 2.0),
148 bottom: BorderSide.lerp(a.bottom, b.bottom, t),
149 left: BorderSide.lerp(a.left, BorderSide.none, t * 2.0),
150 );
151 }
152 return BorderDirectional(
153 top: BorderSide.lerp(a.top, b.top, t),
154 start: BorderSide.lerp(BorderSide.none, b.start, (t - 0.5) * 2.0),
155 end: BorderSide.lerp(BorderSide.none, b.end, (t - 0.5) * 2.0),
156 bottom: BorderSide.lerp(a.bottom, b.bottom, t),
157 );
158 }
159 throw FlutterError.fromParts(<DiagnosticsNode>[
160 ErrorSummary('BoxBorder.lerp can only interpolate Border and BorderDirectional classes.'),
161 ErrorDescription(
162 'BoxBorder.lerp() was called with two objects of type ${a.runtimeType} and ${b.runtimeType}:\n'
163 ' $a\n'
164 ' $b\n'
165 'However, only Border and BorderDirectional classes are supported by this method.',
166 ),
167 ErrorHint('For a more general interpolation method, consider using ShapeBorder.lerp instead.'),
168 ]);
169 }
170
171 @override
172 Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
173 assert(textDirection != null, 'The textDirection argument to $runtimeType.getInnerPath must not be null.');
174 return Path()
175 ..addRect(dimensions.resolve(textDirection).deflateRect(rect));
176 }
177
178 @override
179 Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
180 assert(textDirection != null, 'The textDirection argument to $runtimeType.getOuterPath must not be null.');
181 return Path()
182 ..addRect(rect);
183 }
184
185 @override
186 void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
187 // For `ShapeDecoration(shape: Border.all())`, a rectangle with sharp edges
188 // is always painted. There is no borderRadius parameter for
189 // ShapeDecoration or Border, only for BoxDecoration, which doesn't call
190 // this method.
191 canvas.drawRect(rect, paint);
192 }
193
194 @override
195 bool get preferPaintInterior => true;
196
197 /// Paints the border within the given [Rect] on the given [Canvas].
198 ///
199 /// This is an extension of the [ShapeBorder.paint] method. It allows
200 /// [BoxBorder] borders to be applied to different [BoxShape]s and with
201 /// different [borderRadius] parameters, without changing the [BoxBorder]
202 /// object itself.
203 ///
204 /// The `shape` argument specifies the [BoxShape] to draw the border on.
205 ///
206 /// If the `shape` is specifies a rectangular box shape
207 /// ([BoxShape.rectangle]), then the `borderRadius` argument describes the
208 /// corners of the rectangle.
209 ///
210 /// The [getInnerPath] and [getOuterPath] methods do not know about the
211 /// `shape` and `borderRadius` arguments.
212 ///
213 /// See also:
214 ///
215 /// * [paintBorder], which is used if the border has non-uniform colors or styles and no borderRadius.
216 /// * [Border.paint], similar to this method, includes additional comments
217 /// and provides more details on each parameter than described here.
218 @override
219 void paint(
220 Canvas canvas,
221 Rect rect, {
222 TextDirection? textDirection,
223 BoxShape shape = BoxShape.rectangle,
224 BorderRadius? borderRadius,
225 });
226
227 static void _paintUniformBorderWithRadius(Canvas canvas, Rect rect, BorderSide side, BorderRadius borderRadius) {
228 assert(side.style != BorderStyle.none);
229 final Paint paint = Paint()
230 ..color = side.color;
231 final double width = side.width;
232 if (width == 0.0) {
233 paint
234 ..style = PaintingStyle.stroke
235 ..strokeWidth = 0.0;
236 canvas.drawRRect(borderRadius.toRRect(rect), paint);
237 } else {
238 final RRect borderRect = borderRadius.toRRect(rect);
239 final RRect inner = borderRect.deflate(side.strokeInset);
240 final RRect outer = borderRect.inflate(side.strokeOutset);
241 canvas.drawDRRect(outer, inner, paint);
242 }
243 }
244
245 /// Paints a Border with different widths, styles and strokeAligns, on any
246 /// borderRadius while using a single color.
247 ///
248 /// See also:
249 ///
250 /// * [paintBorder], which supports multiple colors but not borderRadius.
251 /// * [paint], which calls this method.
252 static void paintNonUniformBorder(
253 Canvas canvas,
254 Rect rect, {
255 required BorderRadius? borderRadius,
256 required TextDirection? textDirection,
257 BoxShape shape = BoxShape.rectangle,
258 BorderSide top = BorderSide.none,
259 BorderSide right = BorderSide.none,
260 BorderSide bottom = BorderSide.none,
261 BorderSide left = BorderSide.none,
262 required Color color,
263 }) {
264 final RRect borderRect;
265 switch (shape) {
266 case BoxShape.rectangle:
267 borderRect = (borderRadius ?? BorderRadius.zero)
268 .resolve(textDirection)
269 .toRRect(rect);
270 case BoxShape.circle:
271 assert(borderRadius == null, 'A borderRadius cannot be given when shape is a BoxShape.circle.');
272 borderRect = RRect.fromRectAndRadius(
273 Rect.fromCircle(center: rect.center, radius: rect.shortestSide / 2.0),
274 Radius.circular(rect.width),
275 );
276 }
277 final Paint paint = Paint()..color = color;
278 final RRect inner = _deflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeInset, top.strokeInset, right.strokeInset, bottom.strokeInset));
279 final RRect outer = _inflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeOutset, top.strokeOutset, right.strokeOutset, bottom.strokeOutset));
280 canvas.drawDRRect(outer, inner, paint);
281 }
282
283 static RRect _inflateRRect(RRect rect, EdgeInsets insets) {
284 return RRect.fromLTRBAndCorners(
285 rect.left - insets.left,
286 rect.top - insets.top,
287 rect.right + insets.right,
288 rect.bottom + insets.bottom,
289 topLeft: (rect.tlRadius + Radius.elliptical(insets.left, insets.top)).clamp(minimum: Radius.zero),
290 topRight: (rect.trRadius + Radius.elliptical(insets.right, insets.top)).clamp(minimum: Radius.zero),
291 bottomRight: (rect.brRadius + Radius.elliptical(insets.right, insets.bottom)).clamp(minimum: Radius.zero),
292 bottomLeft: (rect.blRadius + Radius.elliptical(insets.left, insets.bottom)).clamp(minimum: Radius.zero),
293 );
294 }
295
296 static RRect _deflateRRect(RRect rect, EdgeInsets insets) {
297 return RRect.fromLTRBAndCorners(
298 rect.left + insets.left,
299 rect.top + insets.top,
300 rect.right - insets.right,
301 rect.bottom - insets.bottom,
302 topLeft: (rect.tlRadius - Radius.elliptical(insets.left, insets.top)).clamp(minimum: Radius.zero),
303 topRight: (rect.trRadius - Radius.elliptical(insets.right, insets.top)).clamp(minimum: Radius.zero),
304 bottomRight: (rect.brRadius - Radius.elliptical(insets.right, insets.bottom)).clamp(minimum: Radius.zero),
305 bottomLeft:(rect.blRadius - Radius.elliptical(insets.left, insets.bottom)).clamp(minimum: Radius.zero),
306 );
307 }
308
309 static void _paintUniformBorderWithCircle(Canvas canvas, Rect rect, BorderSide side) {
310 assert(side.style != BorderStyle.none);
311 final double radius = (rect.shortestSide + side.strokeOffset) / 2;
312 canvas.drawCircle(rect.center, radius, side.toPaint());
313 }
314
315 static void _paintUniformBorderWithRectangle(Canvas canvas, Rect rect, BorderSide side) {
316 assert(side.style != BorderStyle.none);
317 canvas.drawRect(rect.inflate(side.strokeOffset / 2), side.toPaint());
318 }
319}
320
321/// A border of a box, comprised of four sides: top, right, bottom, left.
322///
323/// The sides are represented by [BorderSide] objects.
324///
325/// {@tool snippet}
326///
327/// All four borders the same, two-pixel wide solid white:
328///
329/// ```dart
330/// Border.all(width: 2.0, color: const Color(0xFFFFFFFF))
331/// ```
332/// {@end-tool}
333/// {@tool snippet}
334///
335/// The border for a Material Design divider:
336///
337/// ```dart
338/// Border(bottom: BorderSide(color: Theme.of(context).dividerColor))
339/// ```
340/// {@end-tool}
341/// {@tool snippet}
342///
343/// A 1990s-era "OK" button:
344///
345/// ```dart
346/// Container(
347/// decoration: const BoxDecoration(
348/// border: Border(
349/// top: BorderSide(color: Color(0xFFFFFFFF)),
350/// left: BorderSide(color: Color(0xFFFFFFFF)),
351/// right: BorderSide(),
352/// bottom: BorderSide(),
353/// ),
354/// ),
355/// child: Container(
356/// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0),
357/// decoration: const BoxDecoration(
358/// border: Border(
359/// top: BorderSide(color: Color(0xFFDFDFDF)),
360/// left: BorderSide(color: Color(0xFFDFDFDF)),
361/// right: BorderSide(color: Color(0xFF7F7F7F)),
362/// bottom: BorderSide(color: Color(0xFF7F7F7F)),
363/// ),
364/// color: Color(0xFFBFBFBF),
365/// ),
366/// child: const Text(
367/// 'OK',
368/// textAlign: TextAlign.center,
369/// style: TextStyle(color: Color(0xFF000000))
370/// ),
371/// ),
372/// )
373/// ```
374/// {@end-tool}
375///
376/// See also:
377///
378/// * [BoxDecoration], which uses this class to describe its edge decoration.
379/// * [BorderSide], which is used to describe each side of the box.
380/// * [Theme], from the material layer, which can be queried to obtain appropriate colors
381/// to use for borders in a [MaterialApp], as shown in the "divider" sample above.
382/// * [paint], which explains the behavior of [BoxDecoration] parameters.
383/// * <https://pub.dev/packages/non_uniform_border>, a package that implements
384/// a Non-Uniform Border on ShapeBorder, which is used by Material Design
385/// buttons and other widgets, under the "shape" field.
386class Border extends BoxBorder {
387 /// Creates a border.
388 ///
389 /// All the sides of the border default to [BorderSide.none].
390 const Border({
391 this.top = BorderSide.none,
392 this.right = BorderSide.none,
393 this.bottom = BorderSide.none,
394 this.left = BorderSide.none,
395 });
396
397 /// Creates a border whose sides are all the same.
398 const Border.fromBorderSide(BorderSide side)
399 : top = side,
400 right = side,
401 bottom = side,
402 left = side;
403
404 /// Creates a border with symmetrical vertical and horizontal sides.
405 ///
406 /// The `vertical` argument applies to the [left] and [right] sides, and the
407 /// `horizontal` argument applies to the [top] and [bottom] sides.
408 ///
409 /// All arguments default to [BorderSide.none].
410 const Border.symmetric({
411 BorderSide vertical = BorderSide.none,
412 BorderSide horizontal = BorderSide.none,
413 }) : left = vertical,
414 top = horizontal,
415 right = vertical,
416 bottom = horizontal;
417
418 /// A uniform border with all sides the same color and width.
419 ///
420 /// The sides default to black solid borders, one logical pixel wide.
421 factory Border.all({
422 Color color = const Color(0xFF000000),
423 double width = 1.0,
424 BorderStyle style = BorderStyle.solid,
425 double strokeAlign = BorderSide.strokeAlignInside,
426 }) {
427 final BorderSide side = BorderSide(color: color, width: width, style: style, strokeAlign: strokeAlign);
428 return Border.fromBorderSide(side);
429 }
430
431 /// Creates a [Border] that represents the addition of the two given
432 /// [Border]s.
433 ///
434 /// It is only valid to call this if [BorderSide.canMerge] returns true for
435 /// the pairwise combination of each side on both [Border]s.
436 static Border merge(Border a, Border b) {
437 assert(BorderSide.canMerge(a.top, b.top));
438 assert(BorderSide.canMerge(a.right, b.right));
439 assert(BorderSide.canMerge(a.bottom, b.bottom));
440 assert(BorderSide.canMerge(a.left, b.left));
441 return Border(
442 top: BorderSide.merge(a.top, b.top),
443 right: BorderSide.merge(a.right, b.right),
444 bottom: BorderSide.merge(a.bottom, b.bottom),
445 left: BorderSide.merge(a.left, b.left),
446 );
447 }
448
449 @override
450 final BorderSide top;
451
452 /// The right side of this border.
453 final BorderSide right;
454
455 @override
456 final BorderSide bottom;
457
458 /// The left side of this border.
459 final BorderSide left;
460
461 @override
462 EdgeInsetsGeometry get dimensions {
463 if (_widthIsUniform) {
464 return EdgeInsets.all(top.strokeInset);
465 }
466 return EdgeInsets.fromLTRB(left.strokeInset, top.strokeInset, right.strokeInset, bottom.strokeInset);
467 }
468
469 @override
470 bool get isUniform => _colorIsUniform && _widthIsUniform && _styleIsUniform && _strokeAlignIsUniform;
471
472 bool get _colorIsUniform {
473 final Color topColor = top.color;
474 return left.color == topColor && bottom.color == topColor && right.color == topColor;
475 }
476
477 bool get _widthIsUniform {
478 final double topWidth = top.width;
479 return left.width == topWidth && bottom.width == topWidth && right.width == topWidth;
480 }
481
482 bool get _styleIsUniform {
483 final BorderStyle topStyle = top.style;
484 return left.style == topStyle && bottom.style == topStyle && right.style == topStyle;
485 }
486
487 bool get _strokeAlignIsUniform {
488 final double topStrokeAlign = top.strokeAlign;
489 return left.strokeAlign == topStrokeAlign
490 && bottom.strokeAlign == topStrokeAlign
491 && right.strokeAlign == topStrokeAlign;
492 }
493
494 Set<Color> _distinctVisibleColors() {
495 final Set<Color> distinctVisibleColors = <Color>{};
496 if (top.style != BorderStyle.none) {
497 distinctVisibleColors.add(top.color);
498 }
499 if (right.style != BorderStyle.none) {
500 distinctVisibleColors.add(right.color);
501 }
502 if (bottom.style != BorderStyle.none) {
503 distinctVisibleColors.add(bottom.color);
504 }
505 if (left.style != BorderStyle.none) {
506 distinctVisibleColors.add(left.color);
507 }
508 return distinctVisibleColors;
509 }
510
511 // [BoxBorder.paintNonUniformBorder] is about 20% faster than [paintBorder],
512 // but [paintBorder] is able to draw hairline borders when width is zero
513 // and style is [BorderStyle.solid].
514 bool get _hasHairlineBorder =>
515 (top.style == BorderStyle.solid && top.width == 0.0) ||
516 (right.style == BorderStyle.solid && right.width == 0.0) ||
517 (bottom.style == BorderStyle.solid && bottom.width == 0.0) ||
518 (left.style == BorderStyle.solid && left.width == 0.0);
519
520 @override
521 Border? add(ShapeBorder other, { bool reversed = false }) {
522 if (other is Border &&
523 BorderSide.canMerge(top, other.top) &&
524 BorderSide.canMerge(right, other.right) &&
525 BorderSide.canMerge(bottom, other.bottom) &&
526 BorderSide.canMerge(left, other.left)) {
527 return Border.merge(this, other);
528 }
529 return null;
530 }
531
532 @override
533 Border scale(double t) {
534 return Border(
535 top: top.scale(t),
536 right: right.scale(t),
537 bottom: bottom.scale(t),
538 left: left.scale(t),
539 );
540 }
541
542 @override
543 ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
544 if (a is Border) {
545 return Border.lerp(a, this, t);
546 }
547 return super.lerpFrom(a, t);
548 }
549
550 @override
551 ShapeBorder? lerpTo(ShapeBorder? b, double t) {
552 if (b is Border) {
553 return Border.lerp(this, b, t);
554 }
555 return super.lerpTo(b, t);
556 }
557
558 /// Linearly interpolate between two borders.
559 ///
560 /// If a border is null, it is treated as having four [BorderSide.none]
561 /// borders.
562 ///
563 /// {@macro dart.ui.shadow.lerp}
564 static Border? lerp(Border? a, Border? b, double t) {
565 if (identical(a, b)) {
566 return a;
567 }
568 if (a == null) {
569 return b!.scale(t);
570 }
571 if (b == null) {
572 return a.scale(1.0 - t);
573 }
574 return Border(
575 top: BorderSide.lerp(a.top, b.top, t),
576 right: BorderSide.lerp(a.right, b.right, t),
577 bottom: BorderSide.lerp(a.bottom, b.bottom, t),
578 left: BorderSide.lerp(a.left, b.left, t),
579 );
580 }
581
582 /// Paints the border within the given [Rect] on the given [Canvas].
583 ///
584 /// Uniform borders and non-uniform borders with similar colors and styles
585 /// are more efficient to paint than more complex borders.
586 ///
587 /// You can provide a [BoxShape] to draw the border on. If the `shape` in
588 /// [BoxShape.circle], there is the requirement that the border has uniform
589 /// color and style.
590 ///
591 /// If you specify a rectangular box shape ([BoxShape.rectangle]), then you
592 /// may specify a [BorderRadius]. If a `borderRadius` is specified, there is
593 /// the requirement that the border has uniform color and style.
594 ///
595 /// The [getInnerPath] and [getOuterPath] methods do not know about the
596 /// `shape` and `borderRadius` arguments.
597 ///
598 /// The `textDirection` argument is not used by this paint method.
599 ///
600 /// See also:
601 ///
602 /// * [paintBorder], which is used if the border has non-uniform colors or styles and no borderRadius.
603 /// * <https://pub.dev/packages/non_uniform_border>, a package that implements
604 /// a Non-Uniform Border on ShapeBorder, which is used by Material Design
605 /// buttons and other widgets, under the "shape" field.
606 @override
607 void paint(
608 Canvas canvas,
609 Rect rect, {
610 TextDirection? textDirection,
611 BoxShape shape = BoxShape.rectangle,
612 BorderRadius? borderRadius,
613 }) {
614 if (isUniform) {
615 switch (top.style) {
616 case BorderStyle.none:
617 return;
618 case BorderStyle.solid:
619 switch (shape) {
620 case BoxShape.circle:
621 assert(borderRadius == null, 'A borderRadius cannot be given when shape is a BoxShape.circle.');
622 BoxBorder._paintUniformBorderWithCircle(canvas, rect, top);
623 case BoxShape.rectangle:
624 if (borderRadius != null && borderRadius != BorderRadius.zero) {
625 BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius);
626 return;
627 }
628 BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top);
629 }
630 return;
631 }
632 }
633
634 if (_styleIsUniform && top.style == BorderStyle.none) {
635 return;
636 }
637
638 // Allow painting non-uniform borders if the visible colors are uniform.
639 final Set<Color> visibleColors = _distinctVisibleColors();
640 final bool hasHairlineBorder = _hasHairlineBorder;
641 // Paint a non uniform border if a single color is visible
642 // and (borderRadius is present) or (border is visible and width != 0.0).
643 if (visibleColors.length == 1 &&
644 !hasHairlineBorder &&
645 (shape == BoxShape.circle ||
646 (borderRadius != null && borderRadius != BorderRadius.zero))) {
647 BoxBorder.paintNonUniformBorder(canvas, rect,
648 shape: shape,
649 borderRadius: borderRadius,
650 textDirection: textDirection,
651 top: top.style == BorderStyle.none ? BorderSide.none : top,
652 right: right.style == BorderStyle.none ? BorderSide.none : right,
653 bottom: bottom.style == BorderStyle.none ? BorderSide.none : bottom,
654 left: left.style == BorderStyle.none ? BorderSide.none : left,
655 color: visibleColors.first);
656 return;
657 }
658
659 assert(() {
660 if (hasHairlineBorder) {
661 assert(borderRadius == null || borderRadius == BorderRadius.zero,
662 'A hairline border like `BorderSide(width: 0.0, style: BorderStyle.solid)` can only be drawn when BorderRadius is zero or null.');
663 }
664 if (borderRadius != null && borderRadius != BorderRadius.zero) {
665 throw FlutterError.fromParts(<DiagnosticsNode>[
666 ErrorSummary('A borderRadius can only be given on borders with uniform colors.'),
667 ErrorDescription('The following is not uniform:'),
668 if (!_colorIsUniform) ErrorDescription('BorderSide.color'),
669 ]);
670 }
671 return true;
672 }());
673 assert(() {
674 if (shape != BoxShape.rectangle) {
675 throw FlutterError.fromParts(<DiagnosticsNode>[
676 ErrorSummary('A Border can only be drawn as a circle on borders with uniform colors.'),
677 ErrorDescription('The following is not uniform:'),
678 if (!_colorIsUniform) ErrorDescription('BorderSide.color'),
679 ]);
680 }
681 return true;
682 }());
683 assert(() {
684 if (!_strokeAlignIsUniform || top.strokeAlign != BorderSide.strokeAlignInside) {
685 throw FlutterError.fromParts(<DiagnosticsNode>[
686 ErrorSummary('A Border can only draw strokeAlign different than BorderSide.strokeAlignInside on borders with uniform colors.'),
687 ]);
688 }
689 return true;
690 }());
691
692 paintBorder(canvas, rect, top: top, right: right, bottom: bottom, left: left);
693 }
694
695 @override
696 bool operator ==(Object other) {
697 if (identical(this, other)) {
698 return true;
699 }
700 if (other.runtimeType != runtimeType) {
701 return false;
702 }
703 return other is Border
704 && other.top == top
705 && other.right == right
706 && other.bottom == bottom
707 && other.left == left;
708 }
709
710 @override
711 int get hashCode => Object.hash(top, right, bottom, left);
712
713 @override
714 String toString() {
715 if (isUniform) {
716 return '${objectRuntimeType(this, 'Border')}.all($top)';
717 }
718 final List<String> arguments = <String>[
719 if (top != BorderSide.none) 'top: $top',
720 if (right != BorderSide.none) 'right: $right',
721 if (bottom != BorderSide.none) 'bottom: $bottom',
722 if (left != BorderSide.none) 'left: $left',
723 ];
724 return '${objectRuntimeType(this, 'Border')}(${arguments.join(", ")})';
725 }
726}
727
728/// A border of a box, comprised of four sides, the lateral sides of which
729/// flip over based on the reading direction.
730///
731/// The lateral sides are called [start] and [end]. When painted in
732/// left-to-right environments, the [start] side will be painted on the left and
733/// the [end] side on the right; in right-to-left environments, it is the
734/// reverse. The other two sides are [top] and [bottom].
735///
736/// The sides are represented by [BorderSide] objects.
737///
738/// If the [start] and [end] sides are the same, then it is slightly more
739/// efficient to use a [Border] object rather than a [BorderDirectional] object.
740///
741/// See also:
742///
743/// * [BoxDecoration], which uses this class to describe its edge decoration.
744/// * [BorderSide], which is used to describe each side of the box.
745/// * [Theme], from the material layer, which can be queried to obtain appropriate colors
746/// to use for borders in a [MaterialApp], as shown in the "divider" sample above.
747/// * <https://pub.dev/packages/non_uniform_border>, a package that implements
748/// a Non-Uniform Border on ShapeBorder, which is used by Material Design
749/// buttons and other widgets, under the "shape" field.
750class BorderDirectional extends BoxBorder {
751 /// Creates a border.
752 ///
753 /// The [start] and [end] sides represent the horizontal sides; the start side
754 /// is on the leading edge given the reading direction, and the end side is on
755 /// the trailing edge. They are resolved during [paint].
756 ///
757 /// All the sides of the border default to [BorderSide.none].
758 const BorderDirectional({
759 this.top = BorderSide.none,
760 this.start = BorderSide.none,
761 this.end = BorderSide.none,
762 this.bottom = BorderSide.none,
763 });
764
765 /// Creates a [BorderDirectional] that represents the addition of the two
766 /// given [BorderDirectional]s.
767 ///
768 /// It is only valid to call this if [BorderSide.canMerge] returns true for
769 /// the pairwise combination of each side on both [BorderDirectional]s.
770 static BorderDirectional merge(BorderDirectional a, BorderDirectional b) {
771 assert(BorderSide.canMerge(a.top, b.top));
772 assert(BorderSide.canMerge(a.start, b.start));
773 assert(BorderSide.canMerge(a.end, b.end));
774 assert(BorderSide.canMerge(a.bottom, b.bottom));
775 return BorderDirectional(
776 top: BorderSide.merge(a.top, b.top),
777 start: BorderSide.merge(a.start, b.start),
778 end: BorderSide.merge(a.end, b.end),
779 bottom: BorderSide.merge(a.bottom, b.bottom),
780 );
781 }
782
783 @override
784 final BorderSide top;
785
786 /// The start side of this border.
787 ///
788 /// This is the side on the left in left-to-right text and on the right in
789 /// right-to-left text.
790 ///
791 /// See also:
792 ///
793 /// * [TextDirection], which is used to describe the reading direction.
794 final BorderSide start;
795
796 /// The end side of this border.
797 ///
798 /// This is the side on the right in left-to-right text and on the left in
799 /// right-to-left text.
800 ///
801 /// See also:
802 ///
803 /// * [TextDirection], which is used to describe the reading direction.
804 final BorderSide end;
805
806 @override
807 final BorderSide bottom;
808
809 @override
810 EdgeInsetsGeometry get dimensions {
811 if (isUniform) {
812 return EdgeInsetsDirectional.all(top.strokeInset);
813 }
814 return EdgeInsetsDirectional.fromSTEB(start.strokeInset, top.strokeInset, end.strokeInset, bottom.strokeInset);
815 }
816
817 @override
818 bool get isUniform => _colorIsUniform && _widthIsUniform && _styleIsUniform && _strokeAlignIsUniform;
819
820 bool get _colorIsUniform {
821 final Color topColor = top.color;
822 return start.color == topColor && bottom.color == topColor && end.color == topColor;
823 }
824
825 bool get _widthIsUniform {
826 final double topWidth = top.width;
827 return start.width == topWidth && bottom.width == topWidth && end.width == topWidth;
828 }
829
830 bool get _styleIsUniform {
831 final BorderStyle topStyle = top.style;
832 return start.style == topStyle && bottom.style == topStyle && end.style == topStyle;
833 }
834
835 bool get _strokeAlignIsUniform {
836 final double topStrokeAlign = top.strokeAlign;
837 return start.strokeAlign == topStrokeAlign
838 && bottom.strokeAlign == topStrokeAlign
839 && end.strokeAlign == topStrokeAlign;
840 }
841
842 Set<Color> _distinctVisibleColors() {
843 final Set<Color> distinctVisibleColors = <Color>{};
844 if (top.style != BorderStyle.none) {
845 distinctVisibleColors.add(top.color);
846 }
847 if (end.style != BorderStyle.none) {
848 distinctVisibleColors.add(end.color);
849 }
850 if (bottom.style != BorderStyle.none) {
851 distinctVisibleColors.add(bottom.color);
852 }
853 if (start.style != BorderStyle.none) {
854 distinctVisibleColors.add(start.color);
855 }
856
857 return distinctVisibleColors;
858 }
859
860
861 bool get _hasHairlineBorder =>
862 (top.style == BorderStyle.solid && top.width == 0.0) ||
863 (end.style == BorderStyle.solid && end.width == 0.0) ||
864 (bottom.style == BorderStyle.solid && bottom.width == 0.0) ||
865 (start.style == BorderStyle.solid && start.width == 0.0);
866
867 @override
868 BoxBorder? add(ShapeBorder other, { bool reversed = false }) {
869 if (other is BorderDirectional) {
870 final BorderDirectional typedOther = other;
871 if (BorderSide.canMerge(top, typedOther.top) &&
872 BorderSide.canMerge(start, typedOther.start) &&
873 BorderSide.canMerge(end, typedOther.end) &&
874 BorderSide.canMerge(bottom, typedOther.bottom)) {
875 return BorderDirectional.merge(this, typedOther);
876 }
877 return null;
878 }
879 if (other is Border) {
880 final Border typedOther = other;
881 if (!BorderSide.canMerge(typedOther.top, top) ||
882 !BorderSide.canMerge(typedOther.bottom, bottom)) {
883 return null;
884 }
885 if (start != BorderSide.none ||
886 end != BorderSide.none) {
887 if (typedOther.left != BorderSide.none ||
888 typedOther.right != BorderSide.none) {
889 return null;
890 }
891 assert(typedOther.left == BorderSide.none);
892 assert(typedOther.right == BorderSide.none);
893 return BorderDirectional(
894 top: BorderSide.merge(typedOther.top, top),
895 start: start,
896 end: end,
897 bottom: BorderSide.merge(typedOther.bottom, bottom),
898 );
899 }
900 assert(start == BorderSide.none);
901 assert(end == BorderSide.none);
902 return Border(
903 top: BorderSide.merge(typedOther.top, top),
904 right: typedOther.right,
905 bottom: BorderSide.merge(typedOther.bottom, bottom),
906 left: typedOther.left,
907 );
908 }
909 return null;
910 }
911
912 @override
913 BorderDirectional scale(double t) {
914 return BorderDirectional(
915 top: top.scale(t),
916 start: start.scale(t),
917 end: end.scale(t),
918 bottom: bottom.scale(t),
919 );
920 }
921
922 @override
923 ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
924 if (a is BorderDirectional) {
925 return BorderDirectional.lerp(a, this, t);
926 }
927 return super.lerpFrom(a, t);
928 }
929
930 @override
931 ShapeBorder? lerpTo(ShapeBorder? b, double t) {
932 if (b is BorderDirectional) {
933 return BorderDirectional.lerp(this, b, t);
934 }
935 return super.lerpTo(b, t);
936 }
937
938 /// Linearly interpolate between two borders.
939 ///
940 /// If a border is null, it is treated as having four [BorderSide.none]
941 /// borders.
942 ///
943 /// {@macro dart.ui.shadow.lerp}
944 static BorderDirectional? lerp(BorderDirectional? a, BorderDirectional? b, double t) {
945 if (identical(a, b)) {
946 return a;
947 }
948 if (a == null) {
949 return b!.scale(t);
950 }
951 if (b == null) {
952 return a.scale(1.0 - t);
953 }
954 return BorderDirectional(
955 top: BorderSide.lerp(a.top, b.top, t),
956 end: BorderSide.lerp(a.end, b.end, t),
957 bottom: BorderSide.lerp(a.bottom, b.bottom, t),
958 start: BorderSide.lerp(a.start, b.start, t),
959 );
960 }
961
962 /// Paints the border within the given [Rect] on the given [Canvas].
963 ///
964 /// Uniform borders are more efficient to paint than more complex borders.
965 ///
966 /// You can provide a [BoxShape] to draw the border on. If the `shape` in
967 /// [BoxShape.circle], there is the requirement that the border [isUniform].
968 ///
969 /// If you specify a rectangular box shape ([BoxShape.rectangle]), then you
970 /// may specify a [BorderRadius]. If a `borderRadius` is specified, there is
971 /// the requirement that the border [isUniform].
972 ///
973 /// The [getInnerPath] and [getOuterPath] methods do not know about the
974 /// `shape` and `borderRadius` arguments.
975 ///
976 /// The `textDirection` argument is used to determine which of [start] and
977 /// [end] map to the left and right. For [TextDirection.ltr], the [start] is
978 /// the left and the [end] is the right; for [TextDirection.rtl], it is the
979 /// reverse.
980 ///
981 /// See also:
982 ///
983 /// * [paintBorder], which is used if the border has non-uniform colors or styles and no borderRadius.
984 @override
985 void paint(
986 Canvas canvas,
987 Rect rect, {
988 TextDirection? textDirection,
989 BoxShape shape = BoxShape.rectangle,
990 BorderRadius? borderRadius,
991 }) {
992 if (isUniform) {
993 switch (top.style) {
994 case BorderStyle.none:
995 return;
996 case BorderStyle.solid:
997 switch (shape) {
998 case BoxShape.circle:
999 assert(borderRadius == null, 'A borderRadius cannot be given when shape is a BoxShape.circle.');
1000 BoxBorder._paintUniformBorderWithCircle(canvas, rect, top);
1001 case BoxShape.rectangle:
1002 if (borderRadius != null && borderRadius != BorderRadius.zero) {
1003 BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius);
1004 return;
1005 }
1006 BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top);
1007 }
1008 return;
1009 }
1010 }
1011
1012 if (_styleIsUniform && top.style == BorderStyle.none) {
1013 return;
1014 }
1015
1016 final BorderSide left, right;
1017 assert(textDirection != null, 'Non-uniform BorderDirectional objects require a TextDirection when painting.');
1018 switch (textDirection!) {
1019 case TextDirection.rtl:
1020 left = end;
1021 right = start;
1022 case TextDirection.ltr:
1023 left = start;
1024 right = end;
1025 }
1026
1027 // Allow painting non-uniform borders if the visible colors are uniform.
1028 final Set<Color> visibleColors = _distinctVisibleColors();
1029 final bool hasHairlineBorder = _hasHairlineBorder;
1030 if (visibleColors.length == 1 &&
1031 !hasHairlineBorder &&
1032 (shape == BoxShape.circle ||
1033 (borderRadius != null && borderRadius != BorderRadius.zero))) {
1034 BoxBorder.paintNonUniformBorder(canvas, rect,
1035 shape: shape,
1036 borderRadius: borderRadius,
1037 textDirection: textDirection,
1038 top: top.style == BorderStyle.none ? BorderSide.none : top,
1039 right: right.style == BorderStyle.none ? BorderSide.none : right,
1040 bottom: bottom.style == BorderStyle.none ? BorderSide.none : bottom,
1041 left: left.style == BorderStyle.none ? BorderSide.none : left,
1042 color: visibleColors.first);
1043 return;
1044 }
1045
1046 if (hasHairlineBorder) {
1047 assert(borderRadius == null || borderRadius == BorderRadius.zero, 'A side like `BorderSide(width: 0.0, style: BorderStyle.solid)` can only be drawn when BorderRadius is zero or null.');
1048 }
1049 assert(borderRadius == null, 'A borderRadius can only be given for borders with uniform colors.');
1050 assert(shape == BoxShape.rectangle, 'A Border can only be drawn as a circle on borders with uniform colors.');
1051 assert(_strokeAlignIsUniform && top.strokeAlign == BorderSide.strokeAlignInside, 'A Border can only draw strokeAlign different than strokeAlignInside on borders with uniform colors.');
1052
1053 paintBorder(canvas, rect, top: top, left: left, bottom: bottom, right: right);
1054 }
1055
1056 @override
1057 bool operator ==(Object other) {
1058 if (identical(this, other)) {
1059 return true;
1060 }
1061 if (other.runtimeType != runtimeType) {
1062 return false;
1063 }
1064 return other is BorderDirectional
1065 && other.top == top
1066 && other.start == start
1067 && other.end == end
1068 && other.bottom == bottom;
1069 }
1070
1071 @override
1072 int get hashCode => Object.hash(top, start, end, bottom);
1073
1074 @override
1075 String toString() {
1076 final List<String> arguments = <String>[
1077 if (top != BorderSide.none) 'top: $top',
1078 if (start != BorderSide.none) 'start: $start',
1079 if (end != BorderSide.none) 'end: $end',
1080 if (bottom != BorderSide.none) 'bottom: $bottom',
1081 ];
1082 return '${objectRuntimeType(this, 'BorderDirectional')}(${arguments.join(", ")})';
1083 }
1084}
1085