Improve and optimize non-uniform Borders. (#124417)
~~Using the same priority order as a Border without borderRadius, it is possible to draw them on top of each other. This is better than the current behavior (crash!) and would work well for a "one color on top, another on bottom" scenario.~~ ~~With this, if approved, we move the current number of possible exceptions from 4 to 1 (`BoxShape.circle` + `borderRadius`).~~ ~~It is kind of odd how `borderRadius.zero` to `borderRadius != BorderRadius.zero` change, but I think it is better than crashing. Alternatively, we just remove the "original function" and see if any goldens are affected.~~ <img width="448" alt="image" src="https://user-images.githubusercontent.com/351125/236550350-7499d758-5b44-40e6-9105-32671eb21998.png"> Another one for @gspencergoog. If this works, we could make the paint method public and re-use in the InputBorder PR (if that's also approved). Single line fix.
This commit is contained in:
parent
fe9c7f4fa7
commit
b7046b32db
@ -242,16 +242,24 @@ abstract class BoxBorder extends ShapeBorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _paintNonUniformBorder(
|
/// Paints a Border with different widths, styles and strokeAligns, on any
|
||||||
|
/// borderRadius while using a single color.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [paintBorder], which supports multiple colors but not borderRadius.
|
||||||
|
/// * [paint], which calls this method.
|
||||||
|
static void paintNonUniformBorder(
|
||||||
Canvas canvas,
|
Canvas canvas,
|
||||||
Rect rect, {
|
Rect rect, {
|
||||||
required BorderRadius? borderRadius,
|
required BorderRadius? borderRadius,
|
||||||
required BoxShape shape,
|
|
||||||
required TextDirection? textDirection,
|
required TextDirection? textDirection,
|
||||||
required BorderSide left,
|
BoxShape shape = BoxShape.rectangle,
|
||||||
required BorderSide top,
|
BorderSide top = BorderSide.none,
|
||||||
required BorderSide right,
|
BorderSide right = BorderSide.none,
|
||||||
required BorderSide bottom,
|
BorderSide bottom = BorderSide.none,
|
||||||
|
BorderSide left = BorderSide.none,
|
||||||
|
required Color color,
|
||||||
}) {
|
}) {
|
||||||
final RRect borderRect;
|
final RRect borderRect;
|
||||||
switch (shape) {
|
switch (shape) {
|
||||||
@ -266,7 +274,7 @@ abstract class BoxBorder extends ShapeBorder {
|
|||||||
Radius.circular(rect.width),
|
Radius.circular(rect.width),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final Paint paint = Paint()..color = top.color;
|
final Paint paint = Paint()..color = color;
|
||||||
final RRect inner = _deflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeInset, top.strokeInset, right.strokeInset, bottom.strokeInset));
|
final RRect inner = _deflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeInset, top.strokeInset, right.strokeInset, bottom.strokeInset));
|
||||||
final RRect outer = _inflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeOutset, top.strokeOutset, right.strokeOutset, bottom.strokeOutset));
|
final RRect outer = _inflateRRect(borderRect, EdgeInsets.fromLTRB(left.strokeOutset, top.strokeOutset, right.strokeOutset, bottom.strokeOutset));
|
||||||
canvas.drawDRRect(outer, inner, paint);
|
canvas.drawDRRect(outer, inner, paint);
|
||||||
@ -489,6 +497,32 @@ class Border extends BoxBorder {
|
|||||||
&& right.strokeAlign == topStrokeAlign;
|
&& right.strokeAlign == topStrokeAlign;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<Color> _distinctVisibleColors() {
|
||||||
|
final Set<Color> distinctVisibleColors = <Color>{};
|
||||||
|
if (top.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(top.color);
|
||||||
|
}
|
||||||
|
if (right.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(right.color);
|
||||||
|
}
|
||||||
|
if (bottom.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(bottom.color);
|
||||||
|
}
|
||||||
|
if (left.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(left.color);
|
||||||
|
}
|
||||||
|
return distinctVisibleColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [BoxBorder.paintNonUniformBorder] is about 20% faster than [paintBorder],
|
||||||
|
// but [paintBorder] is able to draw hairline borders when width is zero
|
||||||
|
// and style is [BorderStyle.solid].
|
||||||
|
bool get _hasHairlineBorder =>
|
||||||
|
(top.style == BorderStyle.solid && top.width == 0.0) ||
|
||||||
|
(right.style == BorderStyle.solid && right.width == 0.0) ||
|
||||||
|
(bottom.style == BorderStyle.solid && bottom.width == 0.0) ||
|
||||||
|
(left.style == BorderStyle.solid && left.width == 0.0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Border? add(ShapeBorder other, { bool reversed = false }) {
|
Border? add(ShapeBorder other, { bool reversed = false }) {
|
||||||
if (other is Border &&
|
if (other is Border &&
|
||||||
@ -603,31 +637,41 @@ class Border extends BoxBorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow painting non-uniform borders if the color and style are uniform.
|
if (_styleIsUniform && top.style == BorderStyle.none) {
|
||||||
if (_colorIsUniform && _styleIsUniform) {
|
return;
|
||||||
switch (top.style) {
|
|
||||||
case BorderStyle.none:
|
|
||||||
return;
|
|
||||||
case BorderStyle.solid:
|
|
||||||
BoxBorder._paintNonUniformBorder(canvas, rect,
|
|
||||||
shape: shape,
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
textDirection: textDirection,
|
|
||||||
left: left,
|
|
||||||
top: top,
|
|
||||||
right: right,
|
|
||||||
bottom: bottom);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(() {
|
// Allow painting non-uniform borders if the visible colors are uniform.
|
||||||
if (borderRadius != null) {
|
final Set<Color> visibleColors = _distinctVisibleColors();
|
||||||
|
final bool hasHairlineBorder = _hasHairlineBorder;
|
||||||
|
// Paint a non uniform border if a single color is visible
|
||||||
|
// and (borderRadius is present) or (border is visible and width != 0.0).
|
||||||
|
if (visibleColors.length == 1 &&
|
||||||
|
!hasHairlineBorder &&
|
||||||
|
(shape == BoxShape.circle ||
|
||||||
|
(borderRadius != null && borderRadius != BorderRadius.zero))) {
|
||||||
|
BoxBorder.paintNonUniformBorder(canvas, rect,
|
||||||
|
shape: shape,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
textDirection: textDirection,
|
||||||
|
top: top.style == BorderStyle.none ? BorderSide.none : top,
|
||||||
|
right: right.style == BorderStyle.none ? BorderSide.none : right,
|
||||||
|
bottom: bottom.style == BorderStyle.none ? BorderSide.none : bottom,
|
||||||
|
left: left.style == BorderStyle.none ? BorderSide.none : left,
|
||||||
|
color: visibleColors.first);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(() {
|
||||||
|
if (hasHairlineBorder) {
|
||||||
|
assert(borderRadius == null || borderRadius == BorderRadius.zero,
|
||||||
|
'A hairline border like `BorderSide(width: 0.0, style: BorderStyle.solid)` can only be drawn when BorderRadius is zero or null.');
|
||||||
|
}
|
||||||
|
if (borderRadius != null && borderRadius != BorderRadius.zero) {
|
||||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||||
ErrorSummary('A borderRadius can only be given on borders with uniform colors and styles.'),
|
ErrorSummary('A borderRadius can only be given on borders with uniform colors.'),
|
||||||
ErrorDescription('The following is not uniform:'),
|
ErrorDescription('The following is not uniform:'),
|
||||||
if (!_colorIsUniform) ErrorDescription('BorderSide.color'),
|
if (!_colorIsUniform) ErrorDescription('BorderSide.color'),
|
||||||
if (!_styleIsUniform) ErrorDescription('BorderSide.style'),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -635,10 +679,9 @@ class Border extends BoxBorder {
|
|||||||
assert(() {
|
assert(() {
|
||||||
if (shape != BoxShape.rectangle) {
|
if (shape != BoxShape.rectangle) {
|
||||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||||
ErrorSummary('A Border can only be drawn as a circle on borders with uniform colors and styles.'),
|
ErrorSummary('A Border can only be drawn as a circle on borders with uniform colors.'),
|
||||||
ErrorDescription('The following is not uniform:'),
|
ErrorDescription('The following is not uniform:'),
|
||||||
if (!_colorIsUniform) ErrorDescription('BorderSide.color'),
|
if (!_colorIsUniform) ErrorDescription('BorderSide.color'),
|
||||||
if (!_styleIsUniform) ErrorDescription('BorderSide.style'),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -646,7 +689,7 @@ class Border extends BoxBorder {
|
|||||||
assert(() {
|
assert(() {
|
||||||
if (!_strokeAlignIsUniform || top.strokeAlign != BorderSide.strokeAlignInside) {
|
if (!_strokeAlignIsUniform || top.strokeAlign != BorderSide.strokeAlignInside) {
|
||||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||||
ErrorSummary('A Border can only draw strokeAlign different than BorderSide.strokeAlignInside on borders with uniform colors and styles.'),
|
ErrorSummary('A Border can only draw strokeAlign different than BorderSide.strokeAlignInside on borders with uniform colors.'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -806,6 +849,31 @@ class BorderDirectional extends BoxBorder {
|
|||||||
&& end.strokeAlign == topStrokeAlign;
|
&& end.strokeAlign == topStrokeAlign;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<Color> _distinctVisibleColors() {
|
||||||
|
final Set<Color> distinctVisibleColors = <Color>{};
|
||||||
|
if (top.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(top.color);
|
||||||
|
}
|
||||||
|
if (end.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(end.color);
|
||||||
|
}
|
||||||
|
if (bottom.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(bottom.color);
|
||||||
|
}
|
||||||
|
if (start.style != BorderStyle.none) {
|
||||||
|
distinctVisibleColors.add(start.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return distinctVisibleColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool get _hasHairlineBorder =>
|
||||||
|
(top.style == BorderStyle.solid && top.width == 0.0) ||
|
||||||
|
(end.style == BorderStyle.solid && end.width == 0.0) ||
|
||||||
|
(bottom.style == BorderStyle.solid && bottom.width == 0.0) ||
|
||||||
|
(start.style == BorderStyle.solid && start.width == 0.0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
BoxBorder? add(ShapeBorder other, { bool reversed = false }) {
|
BoxBorder? add(ShapeBorder other, { bool reversed = false }) {
|
||||||
if (other is BorderDirectional) {
|
if (other is BorderDirectional) {
|
||||||
@ -951,6 +1019,10 @@ class BorderDirectional extends BoxBorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_styleIsUniform && top.style == BorderStyle.none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final BorderSide left, right;
|
final BorderSide left, right;
|
||||||
assert(textDirection != null, 'Non-uniform BorderDirectional objects require a TextDirection when painting.');
|
assert(textDirection != null, 'Non-uniform BorderDirectional objects require a TextDirection when painting.');
|
||||||
switch (textDirection!) {
|
switch (textDirection!) {
|
||||||
@ -962,27 +1034,31 @@ class BorderDirectional extends BoxBorder {
|
|||||||
right = end;
|
right = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow painting non-uniform borders if the color and style are uniform.
|
// Allow painting non-uniform borders if the visible colors are uniform.
|
||||||
if (_colorIsUniform && _styleIsUniform) {
|
final Set<Color> visibleColors = _distinctVisibleColors();
|
||||||
switch (top.style) {
|
final bool hasHairlineBorder = _hasHairlineBorder;
|
||||||
case BorderStyle.none:
|
if (visibleColors.length == 1 &&
|
||||||
return;
|
!hasHairlineBorder &&
|
||||||
case BorderStyle.solid:
|
(shape == BoxShape.circle ||
|
||||||
BoxBorder._paintNonUniformBorder(canvas, rect,
|
(borderRadius != null && borderRadius != BorderRadius.zero))) {
|
||||||
shape: shape,
|
BoxBorder.paintNonUniformBorder(canvas, rect,
|
||||||
borderRadius: borderRadius,
|
shape: shape,
|
||||||
textDirection: textDirection,
|
borderRadius: borderRadius,
|
||||||
left: left,
|
textDirection: textDirection,
|
||||||
top: top,
|
top: top.style == BorderStyle.none ? BorderSide.none : top,
|
||||||
right: right,
|
right: right.style == BorderStyle.none ? BorderSide.none : right,
|
||||||
bottom: bottom);
|
bottom: bottom.style == BorderStyle.none ? BorderSide.none : bottom,
|
||||||
return;
|
left: left.style == BorderStyle.none ? BorderSide.none : left,
|
||||||
}
|
color: visibleColors.first);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(borderRadius == null, 'A borderRadius can only be given for borders with uniform colors and styles.');
|
if (hasHairlineBorder) {
|
||||||
assert(shape == BoxShape.rectangle, 'A Border can only be drawn as a circle on borders with uniform colors and styles.');
|
assert(borderRadius == null || borderRadius == BorderRadius.zero, 'A side like `BorderSide(width: 0.0, style: BorderStyle.solid)` can only be drawn when BorderRadius is zero or null.');
|
||||||
assert(_strokeAlignIsUniform && top.strokeAlign == BorderSide.strokeAlignInside, 'A Border can only draw strokeAlign different than strokeAlignInside on borders with uniform colors and styles.');
|
}
|
||||||
|
assert(borderRadius == null, 'A borderRadius can only be given for borders with uniform colors.');
|
||||||
|
assert(shape == BoxShape.rectangle, 'A Border can only be drawn as a circle on borders with uniform colors.');
|
||||||
|
assert(_strokeAlignIsUniform && top.strokeAlign == BorderSide.strokeAlignInside, 'A Border can only draw strokeAlign different than strokeAlignInside on borders with uniform colors.');
|
||||||
|
|
||||||
paintBorder(canvas, rect, top: top, left: left, bottom: bottom, right: right);
|
paintBorder(canvas, rect, top: top, left: left, bottom: bottom, right: right);
|
||||||
}
|
}
|
||||||
|
@ -1750,7 +1750,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
find.ancestor(of: find.byType(Table), matching: find.byType(Container)),
|
find.ancestor(of: find.byType(Table), matching: find.byType(Container)),
|
||||||
paints..drrect(color: borderColor),
|
paints..path(color: borderColor),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
tester.getTopLeft(find.byType(Table)),
|
tester.getTopLeft(find.byType(Table)),
|
||||||
|
@ -136,8 +136,9 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('Vertical Divider Test 2', (WidgetTester tester) async {
|
testWidgets('Vertical Divider Test 2', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MaterialApp(
|
MaterialApp(
|
||||||
home: Material(
|
theme: ThemeData(useMaterial3: false),
|
||||||
|
home: const Material(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 24.0,
|
height: 24.0,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -273,7 +273,7 @@ void main() {
|
|||||||
expect(error.diagnostics.length, 1);
|
expect(error.diagnostics.length, 1);
|
||||||
expect(
|
expect(
|
||||||
error.diagnostics[0].toStringDeep(),
|
error.diagnostics[0].toStringDeep(),
|
||||||
'A Border can only draw strokeAlign different than\nBorderSide.strokeAlignInside on borders with uniform colors and\nstyles.\n',
|
'A Border can only draw strokeAlign different than\nBorderSide.strokeAlignInside on borders with uniform colors.\n',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -341,8 +341,8 @@ void main() {
|
|||||||
|
|
||||||
// This falls into non-uniform border because of strokeAlign.
|
// This falls into non-uniform border because of strokeAlign.
|
||||||
await tester.pumpWidget(buildWidget(border: allowedBorderVariations));
|
await tester.pumpWidget(buildWidget(border: allowedBorderVariations));
|
||||||
expect(tester.takeException(), isNull,
|
expect(tester.takeException(), isAssertionError,
|
||||||
reason: 'Border with non-uniform strokeAlign should not fail.');
|
reason: 'Border with non-uniform strokeAlign should fail.');
|
||||||
|
|
||||||
await tester.pumpWidget(buildWidget(
|
await tester.pumpWidget(buildWidget(
|
||||||
border: allowedBorderVariations,
|
border: allowedBorderVariations,
|
||||||
@ -364,8 +364,8 @@ void main() {
|
|||||||
borderRadius: BorderRadius.circular(25),
|
borderRadius: BorderRadius.circular(25),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(tester.takeException(), isAssertionError,
|
expect(tester.takeException(), isNull,
|
||||||
reason: 'Border with non-uniform styles should fail with borderRadius.');
|
reason: 'Border with non-uniform styles should work with borderRadius.');
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
buildWidget(
|
buildWidget(
|
||||||
@ -381,6 +381,24 @@ void main() {
|
|||||||
expect(tester.takeException(), isAssertionError,
|
expect(tester.takeException(), isAssertionError,
|
||||||
reason: 'Border with non-uniform colors should fail with borderRadius.');
|
reason: 'Border with non-uniform colors should fail with borderRadius.');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildWidget(
|
||||||
|
border: const Border(bottom: BorderSide(width: 0)),
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(tester.takeException(), isNull,
|
||||||
|
reason: 'Border with a side.width == 0 should work without borderRadius (hairline border).');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildWidget(
|
||||||
|
border: const Border(bottom: BorderSide(width: 0)),
|
||||||
|
borderRadius: BorderRadius.circular(40),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(tester.takeException(), isAssertionError,
|
||||||
|
reason: 'Border with width == 0 and borderRadius should fail (hairline border).');
|
||||||
|
|
||||||
// Tests for BorderDirectional.
|
// Tests for BorderDirectional.
|
||||||
const BorderDirectional allowedBorderDirectionalVariations = BorderDirectional(
|
const BorderDirectional allowedBorderDirectionalVariations = BorderDirectional(
|
||||||
start: BorderSide(width: 5),
|
start: BorderSide(width: 5),
|
||||||
@ -390,7 +408,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(buildWidget(border: allowedBorderDirectionalVariations));
|
await tester.pumpWidget(buildWidget(border: allowedBorderDirectionalVariations));
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isAssertionError);
|
||||||
|
|
||||||
await tester.pumpWidget(buildWidget(
|
await tester.pumpWidget(buildWidget(
|
||||||
border: allowedBorderDirectionalVariations,
|
border: allowedBorderDirectionalVariations,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user