From 440e0e2c4eb0348dafbcd72ab9ff3a0328f132fd Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 19 May 2022 13:18:10 -0300 Subject: [PATCH] Add StrokeAlign to Border (#102112) --- .../painting/beveled_rectangle_border.dart | 40 ++++++- .../flutter/lib/src/painting/borders.dart | 56 ++++++++- .../flutter/lib/src/painting/box_border.dart | 106 ++++++++++++++++-- .../lib/src/painting/circle_border.dart | 38 ++++++- .../painting/rounded_rectangle_border.dart | 78 +++++++++++-- .../lib/src/painting/stadium_border.dart | 93 ++++++++++++--- .../beveled_rectangle_border_test.dart | 25 +++++ .../test/painting/border_side_test.dart | 14 +++ .../flutter/test/painting/border_test.dart | 62 ++++++++++ .../rounded_rectangle_border_test.dart | 20 ++++ .../test/painting/stadium_border_test.dart | 27 +++++ 11 files changed, 512 insertions(+), 47 deletions(-) diff --git a/packages/flutter/lib/src/painting/beveled_rectangle_border.dart b/packages/flutter/lib/src/painting/beveled_rectangle_border.dart index e235a002adc3..de178e92975c 100644 --- a/packages/flutter/lib/src/painting/beveled_rectangle_border.dart +++ b/packages/flutter/lib/src/painting/beveled_rectangle_border.dart @@ -42,7 +42,14 @@ class BeveledRectangleBorder extends OutlinedBorder { @override EdgeInsetsGeometry get dimensions { - return EdgeInsets.all(side.width); + switch (side.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsets.all(side.width); + case StrokeAlign.center: + return EdgeInsets.all(side.width / 2); + case StrokeAlign.outside: + return EdgeInsets.zero; + } } @override @@ -118,7 +125,21 @@ class BeveledRectangleBorder extends OutlinedBorder { @override Path getInnerPath(Rect rect, { TextDirection? textDirection }) { - return _getPath(borderRadius.resolve(textDirection).toRRect(rect).deflate(side.width)); + final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(side.width); + break; + case StrokeAlign.center: + adjustedRect = borderRect.deflate(side.width / 2); + break; + case StrokeAlign.outside: + adjustedRect = borderRect; + break; + } + + return _getPath(adjustedRect); } @override @@ -134,7 +155,20 @@ class BeveledRectangleBorder extends OutlinedBorder { case BorderStyle.none: break; case BorderStyle.solid: - final Path path = getOuterPath(rect, textDirection: textDirection) + final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect; + break; + case StrokeAlign.center: + adjustedRect = borderRect.inflate(side.width / 2); + break; + case StrokeAlign.outside: + adjustedRect = borderRect.inflate(side.width); + break; + } + final Path path = _getPath(adjustedRect) ..addPath(getInnerPath(rect, textDirection: textDirection), Offset.zero); canvas.drawPath(path, side.toPaint()); break; diff --git a/packages/flutter/lib/src/painting/borders.dart b/packages/flutter/lib/src/painting/borders.dart index 42a4a76bf29d..eefb548dfaa9 100644 --- a/packages/flutter/lib/src/painting/borders.dart +++ b/packages/flutter/lib/src/painting/borders.dart @@ -21,6 +21,27 @@ enum BorderStyle { // if you add more, think about how they will lerp } +/// The relative position of the stroke on a [BorderSide] in a [Border] or [OutlinedBorder]. +/// When set to [inside], the stroke is drawn completely inside the widget. +/// For [center] and [outside], a property such as [Container.clipBehavior] +/// can be used in an outside widget to clip it. +/// If [Container.decoration] has a border, the container may incorporate +/// [BorderSide.width] as additional padding: +/// - [inside] provides padding with full [BorderSide.width]. +/// - [center] provides padding with half [BorderSide.width]. +/// - [outside] provides zero padding, as stroke is drawn entirely outside. +enum StrokeAlign { + /// The border is drawn on the inside of the border path. + inside, + + /// The border is drawn on the center of the border path, with half of the + /// [BorderSide.width] on the inside, and the other half on the outside of the path. + center, + + /// The border is drawn on the outside of the border path. + outside, +} + /// A side of a border of a box. /// /// A [Border] consists of four [BorderSide] objects: [Border.top], @@ -66,6 +87,7 @@ class BorderSide { this.color = const Color(0xFF000000), this.width = 1.0, this.style = BorderStyle.solid, + this.strokeAlign = StrokeAlign.inside, }) : assert(color != null), assert(width != null), assert(width >= 0.0), @@ -126,6 +148,9 @@ class BorderSide { /// A hairline black border that is not rendered. static const BorderSide none = BorderSide(width: 0.0, style: BorderStyle.none); + /// The direction of where the border will be drawn relative to the container. + final StrokeAlign strokeAlign; + /// Creates a copy of this border but with the given fields replaced with the new values. BorderSide copyWith({ Color? color, @@ -200,7 +225,8 @@ class BorderSide { (b.style == BorderStyle.none && b.width == 0.0)) return true; return a.style == b.style - && a.color == b.color; + && a.color == b.color + && a.strokeAlign == b.strokeAlign; } /// Linearly interpolate between two border sides. @@ -219,14 +245,15 @@ class BorderSide { final double width = ui.lerpDouble(a.width, b.width, t)!; if (width < 0.0) return BorderSide.none; - if (a.style == b.style) { + if (a.style == b.style && a.strokeAlign == b.strokeAlign) { return BorderSide( color: Color.lerp(a.color, b.color, t)!, width: width, style: a.style, // == b.style + strokeAlign: a.strokeAlign, // == b.strokeAlign ); } - Color colorA, colorB; + final Color colorA, colorB; switch (a.style) { case BorderStyle.solid: colorA = a.color; @@ -243,9 +270,20 @@ class BorderSide { colorB = b.color.withAlpha(0x00); break; } + if (a.strokeAlign != b.strokeAlign) { + // When strokeAlign changes, lerp to 0, then from 0 to the target width. + // All StrokeAlign values share a common zero width state. + final StrokeAlign strokeAlign = t > 0.5 ? b.strokeAlign : a.strokeAlign; + return BorderSide( + color: Color.lerp(colorA, colorB, t)!, + width: t > 0.5 ? ui.lerpDouble(0, b.width, t * 2 - 1)! : ui.lerpDouble(a.width, 0, t * 2)!, + strokeAlign: strokeAlign, + ); + } return BorderSide( color: Color.lerp(colorA, colorB, t)!, width: width, + strokeAlign: a.strokeAlign, // == b.strokeAlign ); } @@ -258,14 +296,20 @@ class BorderSide { return other is BorderSide && other.color == color && other.width == width - && other.style == style; + && other.style == style + && other.strokeAlign == strokeAlign; } @override - int get hashCode => Object.hash(color, width, style); + int get hashCode => Object.hash(color, width, style, strokeAlign); @override - String toString() => '${objectRuntimeType(this, 'BorderSide')}($color, ${width.toStringAsFixed(1)}, $style)'; + String toString() { + if (strokeAlign == StrokeAlign.inside) { + return '${objectRuntimeType(this, 'BorderSide')}($color, ${width.toStringAsFixed(1)}, $style)'; + } + return '${objectRuntimeType(this, 'BorderSide')}($color, ${width.toStringAsFixed(1)}, $style, $strokeAlign)'; + } } /// Base class for shape outlines. diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart index e10058ba61a8..2834bad15d1f 100644 --- a/packages/flutter/lib/src/painting/box_border.dart +++ b/packages/flutter/lib/src/painting/box_border.dart @@ -210,16 +210,29 @@ abstract class BoxBorder extends ShapeBorder { assert(side.style != BorderStyle.none); final Paint paint = Paint() ..color = side.color; - final RRect outer = borderRadius.toRRect(rect); final double width = side.width; if (width == 0.0) { paint ..style = PaintingStyle.stroke ..strokeWidth = 0.0; - canvas.drawRRect(outer, paint); + canvas.drawRRect(borderRadius.toRRect(rect), paint); } else { - final RRect inner = outer.deflate(width); - canvas.drawDRRect(outer, inner, paint); + if (side.strokeAlign == StrokeAlign.inside) { + final RRect outer = borderRadius.toRRect(rect); + final RRect inner = outer.deflate(width); + canvas.drawDRRect(outer, inner, paint); + } else { + final Rect inner; + final Rect outer; + if (side.strokeAlign == StrokeAlign.center) { + inner = rect.deflate(width / 2); + outer = rect.inflate(width / 2); + } else { + inner = rect; + outer = rect.inflate(width); + } + canvas.drawDRRect(borderRadius.toRRect(outer), borderRadius.toRRect(inner), paint); + } } } @@ -227,7 +240,18 @@ abstract class BoxBorder extends ShapeBorder { assert(side.style != BorderStyle.none); final double width = side.width; final Paint paint = side.toPaint(); - final double radius = (rect.shortestSide - width) / 2.0; + final double radius; + switch (side.strokeAlign) { + case StrokeAlign.inside: + radius = (rect.shortestSide - width) / 2.0; + break; + case StrokeAlign.center: + radius = rect.shortestSide / 2.0; + break; + case StrokeAlign.outside: + radius = (rect.shortestSide + width) / 2.0; + break; + } canvas.drawCircle(rect.center, radius, paint); } @@ -235,7 +259,20 @@ abstract class BoxBorder extends ShapeBorder { assert(side.style != BorderStyle.none); final double width = side.width; final Paint paint = side.toPaint(); - canvas.drawRect(rect.deflate(width / 2.0), paint); + final Rect rectToBeDrawn; + switch (side.strokeAlign) { + case StrokeAlign.inside: + rectToBeDrawn = rect.deflate(width / 2.0); + break; + case StrokeAlign.center: + rectToBeDrawn = rect; + break; + case StrokeAlign.outside: + rectToBeDrawn = rect.inflate(width / 2.0); + break; + } + + canvas.drawRect(rectToBeDrawn, paint); } } @@ -349,8 +386,9 @@ class Border extends BoxBorder { Color color = const Color(0xFF000000), double width = 1.0, BorderStyle style = BorderStyle.solid, + StrokeAlign strokeAlign = StrokeAlign.inside, }) { - final BorderSide side = BorderSide(color: color, width: width, style: style); + final BorderSide side = BorderSide(color: color, width: width, style: style, strokeAlign: strokeAlign); return Border.fromBorderSide(side); } @@ -390,11 +428,21 @@ class Border extends BoxBorder { @override EdgeInsetsGeometry get dimensions { + if (isUniform) { + switch (top.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsets.all(top.width); + case StrokeAlign.center: + return EdgeInsets.all(top.width / 2); + case StrokeAlign.outside: + return EdgeInsets.zero; + } + } return EdgeInsets.fromLTRB(left.width, top.width, right.width, bottom.width); } @override - bool get isUniform => _colorIsUniform && _widthIsUniform && _styleIsUniform; + bool get isUniform => _colorIsUniform && _widthIsUniform && _styleIsUniform && _strokeAlignIsUniform; bool get _colorIsUniform { final Color topColor = top.color; @@ -411,6 +459,13 @@ class Border extends BoxBorder { return right.style == topStyle && bottom.style == topStyle && left.style == topStyle; } + bool get _strokeAlignIsUniform { + final StrokeAlign topStrokeAlign = top.strokeAlign; + return right.strokeAlign == topStrokeAlign + && bottom.strokeAlign == topStrokeAlign + && left.strokeAlign == topStrokeAlign; + } + @override Border? add(ShapeBorder other, { bool reversed = false }) { if (other is Border && @@ -526,6 +581,7 @@ class Border extends BoxBorder { if (!_colorIsUniform) ErrorDescription('BorderSide.color'), if (!_widthIsUniform) ErrorDescription('BorderSide.width'), if (!_styleIsUniform) ErrorDescription('BorderSide.style'), + if (!_strokeAlignIsUniform) ErrorDescription('BorderSide.strokeAlign'), ]); } return true; @@ -533,11 +589,20 @@ class Border extends BoxBorder { assert(() { if (shape != BoxShape.rectangle) { throw FlutterError.fromParts([ - ErrorSummary('A Border can only be drawn as a circle if it is uniform'), + ErrorSummary('A Border can only be drawn as a circle if it is uniform.'), ErrorDescription('The following is not uniform:'), if (!_colorIsUniform) ErrorDescription('BorderSide.color'), if (!_widthIsUniform) ErrorDescription('BorderSide.width'), if (!_styleIsUniform) ErrorDescription('BorderSide.style'), + if (!_strokeAlignIsUniform) ErrorDescription('BorderSide.strokeAlign'), + ]); + } + return true; + }()); + assert(() { + if (!_strokeAlignIsUniform || top.strokeAlign != StrokeAlign.inside) { + throw FlutterError.fromParts([ + ErrorSummary('A Border can only draw strokeAlign different than StrokeAlign.inside on uniform borders.'), ]); } return true; @@ -665,6 +730,16 @@ class BorderDirectional extends BoxBorder { @override EdgeInsetsGeometry get dimensions { + if (isUniform) { + switch (top.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsetsDirectional.all(top.width); + case StrokeAlign.center: + return EdgeInsetsDirectional.all(top.width / 2); + case StrokeAlign.outside: + return EdgeInsetsDirectional.zero; + } + } return EdgeInsetsDirectional.fromSTEB(start.width, top.width, end.width, bottom.width); } @@ -688,9 +763,19 @@ class BorderDirectional extends BoxBorder { bottom.style != topStyle) return false; + if (_strokeAlignIsUniform == false) + return false; + return true; } + bool get _strokeAlignIsUniform { + final StrokeAlign topStrokeAlign = top.strokeAlign; + return start.strokeAlign == topStrokeAlign + && bottom.strokeAlign == topStrokeAlign + && end.strokeAlign == topStrokeAlign; + } + @override BoxBorder? add(ShapeBorder other, { bool reversed = false }) { if (other is BorderDirectional) { @@ -834,8 +919,9 @@ class BorderDirectional extends BoxBorder { assert(borderRadius == null, 'A borderRadius can only be given for uniform borders.'); assert(shape == BoxShape.rectangle, 'A border can only be drawn as a circle if it is uniform.'); + assert(_strokeAlignIsUniform && top.strokeAlign == StrokeAlign.inside, 'A Border can only draw strokeAlign different than StrokeAlign.inside on uniform borders.'); - BorderSide left, right; + final BorderSide left, right; assert(textDirection != null, 'Non-uniform BorderDirectional objects require a TextDirection when painting.'); switch (textDirection!) { case TextDirection.rtl: diff --git a/packages/flutter/lib/src/painting/circle_border.dart b/packages/flutter/lib/src/painting/circle_border.dart index b8927697fac7..65d4f1ed835c 100644 --- a/packages/flutter/lib/src/painting/circle_border.dart +++ b/packages/flutter/lib/src/painting/circle_border.dart @@ -31,7 +31,14 @@ class CircleBorder extends OutlinedBorder { @override EdgeInsetsGeometry get dimensions { - return EdgeInsets.all(side.width); + switch (side.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsets.all(side.width); + case StrokeAlign.center: + return EdgeInsets.all(side.width / 2); + case StrokeAlign.outside: + return EdgeInsets.zero; + } } @override @@ -53,10 +60,23 @@ class CircleBorder extends OutlinedBorder { @override Path getInnerPath(Rect rect, { TextDirection? textDirection }) { + final double radius = rect.shortestSide / 2.0; + final double adjustedRadius; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRadius = radius - side.width; + break; + case StrokeAlign.center: + adjustedRadius = radius - side.width / 2.0; + break; + case StrokeAlign.outside: + adjustedRadius = radius; + break; + } return Path() ..addOval(Rect.fromCircle( center: rect.center, - radius: math.max(0.0, rect.shortestSide / 2.0 - side.width), + radius: math.max(0.0, adjustedRadius), )); } @@ -80,7 +100,19 @@ class CircleBorder extends OutlinedBorder { case BorderStyle.none: break; case BorderStyle.solid: - canvas.drawCircle(rect.center, (rect.shortestSide - side.width) / 2.0, side.toPaint()); + final double radius; + switch (side.strokeAlign) { + case StrokeAlign.inside: + radius = (rect.shortestSide - side.width) / 2.0; + break; + case StrokeAlign.center: + radius = rect.shortestSide / 2.0; + break; + case StrokeAlign.outside: + radius = (rect.shortestSide + side.width) / 2.0; + break; + } + canvas.drawCircle(rect.center, radius, side.toPaint()); } } diff --git a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart index 2e59053c032f..6177ea3ba576 100644 --- a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart +++ b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart @@ -39,7 +39,14 @@ class RoundedRectangleBorder extends OutlinedBorder { @override EdgeInsetsGeometry get dimensions { - return EdgeInsets.all(side.width); + switch (side.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsets.all(side.width); + case StrokeAlign.center: + return EdgeInsets.all(side.width / 2); + case StrokeAlign.outside: + return EdgeInsets.zero; + } } @override @@ -100,8 +107,21 @@ class RoundedRectangleBorder extends OutlinedBorder { @override Path getInnerPath(Rect rect, { TextDirection? textDirection }) { + final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(side.width); + break; + case StrokeAlign.center: + adjustedRect = borderRect.deflate(side.width / 2); + break; + case StrokeAlign.outside: + adjustedRect = borderRect; + break; + } return Path() - ..addRRect(borderRadius.resolve(textDirection).toRRect(rect).deflate(side.width)); + ..addRRect(adjustedRect); } @override @@ -120,12 +140,26 @@ class RoundedRectangleBorder extends OutlinedBorder { if (width == 0.0) { canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), side.toPaint()); } else { - final RRect outer = borderRadius.resolve(textDirection).toRRect(rect); - final RRect inner = outer.deflate(width); final Paint paint = Paint() ..color = side.color; - canvas.drawDRRect(outer, inner, paint); + if (side.strokeAlign == StrokeAlign.inside) { + final RRect outer = borderRadius.resolve(textDirection).toRRect(rect); + final RRect inner = outer.deflate(width); + canvas.drawDRRect(outer, inner, paint); + } else { + final Rect inner; + final Rect outer; + if (side.strokeAlign == StrokeAlign.center) { + inner = rect.deflate(width / 2); + outer = rect.inflate(width / 2); + } else { + inner = rect; + outer = rect.inflate(width); + } + final BorderRadius borderRadiusResolved = borderRadius.resolve(textDirection); + canvas.drawDRRect(borderRadiusResolved.toRRect(outer), borderRadiusResolved.toRRect(inner), paint); } + } } } @@ -258,8 +292,21 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { @override Path getInnerPath(Rect rect, { TextDirection? textDirection }) { + final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(side.width); + break; + case StrokeAlign.center: + adjustedRect = borderRect.deflate(side.width / 2); + break; + case StrokeAlign.outside: + adjustedRect = borderRect; + break; + } return Path() - ..addRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)).deflate(side.width)); + ..addRRect(adjustedRect); } @override @@ -287,11 +334,20 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { if (width == 0.0) { canvas.drawRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)), side.toPaint()); } else { - final RRect outer = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)); - final RRect inner = outer.deflate(width); - final Paint paint = Paint() - ..color = side.color; - canvas.drawDRRect(outer, inner, paint); + final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(width / 2); + break; + case StrokeAlign.center: + adjustedRect = borderRect; + break; + case StrokeAlign.outside: + adjustedRect = borderRect.inflate(width / 2); + break; + } + canvas.drawRRect(adjustedRect, side.toPaint()); } } } diff --git a/packages/flutter/lib/src/painting/stadium_border.dart b/packages/flutter/lib/src/painting/stadium_border.dart index 52c0f66c97f0..70da0a192710 100644 --- a/packages/flutter/lib/src/painting/stadium_border.dart +++ b/packages/flutter/lib/src/painting/stadium_border.dart @@ -32,7 +32,14 @@ class StadiumBorder extends OutlinedBorder { @override EdgeInsetsGeometry get dimensions { - return EdgeInsets.all(side.width); + switch (side.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsets.all(side.width); + case StrokeAlign.center: + return EdgeInsets.all(side.width / 2); + case StrokeAlign.outside: + return EdgeInsets.zero; + } } @override @@ -88,8 +95,21 @@ class StadiumBorder extends OutlinedBorder { @override Path getInnerPath(Rect rect, { TextDirection? textDirection }) { final Radius radius = Radius.circular(rect.shortestSide / 2.0); + final RRect borderRect = RRect.fromRectAndRadius(rect, radius); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(side.width); + break; + case StrokeAlign.center: + adjustedRect = borderRect.deflate(side.width / 2); + break; + case StrokeAlign.outside: + adjustedRect = borderRect; + break; + } return Path() - ..addRRect(RRect.fromRectAndRadius(rect, radius).deflate(side.width)); + ..addRRect(adjustedRect); } @override @@ -106,8 +126,21 @@ class StadiumBorder extends OutlinedBorder { break; case BorderStyle.solid: final Radius radius = Radius.circular(rect.shortestSide / 2.0); + final RRect borderRect = RRect.fromRectAndRadius(rect, radius); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(side.width / 2); + break; + case StrokeAlign.center: + adjustedRect = borderRect; + break; + case StrokeAlign.outside: + adjustedRect = borderRect.inflate(side.width /2); + break; + } canvas.drawRRect( - RRect.fromRectAndRadius(rect, radius).deflate(side.width / 2.0), + adjustedRect, side.toPaint(), ); } @@ -257,11 +290,20 @@ class _StadiumToCircleBorder extends OutlinedBorder { if (width == 0.0) { canvas.drawRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)), side.toPaint()); } else { - final RRect outer = _adjustBorderRadius(rect).toRRect(_adjustRect(rect)); - final RRect inner = outer.deflate(width); - final Paint paint = Paint() - ..color = side.color; - canvas.drawDRRect(outer, inner, paint); + final RRect borderRect = _adjustBorderRadius(rect).toRRect(_adjustRect(rect)); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(width / 2); + break; + case StrokeAlign.center: + adjustedRect = borderRect; + break; + case StrokeAlign.outside: + adjustedRect = borderRect.inflate(width / 2); + break; + } + canvas.drawRRect(adjustedRect, side.toPaint()); } } } @@ -377,8 +419,21 @@ class _StadiumToRoundedRectangleBorder extends OutlinedBorder { @override Path getInnerPath(Rect rect, { TextDirection? textDirection }) { + final RRect borderRect = _adjustBorderRadius(rect).toRRect(rect); + final RRect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(side.width); + break; + case StrokeAlign.center: + adjustedRect = borderRect.deflate(side.width / 2); + break; + case StrokeAlign.outside: + adjustedRect = borderRect; + break; + } return Path() - ..addRRect(_adjustBorderRadius(rect).toRRect(rect).deflate(side.width)); + ..addRRect(adjustedRect); } @override @@ -406,11 +461,21 @@ class _StadiumToRoundedRectangleBorder extends OutlinedBorder { if (width == 0.0) { canvas.drawRRect(_adjustBorderRadius(rect).toRRect(rect), side.toPaint()); } else { - final RRect outer = _adjustBorderRadius(rect).toRRect(rect); - final RRect inner = outer.deflate(width); - final Paint paint = Paint() - ..color = side.color; - canvas.drawDRRect(outer, inner, paint); + if (side.strokeAlign == StrokeAlign.inside) { + final RRect outer = _adjustBorderRadius(rect).toRRect(rect); + final RRect inner = outer.deflate(width); + final Paint paint = Paint() + ..color = side.color; + canvas.drawDRRect(outer, inner, paint); + } else { + final RRect outer; + if (side.strokeAlign == StrokeAlign.center) { + outer = _adjustBorderRadius(rect).toRRect(rect); + } else { + outer = _adjustBorderRadius(rect.inflate(width)).toRRect(rect.inflate(width / 2)); + } + canvas.drawRRect(outer, side.toPaint()); + } } } } diff --git a/packages/flutter/test/painting/beveled_rectangle_border_test.dart b/packages/flutter/test/painting/beveled_rectangle_border_test.dart index 3cefc32b8bea..14f4dd9c9e1d 100644 --- a/packages/flutter/test/painting/beveled_rectangle_border_test.dart +++ b/packages/flutter/test/painting/beveled_rectangle_border_test.dart @@ -106,4 +106,29 @@ void main() { expect(border.getOuterPath(rect,textDirection: TextDirection.rtl), looksLikeRectRtl); expect(border.getInnerPath(rect,textDirection: TextDirection.rtl), looksLikeRectRtl); }); + + test('BeveledRectangleBorder with StrokeAlign', () { + const BorderRadius borderRadius = BorderRadius.all(Radius.circular(10)); + const BeveledRectangleBorder inside = BeveledRectangleBorder(side: BorderSide(width: 10.0), borderRadius: borderRadius); + const BeveledRectangleBorder center = BeveledRectangleBorder(side: BorderSide(width: 10.0, strokeAlign: StrokeAlign.center), borderRadius: borderRadius); + const BeveledRectangleBorder outside = BeveledRectangleBorder(side: BorderSide(width: 10.0, strokeAlign: StrokeAlign.outside), borderRadius: borderRadius); + expect(inside.dimensions, const EdgeInsets.all(10.0)); + expect(center.dimensions, const EdgeInsets.all(5.0)); + expect(outside.dimensions, EdgeInsets.zero); + + const Rect rect = Rect.fromLTWH(0.0, 0.0, 120.0, 40.0); + + expect(inside.getInnerPath(rect), isPathThat( + includes: const [ Offset(10, 20), Offset(100, 10), Offset(50, 30), Offset(50, 20) ], + excludes: const [ Offset(9, 9), Offset(100, 0), Offset(110, 31), Offset(9, 31) ], + )); + expect(center.getInnerPath(rect), isPathThat( + includes: const [ Offset(9, 9), Offset(100, 10), Offset(110, 31), Offset(9, 31) ], + excludes: const [ Offset(4, 4), Offset(100, 0), Offset(116, 31), Offset(4, 31) ], + )); + expect(outside.getInnerPath(rect), isPathThat( + includes: const [ Offset(5, 5), Offset(110, 0), Offset(116, 31), Offset(4, 31) ], + excludes: const [ Offset.zero, Offset(120, 0), Offset(120, 31), Offset(0, 31) ], + )); + }); } diff --git a/packages/flutter/test/painting/border_side_test.dart b/packages/flutter/test/painting/border_side_test.dart index c655cefd4bd1..35f502394cf8 100644 --- a/packages/flutter/test/painting/border_side_test.dart +++ b/packages/flutter/test/painting/border_side_test.dart @@ -122,4 +122,18 @@ void main() { 'BorderSide(Color(0xffaabbcc), 1.2, BorderStyle.solid)', ); }); + + test('BorderSide - lerp with strokeAlign', () { + const BorderSide side0 = BorderSide(width: 2.0, strokeAlign: StrokeAlign.center); + const BorderSide side1 = BorderSide(width: 2.0, strokeAlign: StrokeAlign.outside); + expect(BorderSide.lerp(side0, side1, 0), const BorderSide(width: 2.0, strokeAlign: StrokeAlign.center)); + expect(BorderSide.lerp(side0, side1, 0.5), const BorderSide(width: 0.0, strokeAlign: StrokeAlign.center)); + expect(BorderSide.lerp(side0, side1, 1), const BorderSide(width: 2.0, strokeAlign: StrokeAlign.outside)); + + const BorderSide side2 = BorderSide(width: 2.0); + const BorderSide side3 = BorderSide(width: 2.0, strokeAlign: StrokeAlign.center); + expect(BorderSide.lerp(side2, side3, 0), const BorderSide(width: 2.0)); + expect(BorderSide.lerp(side2, side3, 0.5), const BorderSide(width: 0.0)); + expect(BorderSide.lerp(side2, side3, 1), const BorderSide(width: 2.0, strokeAlign: StrokeAlign.center)); + }); } diff --git a/packages/flutter/test/painting/border_test.dart b/packages/flutter/test/painting/border_test.dart index 355964bd9d7e..fa2fd4758fa7 100644 --- a/packages/flutter/test/painting/border_test.dart +++ b/packages/flutter/test/painting/border_test.dart @@ -2,9 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart' show FlutterError; import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; +class TestCanvas implements Canvas { + final List invocations = []; + + @override + void noSuchMethod(Invocation invocation) { + invocations.add(invocation); + } +} + void main() { test('Border.uniform constructor', () { const BorderSide side = BorderSide(); @@ -189,6 +199,14 @@ void main() { ).isUniform, false, ); + expect( + const Border( + left: BorderSide(), + top: BorderSide(strokeAlign: StrokeAlign.center), + right: BorderSide(strokeAlign: StrokeAlign.outside), + ).isUniform, + false, + ); expect( const Border().isUniform, true, @@ -238,4 +256,48 @@ void main() { expect(Border.lerp(null, visualWithTop10, 2.0), const Border(top: BorderSide(width: 20.0))); expect(Border.lerp(at0, at100, 2.0), at200); }); + + test('Border - throws correct exception with strokeAlign', () { + late FlutterError error; + try { + final TestCanvas canvas = TestCanvas(); + // Border.all supports all StrokeAlign values. + // Border() supports StrokeAlign.inside only. + const Border( + left: BorderSide(strokeAlign: StrokeAlign.center), + right: BorderSide(strokeAlign: StrokeAlign.outside), + ).paint(canvas, const Rect.fromLTWH(10.0, 20.0, 30.0, 40.0)); + } on FlutterError catch (e) { + error = e; + } + expect(error, isNotNull); + expect(error.diagnostics.length, 1); + expect( + error.diagnostics[0].toStringDeep(), + 'A Border can only draw strokeAlign different than\nStrokeAlign.inside on uniform borders.\n', + ); + }); + + test('Border.dimension', () { + final Border insideBorder = Border.all(width: 10); + expect(insideBorder.dimensions, const EdgeInsets.all(10)); + + final Border centerBorder = Border.all(width: 10, strokeAlign: StrokeAlign.center); + expect(centerBorder.dimensions, const EdgeInsets.all(5)); + + final Border outsideBorder = Border.all(width: 10, strokeAlign: StrokeAlign.outside); + expect(outsideBorder.dimensions, EdgeInsets.zero); + + const BorderSide insideSide = BorderSide(width: 10); + const BorderDirectional insideBorderDirectional = BorderDirectional(top: insideSide, bottom: insideSide, start: insideSide, end: insideSide); + expect(insideBorderDirectional.dimensions, const EdgeInsetsDirectional.all(10)); + + const BorderSide centerSide = BorderSide(width: 10, strokeAlign: StrokeAlign.center); + const BorderDirectional centerBorderDirectional = BorderDirectional(top: centerSide, bottom: centerSide, start: centerSide, end: centerSide); + expect(centerBorderDirectional.dimensions, const EdgeInsetsDirectional.all(5)); + + const BorderSide outsideSide = BorderSide(width: 10, strokeAlign: StrokeAlign.outside); + const BorderDirectional outsideBorderDirectional = BorderDirectional(top: outsideSide, bottom: outsideSide, start: outsideSide, end: outsideSide); + expect(outsideBorderDirectional.dimensions, EdgeInsetsDirectional.zero); + }); } diff --git a/packages/flutter/test/painting/rounded_rectangle_border_test.dart b/packages/flutter/test/painting/rounded_rectangle_border_test.dart index e137fa9c61c1..aa9c6c888078 100644 --- a/packages/flutter/test/painting/rounded_rectangle_border_test.dart +++ b/packages/flutter/test/painting/rounded_rectangle_border_test.dart @@ -130,4 +130,24 @@ void main() { expect(direct50.hashCode, indirect50.hashCode); expect(direct50.toString(), indirect50.toString()); }); + + test('RoundedRectangleBorder.dimensions and CircleBorder.dimensions', () { + const RoundedRectangleBorder insideRoundedRectangleBorder = RoundedRectangleBorder(side: BorderSide(width: 10)); + expect(insideRoundedRectangleBorder.dimensions, const EdgeInsets.all(10)); + + const RoundedRectangleBorder centerRoundedRectangleBorder = RoundedRectangleBorder(side: BorderSide(width: 10, strokeAlign: StrokeAlign.center)); + expect(centerRoundedRectangleBorder.dimensions, const EdgeInsets.all(5)); + + const RoundedRectangleBorder outsideRoundedRectangleBorder = RoundedRectangleBorder(side: BorderSide(width: 10, strokeAlign: StrokeAlign.outside)); + expect(outsideRoundedRectangleBorder.dimensions, EdgeInsets.zero); + + const CircleBorder insideCircleBorder = CircleBorder(side: BorderSide(width: 10)); + expect(insideCircleBorder.dimensions, const EdgeInsets.all(10)); + + const CircleBorder centerCircleBorder = CircleBorder(side: BorderSide(width: 10, strokeAlign: StrokeAlign.center)); + expect(centerCircleBorder.dimensions, const EdgeInsets.all(5)); + + const CircleBorder outsideCircleBorder = CircleBorder(side: BorderSide(width: 10, strokeAlign: StrokeAlign.outside)); + expect(outsideCircleBorder.dimensions, EdgeInsets.zero); + }); } diff --git a/packages/flutter/test/painting/stadium_border_test.dart b/packages/flutter/test/painting/stadium_border_test.dart index 1743be29316a..5b7edb5cc34d 100644 --- a/packages/flutter/test/painting/stadium_border_test.dart +++ b/packages/flutter/test/painting/stadium_border_test.dart @@ -47,6 +47,33 @@ void main() { ); }); + test('StadiumBorder with StrokeAlign', () { + const StadiumBorder center = StadiumBorder(side: BorderSide(width: 10.0, strokeAlign: StrokeAlign.center)); + const StadiumBorder outside = StadiumBorder(side: BorderSide(width: 10.0, strokeAlign: StrokeAlign.outside)); + expect(center.dimensions, const EdgeInsets.all(5.0)); + expect(outside.dimensions, EdgeInsets.zero); + + const Rect rect = Rect.fromLTRB(10.0, 20.0, 100.0, 200.0); + + expect( + (Canvas canvas) => center.paint(canvas, rect), + paints + ..rrect( + rrect: RRect.fromRectAndRadius(rect, Radius.circular(rect.shortestSide / 2.0)), + strokeWidth: 10.0, + ), + ); + + expect( + (Canvas canvas) => outside.paint(canvas, rect), + paints + ..rrect( + rrect: RRect.fromRectAndRadius(rect, Radius.circular(rect.shortestSide / 2.0)).inflate(5.0), + strokeWidth: 10.0, + ), + ); + }); + test('StadiumBorder and CircleBorder', () { const StadiumBorder stadium = StadiumBorder(); const CircleBorder circle = CircleBorder();