1// Copyright 2015 The Chromium 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 'dart:math' as math; 6import 'dart:ui' as ui show lerpDouble; 7 8import 'package:flutter/foundation.dart'; 9 10import 'basic_types.dart'; 11import 'edge_insets.dart'; 12 13/// The style of line to draw for a [BorderSide] in a [Border]. 14enum BorderStyle { 15 /// Skip the border. 16 none, 17 18 /// Draw the border as a solid line. 19 solid, 20 21 // if you add more, think about how they will lerp 22} 23 24/// A side of a border of a box. 25/// 26/// A [Border] consists of four [BorderSide] objects: [Border.top], 27/// [Border.left], [Border.right], and [Border.bottom]. 28/// 29/// Note that setting [BorderSide.width] to 0.0 will result in hairline 30/// rendering. A more involved explanation is present in [BorderSide.width]. 31/// 32/// {@tool sample} 33/// 34/// This sample shows how [BorderSide] objects can be used in a [Container], via 35/// a [BoxDecoration] and a [Border], to decorate some [Text]. In this example, 36/// the text has a thick bar above it that is light blue, and a thick bar below 37/// it that is a darker shade of blue. 38/// 39/// ```dart 40/// Container( 41/// padding: EdgeInsets.all(8.0), 42/// decoration: BoxDecoration( 43/// border: Border( 44/// top: BorderSide(width: 16.0, color: Colors.lightBlue.shade50), 45/// bottom: BorderSide(width: 16.0, color: Colors.lightBlue.shade900), 46/// ), 47/// ), 48/// child: Text('Flutter in the sky', textAlign: TextAlign.center), 49/// ) 50/// ``` 51/// {@end-tool} 52/// 53/// See also: 54/// 55/// * [Border], which uses [BorderSide] objects to represent its sides. 56/// * [BoxDecoration], which optionally takes a [Border] object. 57/// * [TableBorder], which is similar to [Border] but has two more sides 58/// ([TableBorder.horizontalInside] and [TableBorder.verticalInside]), both 59/// of which are also [BorderSide] objects. 60@immutable 61class BorderSide { 62 /// Creates the side of a border. 63 /// 64 /// By default, the border is 1.0 logical pixels wide and solid black. 65 const BorderSide({ 66 this.color = const Color(0xFF000000), 67 this.width = 1.0, 68 this.style = BorderStyle.solid, 69 }) : assert(color != null), 70 assert(width != null), 71 assert(width >= 0.0), 72 assert(style != null); 73 74 /// Creates a [BorderSide] that represents the addition of the two given 75 /// [BorderSide]s. 76 /// 77 /// It is only valid to call this if [canMerge] returns true for the two 78 /// sides. 79 /// 80 /// If one of the sides is zero-width with [BorderStyle.none], then the other 81 /// side is return as-is. If both of the sides are zero-width with 82 /// [BorderStyle.none], then [BorderSide.zero] is returned. 83 /// 84 /// The arguments must not be null. 85 static BorderSide merge(BorderSide a, BorderSide b) { 86 assert(a != null); 87 assert(b != null); 88 assert(canMerge(a, b)); 89 final bool aIsNone = a.style == BorderStyle.none && a.width == 0.0; 90 final bool bIsNone = b.style == BorderStyle.none && b.width == 0.0; 91 if (aIsNone && bIsNone) 92 return BorderSide.none; 93 if (aIsNone) 94 return b; 95 if (bIsNone) 96 return a; 97 assert(a.color == b.color); 98 assert(a.style == b.style); 99 return BorderSide( 100 color: a.color, // == b.color 101 width: a.width + b.width, 102 style: a.style, // == b.style 103 ); 104 } 105 106 /// The color of this side of the border. 107 final Color color; 108 109 /// The width of this side of the border, in logical pixels. 110 /// 111 /// Setting width to 0.0 will result in a hairline border. This means that 112 /// the border will have the width of one physical pixel. Also, hairline 113 /// rendering takes shortcuts when the path overlaps a pixel more than once. 114 /// This means that it will render faster than otherwise, but it might 115 /// double-hit pixels, giving it a slightly darker/lighter result. 116 /// 117 /// To omit the border entirely, set the [style] to [BorderStyle.none]. 118 final double width; 119 120 /// The style of this side of the border. 121 /// 122 /// To omit a side, set [style] to [BorderStyle.none]. This skips 123 /// painting the border, but the border still has a [width]. 124 final BorderStyle style; 125 126 /// A hairline black border that is not rendered. 127 static const BorderSide none = BorderSide(width: 0.0, style: BorderStyle.none); 128 129 /// Creates a copy of this border but with the given fields replaced with the new values. 130 BorderSide copyWith({ 131 Color color, 132 double width, 133 BorderStyle style, 134 }) { 135 assert(width == null || width >= 0.0); 136 return BorderSide( 137 color: color ?? this.color, 138 width: width ?? this.width, 139 style: style ?? this.style, 140 ); 141 } 142 143 /// Creates a copy of this border side description but with the width scaled 144 /// by the factor `t`. 145 /// 146 /// The `t` argument represents the multiplicand, or the position on the 147 /// timeline for an interpolation from nothing to `this`, with 0.0 meaning 148 /// that the object returned should be the nil variant of this object, 1.0 149 /// meaning that no change should be applied, returning `this` (or something 150 /// equivalent to `this`), and other values meaning that the object should be 151 /// multiplied by `t`. Negative values are treated like zero. 152 /// 153 /// Since a zero width is normally painted as a hairline width rather than no 154 /// border at all, the zero factor is special-cased to instead change the 155 /// style to [BorderStyle.none]. 156 /// 157 /// Values for `t` are usually obtained from an [Animation<double>], such as 158 /// an [AnimationController]. 159 BorderSide scale(double t) { 160 assert(t != null); 161 return BorderSide( 162 color: color, 163 width: math.max(0.0, width * t), 164 style: t <= 0.0 ? BorderStyle.none : style, 165 ); 166 } 167 168 /// Create a [Paint] object that, if used to stroke a line, will draw the line 169 /// in this border's style. 170 /// 171 /// Not all borders use this method to paint their border sides. For example, 172 /// non-uniform rectangular [Border]s have beveled edges and so paint their 173 /// border sides as filled shapes rather than using a stroke. 174 Paint toPaint() { 175 switch (style) { 176 case BorderStyle.solid: 177 return Paint() 178 ..color = color 179 ..strokeWidth = width 180 ..style = PaintingStyle.stroke; 181 case BorderStyle.none: 182 return Paint() 183 ..color = const Color(0x00000000) 184 ..strokeWidth = 0.0 185 ..style = PaintingStyle.stroke; 186 } 187 return null; 188 } 189 190 /// Whether the two given [BorderSide]s can be merged using [new 191 /// BorderSide.merge]. 192 /// 193 /// Two sides can be merged if one or both are zero-width with 194 /// [BorderStyle.none], or if they both have the same color and style. 195 /// 196 /// The arguments must not be null. 197 static bool canMerge(BorderSide a, BorderSide b) { 198 assert(a != null); 199 assert(b != null); 200 if ((a.style == BorderStyle.none && a.width == 0.0) || 201 (b.style == BorderStyle.none && b.width == 0.0)) 202 return true; 203 return a.style == b.style 204 && a.color == b.color; 205 } 206 207 /// Linearly interpolate between two border sides. 208 /// 209 /// The arguments must not be null. 210 /// 211 /// {@macro dart.ui.shadow.lerp} 212 static BorderSide lerp(BorderSide a, BorderSide b, double t) { 213 assert(a != null); 214 assert(b != null); 215 assert(t != null); 216 if (t == 0.0) 217 return a; 218 if (t == 1.0) 219 return b; 220 final double width = ui.lerpDouble(a.width, b.width, t); 221 if (width < 0.0) 222 return BorderSide.none; 223 if (a.style == b.style) { 224 return BorderSide( 225 color: Color.lerp(a.color, b.color, t), 226 width: width, 227 style: a.style, // == b.style 228 ); 229 } 230 Color colorA, colorB; 231 switch (a.style) { 232 case BorderStyle.solid: 233 colorA = a.color; 234 break; 235 case BorderStyle.none: 236 colorA = a.color.withAlpha(0x00); 237 break; 238 } 239 switch (b.style) { 240 case BorderStyle.solid: 241 colorB = b.color; 242 break; 243 case BorderStyle.none: 244 colorB = b.color.withAlpha(0x00); 245 break; 246 } 247 return BorderSide( 248 color: Color.lerp(colorA, colorB, t), 249 width: width, 250 style: BorderStyle.solid, 251 ); 252 } 253 254 @override 255 bool operator ==(dynamic other) { 256 if (identical(this, other)) 257 return true; 258 if (runtimeType != other.runtimeType) 259 return false; 260 final BorderSide typedOther = other; 261 return color == typedOther.color && 262 width == typedOther.width && 263 style == typedOther.style; 264 } 265 266 @override 267 int get hashCode => hashValues(color, width, style); 268 269 @override 270 String toString() => '$runtimeType($color, ${width.toStringAsFixed(1)}, $style)'; 271} 272 273/// Base class for shape outlines. 274/// 275/// This class handles how to add multiple borders together. Subclasses define 276/// various shapes, like circles ([CircleBorder]), rounded rectangles 277/// ([RoundedRectangleBorder]), continuous rectangles 278/// ([ContinuousRectangleBorder]), or beveled rectangles 279/// ([BeveledRectangleBorder]). 280/// 281/// See also: 282/// 283/// * [ShapeDecoration], which can be used with [DecoratedBox] to show a shape. 284/// * [Material] (and many other widgets in the Material library), which takes 285/// a [ShapeBorder] to define its shape. 286/// * [NotchedShape], which describes a shape with a hole in it. 287@immutable 288abstract class ShapeBorder { 289 /// Abstract const constructor. This constructor enables subclasses to provide 290 /// const constructors so that they can be used in const expressions. 291 const ShapeBorder(); 292 293 /// The widths of the sides of this border represented as an [EdgeInsets]. 294 /// 295 /// Specifically, this is the amount by which a rectangle should be inset so 296 /// as to avoid painting over any important part of the border. It is the 297 /// amount by which additional borders will be inset before they are drawn. 298 /// 299 /// This can be used, for example, with a [Padding] widget to inset a box by 300 /// the size of these borders. 301 /// 302 /// Shapes that have a fixed ratio regardless of the area on which they are 303 /// painted, or that change their rendering based on the size they are given 304 /// when painting (for instance [CircleBorder]), will not return valid 305 /// [dimensions] information because they cannot know their eventual size when 306 /// computing their [dimensions]. 307 EdgeInsetsGeometry get dimensions; 308 309 /// Attempts to create a new object that represents the amalgamation of `this` 310 /// border and the `other` border. 311 /// 312 /// If the type of the other border isn't known, or the given instance cannot 313 /// be reasonably added to this instance, then this should return null. 314 /// 315 /// This method is used by the [operator +] implementation. 316 /// 317 /// The `reversed` argument is true if this object was the right operand of 318 /// the `+` operator, and false if it was the left operand. 319 @protected 320 ShapeBorder add(ShapeBorder other, { bool reversed = false }) => null; 321 322 /// Creates a new border consisting of the two borders on either side of the 323 /// operator. 324 /// 325 /// If the borders belong to classes that know how to add themselves, then 326 /// this results in a new border that represents the intelligent addition of 327 /// those two borders (see [add]). Otherwise, an object is returned that 328 /// merely paints the two borders sequentially, with the left hand operand on 329 /// the inside and the right hand operand on the outside. 330 ShapeBorder operator +(ShapeBorder other) { 331 return add(other) ?? other.add(this, reversed: true) ?? _CompoundBorder(<ShapeBorder>[other, this]); 332 } 333 334 /// Creates a copy of this border, scaled by the factor `t`. 335 /// 336 /// Typically this means scaling the width of the border's side, but it can 337 /// also include scaling other artifacts of the border, e.g. the border radius 338 /// of a [RoundedRectangleBorder]. 339 /// 340 /// The `t` argument represents the multiplicand, or the position on the 341 /// timeline for an interpolation from nothing to `this`, with 0.0 meaning 342 /// that the object returned should be the nil variant of this object, 1.0 343 /// meaning that no change should be applied, returning `this` (or something 344 /// equivalent to `this`), and other values meaning that the object should be 345 /// multiplied by `t`. Negative values are allowed but may be meaningless 346 /// (they correspond to extrapolating the interpolation from this object to 347 /// nothing, and going beyond nothing) 348 /// 349 /// Values for `t` are usually obtained from an [Animation<double>], such as 350 /// an [AnimationController]. 351 /// 352 /// See also: 353 /// 354 /// * [BorderSide.scale], which most [ShapeBorder] subclasses defer to for 355 /// the actual computation. 356 ShapeBorder scale(double t); 357 358 /// Linearly interpolates from another [ShapeBorder] (possibly of another 359 /// class) to `this`. 360 /// 361 /// When implementing this method in subclasses, return null if this class 362 /// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo] 363 /// method instead. If `a` is null, this must not return null. 364 /// 365 /// The base class implementation handles the case of `a` being null by 366 /// deferring to [scale]. 367 /// 368 /// The `t` argument represents position on the timeline, with 0.0 meaning 369 /// that the interpolation has not started, returning `a` (or something 370 /// equivalent to `a`), 1.0 meaning that the interpolation has finished, 371 /// returning `this` (or something equivalent to `this`), and values in 372 /// between meaning that the interpolation is at the relevant point on the 373 /// timeline between `a` and `this`. The interpolation can be extrapolated 374 /// beyond 0.0 and 1.0, so negative values and values greater than 1.0 are 375 /// valid (and can easily be generated by curves such as 376 /// [Curves.elasticInOut]). 377 /// 378 /// Values for `t` are usually obtained from an [Animation<double>], such as 379 /// an [AnimationController]. 380 /// 381 /// Instead of calling this directly, use [ShapeBorder.lerp]. 382 @protected 383 ShapeBorder lerpFrom(ShapeBorder a, double t) { 384 if (a == null) 385 return scale(t); 386 return null; 387 } 388 389 /// Linearly interpolates from `this` to another [ShapeBorder] (possibly of 390 /// another class). 391 /// 392 /// This is called if `b`'s [lerpTo] did not know how to handle this class. 393 /// 394 /// When implementing this method in subclasses, return null if this class 395 /// cannot interpolate from `b`. In that case, [lerp] will apply a default 396 /// behavior instead. If `b` is null, this must not return null. 397 /// 398 /// The base class implementation handles the case of `b` being null by 399 /// deferring to [scale]. 400 /// 401 /// The `t` argument represents position on the timeline, with 0.0 meaning 402 /// that the interpolation has not started, returning `this` (or something 403 /// equivalent to `this`), 1.0 meaning that the interpolation has finished, 404 /// returning `b` (or something equivalent to `b`), and values in between 405 /// meaning that the interpolation is at the relevant point on the timeline 406 /// between `this` and `b`. The interpolation can be extrapolated beyond 0.0 407 /// and 1.0, so negative values and values greater than 1.0 are valid (and can 408 /// easily be generated by curves such as [Curves.elasticInOut]). 409 /// 410 /// Values for `t` are usually obtained from an [Animation<double>], such as 411 /// an [AnimationController]. 412 /// 413 /// Instead of calling this directly, use [ShapeBorder.lerp]. 414 @protected 415 ShapeBorder lerpTo(ShapeBorder b, double t) { 416 if (b == null) 417 return scale(1.0 - t); 418 return null; 419 } 420 421 /// Linearly interpolates between two [ShapeBorder]s. 422 /// 423 /// This defers to `b`'s [lerpTo] function if `b` is not null. If `b` is 424 /// null or if its [lerpTo] returns null, it uses `a`'s [lerpFrom] 425 /// function instead. If both return null, it returns `a` before `t=0.5` 426 /// and `b` after `t=0.5`. 427 /// 428 /// {@macro dart.ui.shadow.lerp} 429 static ShapeBorder lerp(ShapeBorder a, ShapeBorder b, double t) { 430 assert(t != null); 431 ShapeBorder result; 432 if (b != null) 433 result = b.lerpFrom(a, t); 434 if (result == null && a != null) 435 result = a.lerpTo(b, t); 436 return result ?? (t < 0.5 ? a : b); 437 } 438 439 /// Create a [Path] that describes the outer edge of the border. 440 /// 441 /// This path must not cross the path given by [getInnerPath] for the same 442 /// [Rect]. 443 /// 444 /// To obtain a [Path] that describes the area of the border itself, set the 445 /// [Path.fillType] of the returned object to [PathFillType.evenOdd], and add 446 /// to this object the path returned from [getInnerPath] (using 447 /// [Path.addPath]). 448 /// 449 /// The `textDirection` argument must be provided non-null if the border 450 /// has a text direction dependency (for example if it is expressed in terms 451 /// of "start" and "end" instead of "left" and "right"). It may be null if 452 /// the border will not need the text direction to paint itself. 453 /// 454 /// See also: 455 /// 456 /// * [getInnerPath], which creates the path for the inner edge. 457 /// * [Path.contains], which can tell if an [Offset] is within a [Path]. 458 Path getOuterPath(Rect rect, { TextDirection textDirection }); 459 460 /// Create a [Path] that describes the inner edge of the border. 461 /// 462 /// This path must not cross the path given by [getOuterPath] for the same 463 /// [Rect]. 464 /// 465 /// To obtain a [Path] that describes the area of the border itself, set the 466 /// [Path.fillType] of the returned object to [PathFillType.evenOdd], and add 467 /// to this object the path returned from [getOuterPath] (using 468 /// [Path.addPath]). 469 /// 470 /// The `textDirection` argument must be provided and non-null if the border 471 /// has a text direction dependency (for example if it is expressed in terms 472 /// of "start" and "end" instead of "left" and "right"). It may be null if 473 /// the border will not need the text direction to paint itself. 474 /// 475 /// See also: 476 /// 477 /// * [getOuterPath], which creates the path for the outer edge. 478 /// * [Path.contains], which can tell if an [Offset] is within a [Path]. 479 Path getInnerPath(Rect rect, { TextDirection textDirection }); 480 481 /// Paints the border within the given [Rect] on the given [Canvas]. 482 /// 483 /// The `textDirection` argument must be provided and non-null if the border 484 /// has a text direction dependency (for example if it is expressed in terms 485 /// of "start" and "end" instead of "left" and "right"). It may be null if 486 /// the border will not need the text direction to paint itself. 487 void paint(Canvas canvas, Rect rect, { TextDirection textDirection }); 488 489 @override 490 String toString() { 491 return '$runtimeType()'; 492 } 493} 494 495/// Represents the addition of two otherwise-incompatible borders. 496/// 497/// The borders are listed from the outside to the inside. 498class _CompoundBorder extends ShapeBorder { 499 _CompoundBorder(this.borders) 500 : assert(borders != null), 501 assert(borders.length >= 2), 502 assert(!borders.any((ShapeBorder border) => border is _CompoundBorder)); 503 504 final List<ShapeBorder> borders; 505 506 @override 507 EdgeInsetsGeometry get dimensions { 508 return borders.fold<EdgeInsetsGeometry>( 509 EdgeInsets.zero, 510 (EdgeInsetsGeometry previousValue, ShapeBorder border) { 511 return previousValue.add(border.dimensions); 512 }, 513 ); 514 } 515 516 @override 517 ShapeBorder add(ShapeBorder other, { bool reversed = false }) { 518 // This wraps the list of borders with "other", or, if "reversed" is true, 519 // wraps "other" with the list of borders. 520 // If "reversed" is false, "other" should end up being at the start of the 521 // list, otherwise, if "reversed" is true, it should end up at the end. 522 // First, see if we can merge the new adjacent borders. 523 if (other is! _CompoundBorder) { 524 // Here, "ours" is the border at the side where we're adding the new 525 // border, and "merged" is the result of attempting to merge it with the 526 // new border. If it's null, it couldn't be merged. 527 final ShapeBorder ours = reversed ? borders.last : borders.first; 528 final ShapeBorder merged = ours.add(other, reversed: reversed) 529 ?? other.add(ours, reversed: !reversed); 530 if (merged != null) { 531 final List<ShapeBorder> result = <ShapeBorder>[...borders]; 532 result[reversed ? result.length - 1 : 0] = merged; 533 return _CompoundBorder(result); 534 } 535 } 536 // We can't, so fall back to just adding the new border to the list. 537 final List<ShapeBorder> mergedBorders = <ShapeBorder>[ 538 if (reversed) ...borders, 539 if (other is _CompoundBorder) ...other.borders 540 else other, 541 if (!reversed) ...borders, 542 ]; 543 return _CompoundBorder(mergedBorders); 544 } 545 546 @override 547 ShapeBorder scale(double t) { 548 return _CompoundBorder( 549 borders.map<ShapeBorder>((ShapeBorder border) => border.scale(t)).toList() 550 ); 551 } 552 553 @override 554 ShapeBorder lerpFrom(ShapeBorder a, double t) { 555 return _CompoundBorder.lerp(a, this, t); 556 } 557 558 @override 559 ShapeBorder lerpTo(ShapeBorder b, double t) { 560 return _CompoundBorder.lerp(this, b, t); 561 } 562 563 static _CompoundBorder lerp(ShapeBorder a, ShapeBorder b, double t) { 564 assert(t != null); 565 assert(a is _CompoundBorder || b is _CompoundBorder); // Not really necessary, but all call sites currently intend this. 566 final List<ShapeBorder> aList = a is _CompoundBorder ? a.borders : <ShapeBorder>[a]; 567 final List<ShapeBorder> bList = b is _CompoundBorder ? b.borders : <ShapeBorder>[b]; 568 final List<ShapeBorder> results = <ShapeBorder>[]; 569 final int length = math.max(aList.length, bList.length); 570 for (int index = 0; index < length; index += 1) { 571 final ShapeBorder localA = index < aList.length ? aList[index] : null; 572 final ShapeBorder localB = index < bList.length ? bList[index] : null; 573 if (localA != null && localB != null) { 574 final ShapeBorder localResult = localA.lerpTo(localB, t) ?? localB.lerpFrom(localA, t); 575 if (localResult != null) { 576 results.add(localResult); 577 continue; 578 } 579 } 580 // If we're changing from one shape to another, make sure the shape that is coming in 581 // is inserted before the shape that is going away, so that the outer path changes to 582 // the new border earlier rather than later. (This affects, among other things, where 583 // the ShapeDecoration class puts its background.) 584 if (localB != null) 585 results.add(localB.scale(t)); 586 if (localA != null) 587 results.add(localA.scale(1.0 - t)); 588 } 589 return _CompoundBorder(results); 590 } 591 592 @override 593 Path getInnerPath(Rect rect, { TextDirection textDirection }) { 594 for (int index = 0; index < borders.length - 1; index += 1) 595 rect = borders[index].dimensions.resolve(textDirection).deflateRect(rect); 596 return borders.last.getInnerPath(rect, textDirection: textDirection); 597 } 598 599 @override 600 Path getOuterPath(Rect rect, { TextDirection textDirection }) { 601 return borders.first.getOuterPath(rect, textDirection: textDirection); 602 } 603 604 @override 605 void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { 606 for (ShapeBorder border in borders) { 607 border.paint(canvas, rect, textDirection: textDirection); 608 rect = border.dimensions.resolve(textDirection).deflateRect(rect); 609 } 610 } 611 612 @override 613 bool operator ==(dynamic other) { 614 if (identical(this, other)) 615 return true; 616 if (runtimeType != other.runtimeType) 617 return false; 618 final _CompoundBorder typedOther = other; 619 if (borders == typedOther.borders) 620 return true; 621 if (borders.length != typedOther.borders.length) 622 return false; 623 for (int index = 0; index < borders.length; index += 1) { 624 if (borders[index] != typedOther.borders[index]) 625 return false; 626 } 627 return true; 628 } 629 630 @override 631 int get hashCode => hashList(borders); 632 633 @override 634 String toString() { 635 // We list them in reverse order because when adding two borders they end up 636 // in the list in the opposite order of what the source looks like: a + b => 637 // [b, a]. We do this to make the painting code more optimal, and most of 638 // the rest of the code doesn't care, except toString() (for debugging). 639 return borders.reversed.map<String>((ShapeBorder border) => border.toString()).join(' + '); 640 } 641} 642 643/// Paints a border around the given rectangle on the canvas. 644/// 645/// The four sides can be independently specified. They are painted in the order 646/// top, right, bottom, left. This is only notable if the widths of the borders 647/// and the size of the given rectangle are such that the border sides will 648/// overlap each other. No effort is made to optimize the rendering of uniform 649/// borders (where all the borders have the same configuration); to render a 650/// uniform border, consider using [Canvas.drawRect] directly. 651/// 652/// The arguments must not be null. 653/// 654/// See also: 655/// 656/// * [paintImage], which paints an image in a rectangle on a canvas. 657/// * [Border], which uses this function to paint its border when the border is 658/// not uniform. 659/// * [BoxDecoration], which describes its border using the [Border] class. 660void paintBorder( 661 Canvas canvas, 662 Rect rect, { 663 BorderSide top = BorderSide.none, 664 BorderSide right = BorderSide.none, 665 BorderSide bottom = BorderSide.none, 666 BorderSide left = BorderSide.none, 667}) { 668 assert(canvas != null); 669 assert(rect != null); 670 assert(top != null); 671 assert(right != null); 672 assert(bottom != null); 673 assert(left != null); 674 675 // We draw the borders as filled shapes, unless the borders are hairline 676 // borders, in which case we use PaintingStyle.stroke, with the stroke width 677 // specified here. 678 final Paint paint = Paint() 679 ..strokeWidth = 0.0; 680 681 final Path path = Path(); 682 683 switch (top.style) { 684 case BorderStyle.solid: 685 paint.color = top.color; 686 path.reset(); 687 path.moveTo(rect.left, rect.top); 688 path.lineTo(rect.right, rect.top); 689 if (top.width == 0.0) { 690 paint.style = PaintingStyle.stroke; 691 } else { 692 paint.style = PaintingStyle.fill; 693 path.lineTo(rect.right - right.width, rect.top + top.width); 694 path.lineTo(rect.left + left.width, rect.top + top.width); 695 } 696 canvas.drawPath(path, paint); 697 break; 698 case BorderStyle.none: 699 break; 700 } 701 702 switch (right.style) { 703 case BorderStyle.solid: 704 paint.color = right.color; 705 path.reset(); 706 path.moveTo(rect.right, rect.top); 707 path.lineTo(rect.right, rect.bottom); 708 if (right.width == 0.0) { 709 paint.style = PaintingStyle.stroke; 710 } else { 711 paint.style = PaintingStyle.fill; 712 path.lineTo(rect.right - right.width, rect.bottom - bottom.width); 713 path.lineTo(rect.right - right.width, rect.top + top.width); 714 } 715 canvas.drawPath(path, paint); 716 break; 717 case BorderStyle.none: 718 break; 719 } 720 721 switch (bottom.style) { 722 case BorderStyle.solid: 723 paint.color = bottom.color; 724 path.reset(); 725 path.moveTo(rect.right, rect.bottom); 726 path.lineTo(rect.left, rect.bottom); 727 if (bottom.width == 0.0) { 728 paint.style = PaintingStyle.stroke; 729 } else { 730 paint.style = PaintingStyle.fill; 731 path.lineTo(rect.left + left.width, rect.bottom - bottom.width); 732 path.lineTo(rect.right - right.width, rect.bottom - bottom.width); 733 } 734 canvas.drawPath(path, paint); 735 break; 736 case BorderStyle.none: 737 break; 738 } 739 740 switch (left.style) { 741 case BorderStyle.solid: 742 paint.color = left.color; 743 path.reset(); 744 path.moveTo(rect.left, rect.bottom); 745 path.lineTo(rect.left, rect.top); 746 if (left.width == 0.0) { 747 paint.style = PaintingStyle.stroke; 748 } else { 749 paint.style = PaintingStyle.fill; 750 path.lineTo(rect.left + left.width, rect.top + top.width); 751 path.lineTo(rect.left + left.width, rect.bottom - bottom.width); 752 } 753 canvas.drawPath(path, paint); 754 break; 755 case BorderStyle.none: 756 break; 757 } 758} 759