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 | import 'package:flutter/foundation.dart'; |
6 | |
7 | import 'basic_types.dart'; |
8 | import 'border_radius.dart'; |
9 | import 'borders.dart'; |
10 | import '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. |
21 | enum 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. |
60 | abstract 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. |
386 | class 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. |
750 | class 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 | |