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

Provided by KDAB

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