| 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'; |
| 8 | library; |
| 9 | |
| 10 | import 'package:flutter/foundation.dart'; |
| 11 | |
| 12 | import 'basic_types.dart'; |
| 13 | import 'border_radius.dart'; |
| 14 | import 'borders.dart'; |
| 15 | import '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. |
| 26 | enum 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. |
| 67 | abstract 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 circle cannot have a border radius. Remove either the shape or the borderRadius argument.' , |
| 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. |
| 471 | class 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 circle cannot have a border radius. Remove either the shape or the borderRadius argument.' , |
| 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. |
| 844 | class 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 circle cannot have a border radius. Remove either the shape or the borderRadius argument.' , |
| 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 | |