Skip to content

Commit

Permalink
Add StrokeAlign to Border (#102112)
Browse files Browse the repository at this point in the history
  • Loading branch information
bernaferrari authored May 19, 2022
1 parent 893b58d commit 440e0e2
Show file tree
Hide file tree
Showing 11 changed files with 512 additions and 47 deletions.
40 changes: 37 additions & 3 deletions packages/flutter/lib/src/painting/beveled_rectangle_border.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
56 changes: 50 additions & 6 deletions packages/flutter/lib/src/painting/borders.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Expand All @@ -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
);
}

Expand All @@ -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.
Expand Down
106 changes: 96 additions & 10 deletions packages/flutter/lib/src/painting/box_border.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,32 +210,69 @@ 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);
}
}
}

static void _paintUniformBorderWithCircle(Canvas canvas, Rect rect, BorderSide side) {
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);
}

static void _paintUniformBorderWithRectangle(Canvas canvas, Rect rect, BorderSide side) {
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);
}
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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 &&
Expand Down Expand Up @@ -526,18 +581,28 @@ 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;
}());
assert(() {
if (shape != BoxShape.rectangle) {
throw FlutterError.fromParts(<DiagnosticsNode>[
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(<DiagnosticsNode>[
ErrorSummary('A Border can only draw strokeAlign different than StrokeAlign.inside on uniform borders.'),
]);
}
return true;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 440e0e2

Please sign in to comment.