Skip to content

Commit ce63c02

Browse files
authored
Fix anti-aliasing when painting borders with solid colors. (#153365)
Trying to reland flutter/flutter#122317 in 2024. Let's see if we can. <img width="666" alt="image" src="https://user-images.githubusercontent.com/351125/182002867-03d55bbb-163d-48b9-ba3c-ed32dbef2680.png"> Side effect: shapes with border will be rounder: ![Frame 6](https://github.com/user-attachments/assets/95324ebc-8db5-4365-817f-bc62304b9044) Close flutter/flutter#13675.
1 parent 7046085 commit ce63c02

File tree

5 files changed

+198
-3
lines changed

5 files changed

+198
-3
lines changed

packages/flutter/lib/src/painting/box_decoration.dart

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart';
1111

1212
import 'basic_types.dart';
1313
import 'border_radius.dart';
14+
import 'borders.dart';
1415
import 'box_border.dart';
1516
import 'box_shadow.dart';
1617
import 'colors.dart';
@@ -462,10 +463,63 @@ class _BoxDecorationPainter extends BoxPainter {
462463

463464
void _paintBackgroundColor(Canvas canvas, Rect rect, TextDirection? textDirection) {
464465
if (_decoration.color != null || _decoration.gradient != null) {
465-
_paintBox(canvas, rect, _getBackgroundPaint(rect, textDirection), textDirection);
466+
// When border is filled, the rect is reduced to avoid anti-aliasing
467+
// rounding error leaking the background color around the clipped shape.
468+
final Rect adjustedRect = _adjustedRectOnOutlinedBorder(rect, textDirection);
469+
_paintBox(canvas, adjustedRect, _getBackgroundPaint(rect, textDirection), textDirection);
466470
}
467471
}
468472

473+
double _calculateAdjustedSide(BorderSide side) {
474+
if (side.color.alpha == 255 && side.style == BorderStyle.solid) {
475+
return side.strokeInset;
476+
}
477+
return 0;
478+
}
479+
480+
Rect _adjustedRectOnOutlinedBorder(Rect rect, TextDirection? textDirection) {
481+
if (_decoration.border == null) {
482+
return rect;
483+
}
484+
485+
if (_decoration.border is Border) {
486+
final Border border = _decoration.border! as Border;
487+
488+
final EdgeInsets insets = EdgeInsets.fromLTRB(
489+
_calculateAdjustedSide(border.left),
490+
_calculateAdjustedSide(border.top),
491+
_calculateAdjustedSide(border.right),
492+
_calculateAdjustedSide(border.bottom),
493+
) / 2;
494+
495+
return Rect.fromLTRB(
496+
rect.left + insets.left,
497+
rect.top + insets.top,
498+
rect.right - insets.right,
499+
rect.bottom - insets.bottom,
500+
);
501+
} else if (_decoration.border is BorderDirectional && textDirection != null) {
502+
final BorderDirectional border = _decoration.border! as BorderDirectional;
503+
final BorderSide leftSide = textDirection == TextDirection.rtl ? border.end : border.start;
504+
final BorderSide rightSide = textDirection == TextDirection.rtl ? border.start : border.end;
505+
506+
final EdgeInsets insets = EdgeInsets.fromLTRB(
507+
_calculateAdjustedSide(leftSide),
508+
_calculateAdjustedSide(border.top),
509+
_calculateAdjustedSide(rightSide),
510+
_calculateAdjustedSide(border.bottom),
511+
) / 2;
512+
513+
return Rect.fromLTRB(
514+
rect.left + insets.left,
515+
rect.top + insets.top,
516+
rect.right - insets.right,
517+
rect.bottom - insets.bottom,
518+
);
519+
}
520+
return rect;
521+
}
522+
469523
DecorationImagePainter? _imagePainter;
470524
void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
471525
if (_decoration.image == null) {

packages/flutter/lib/src/painting/shape_decoration.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,26 @@ class _ShapeDecorationPainter extends BoxPainter {
406406
void _paintInterior(Canvas canvas, Rect rect, TextDirection? textDirection) {
407407
if (_interiorPaint != null) {
408408
if (_decoration.shape.preferPaintInterior) {
409-
_decoration.shape.paintInterior(canvas, rect, _interiorPaint!, textDirection: textDirection);
409+
// When border is filled, the rect is reduced to avoid anti-aliasing
410+
// rounding error leaking the background color around the clipped shape.
411+
final Rect adjustedRect = _adjustedRectOnOutlinedBorder(rect);
412+
_decoration.shape.paintInterior(canvas, adjustedRect, _interiorPaint!, textDirection: textDirection);
410413
} else {
411414
canvas.drawPath(_outerPath, _interiorPaint!);
412415
}
413416
}
414417
}
415418

419+
Rect _adjustedRectOnOutlinedBorder(Rect rect) {
420+
if (_decoration.shape is OutlinedBorder && _decoration.color != null) {
421+
final BorderSide side = (_decoration.shape as OutlinedBorder).side;
422+
if (side.color.alpha == 255 && side.style == BorderStyle.solid) {
423+
return rect.deflate(side.strokeInset / 2);
424+
}
425+
}
426+
return rect;
427+
}
428+
416429
DecorationImagePainter? _imagePainter;
417430
void _paintImage(Canvas canvas, ImageConfiguration configuration) {
418431
if (_decoration.image == null) {

packages/flutter/test/material/data_table_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1790,7 +1790,7 @@ void main() {
17901790
expect(
17911791
find.ancestor(of: find.byType(Table), matching: find.byType(Container)),
17921792
paints..rect(
1793-
rect: const Rect.fromLTRB(0.0, 0.0, width, height),
1793+
rect: const Rect.fromLTRB(borderVertical / 2, borderHorizontal / 2, width - borderVertical / 2, height - borderHorizontal / 2),
17941794
color: backgroundColor,
17951795
),
17961796
);

packages/flutter/test/widgets/box_decoration_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
@Tags(<String>['reduced-test-set'])
6+
library;
7+
58
import 'dart:async';
69
import 'dart:math' as math;
710
import 'dart:ui' as ui show Image;
@@ -577,4 +580,64 @@ Future<void> main() async {
577580
),
578581
);
579582
});
583+
584+
// Regression test for https://github.com/flutter/flutter/issues/13675
585+
testWidgets('Border avoids clipping edges when possible', (WidgetTester tester) async {
586+
final Key key = UniqueKey();
587+
Widget buildWidget(Color color) {
588+
final List<Widget> circles = <Widget>[];
589+
for (int i = 100; i > 25; i--) {
590+
final double radius = i * 2.5;
591+
final double angle = i * 0.5;
592+
final double x = radius * math.cos(angle);
593+
final double y = radius * math.sin(angle);
594+
final Widget circle = Positioned(
595+
left: 275 - x,
596+
top: 275 - y,
597+
child: Container(
598+
width: 250,
599+
height: 250,
600+
decoration: BoxDecoration(
601+
borderRadius: BorderRadius.circular(75),
602+
color: Colors.black,
603+
border: Border.all(color: color, width: 50),
604+
),
605+
),
606+
);
607+
circles.add(circle);
608+
}
609+
610+
return Center(
611+
key: key,
612+
child: Container(
613+
width: 800,
614+
height: 800,
615+
decoration: const ShapeDecoration(
616+
color: Colors.orangeAccent,
617+
shape: CircleBorder(
618+
side: BorderSide(strokeAlign: BorderSide.strokeAlignOutside),
619+
),
620+
),
621+
child: Directionality(
622+
textDirection: TextDirection.ltr,
623+
child: Stack(
624+
children: circles,
625+
),
626+
),
627+
),
628+
);
629+
}
630+
631+
await tester.pumpWidget(buildWidget(const Color(0xffffffff)));
632+
await expectLater(
633+
find.byKey(key),
634+
matchesGoldenFile('painting.box_decoration.border.should_be_white.png'),
635+
);
636+
637+
await tester.pumpWidget(buildWidget(const Color(0xfeffffff)));
638+
await expectLater(
639+
find.byKey(key),
640+
matchesGoldenFile('painting.box_decoration.border.show_lines_due_to_opacity.png'),
641+
);
642+
});
580643
}

packages/flutter/test/widgets/shape_decoration_test.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
@Tags(<String>['reduced-test-set'])
6+
library;
7+
8+
import 'dart:math' as math;
59
import 'dart:typed_data';
610
import 'dart:ui' as ui show Image;
711

@@ -158,4 +162,65 @@ Future<void> main() async {
158162
expect(a.hashCode, equals(b.hashCode));
159163
expect(a, equals(b));
160164
});
165+
166+
// Regression test for https://github.com/flutter/flutter/issues/13675
167+
testWidgets('OutlinedBorder avoids clipping edges when possible', (WidgetTester tester) async {
168+
final Key key = UniqueKey();
169+
Widget buildWidget(Color color) {
170+
final List<Widget> circles = <Widget>[];
171+
for (int i = 100; i > 25; i--) {
172+
final double radius = i * 2.5;
173+
final double angle = i * 0.5;
174+
final double x = radius * math.cos(angle);
175+
final double y = radius * math.sin(angle);
176+
final Widget circle = Positioned(
177+
left: 275 - x,
178+
top: 275 - y,
179+
child: Container(
180+
width: 250,
181+
height: 250,
182+
decoration: ShapeDecoration(
183+
color: Colors.black,
184+
shape: CircleBorder(
185+
side: BorderSide(color: color, width: 50),
186+
),
187+
),
188+
),
189+
);
190+
circles.add(circle);
191+
}
192+
193+
return Center(
194+
key: key,
195+
child: Container(
196+
width: 800,
197+
height: 800,
198+
decoration: const ShapeDecoration(
199+
color: Colors.redAccent,
200+
shape: CircleBorder(
201+
side: BorderSide(strokeAlign: BorderSide.strokeAlignOutside),
202+
),
203+
),
204+
child: Directionality(
205+
textDirection: TextDirection.ltr,
206+
child: Stack(
207+
children: circles,
208+
),
209+
),
210+
),
211+
);
212+
}
213+
214+
await tester.pumpWidget(buildWidget(const Color(0xffffffff)));
215+
await expectLater(
216+
find.byKey(key),
217+
matchesGoldenFile('painting.shape_decoration.outlined_border.should_be_white.png'),
218+
);
219+
220+
await tester.pumpWidget(buildWidget(const Color(0xfeffffff)));
221+
await expectLater(
222+
find.byKey(key),
223+
matchesGoldenFile('painting.shape_decoration.outlined_border.show_lines_due_to_opacity.png'),
224+
);
225+
});
161226
}

0 commit comments

Comments
 (0)