Fix anti-aliasing when painting borders with solid colors. (#153365)
Trying to reland https://github.com/flutter/flutter/pull/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:  Close https://github.com/flutter/flutter/issues/13675.
This commit is contained in:
parent
70460854d1
commit
ce63c029c5
@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'basic_types.dart';
|
||||
import 'border_radius.dart';
|
||||
import 'borders.dart';
|
||||
import 'box_border.dart';
|
||||
import 'box_shadow.dart';
|
||||
import 'colors.dart';
|
||||
@ -462,10 +463,63 @@ class _BoxDecorationPainter extends BoxPainter {
|
||||
|
||||
void _paintBackgroundColor(Canvas canvas, Rect rect, TextDirection? textDirection) {
|
||||
if (_decoration.color != null || _decoration.gradient != null) {
|
||||
_paintBox(canvas, rect, _getBackgroundPaint(rect, textDirection), textDirection);
|
||||
// When border is filled, the rect is reduced to avoid anti-aliasing
|
||||
// rounding error leaking the background color around the clipped shape.
|
||||
final Rect adjustedRect = _adjustedRectOnOutlinedBorder(rect, textDirection);
|
||||
_paintBox(canvas, adjustedRect, _getBackgroundPaint(rect, textDirection), textDirection);
|
||||
}
|
||||
}
|
||||
|
||||
double _calculateAdjustedSide(BorderSide side) {
|
||||
if (side.color.alpha == 255 && side.style == BorderStyle.solid) {
|
||||
return side.strokeInset;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Rect _adjustedRectOnOutlinedBorder(Rect rect, TextDirection? textDirection) {
|
||||
if (_decoration.border == null) {
|
||||
return rect;
|
||||
}
|
||||
|
||||
if (_decoration.border is Border) {
|
||||
final Border border = _decoration.border! as Border;
|
||||
|
||||
final EdgeInsets insets = EdgeInsets.fromLTRB(
|
||||
_calculateAdjustedSide(border.left),
|
||||
_calculateAdjustedSide(border.top),
|
||||
_calculateAdjustedSide(border.right),
|
||||
_calculateAdjustedSide(border.bottom),
|
||||
) / 2;
|
||||
|
||||
return Rect.fromLTRB(
|
||||
rect.left + insets.left,
|
||||
rect.top + insets.top,
|
||||
rect.right - insets.right,
|
||||
rect.bottom - insets.bottom,
|
||||
);
|
||||
} else if (_decoration.border is BorderDirectional && textDirection != null) {
|
||||
final BorderDirectional border = _decoration.border! as BorderDirectional;
|
||||
final BorderSide leftSide = textDirection == TextDirection.rtl ? border.end : border.start;
|
||||
final BorderSide rightSide = textDirection == TextDirection.rtl ? border.start : border.end;
|
||||
|
||||
final EdgeInsets insets = EdgeInsets.fromLTRB(
|
||||
_calculateAdjustedSide(leftSide),
|
||||
_calculateAdjustedSide(border.top),
|
||||
_calculateAdjustedSide(rightSide),
|
||||
_calculateAdjustedSide(border.bottom),
|
||||
) / 2;
|
||||
|
||||
return Rect.fromLTRB(
|
||||
rect.left + insets.left,
|
||||
rect.top + insets.top,
|
||||
rect.right - insets.right,
|
||||
rect.bottom - insets.bottom,
|
||||
);
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
DecorationImagePainter? _imagePainter;
|
||||
void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
|
||||
if (_decoration.image == null) {
|
||||
|
@ -406,13 +406,26 @@ class _ShapeDecorationPainter extends BoxPainter {
|
||||
void _paintInterior(Canvas canvas, Rect rect, TextDirection? textDirection) {
|
||||
if (_interiorPaint != null) {
|
||||
if (_decoration.shape.preferPaintInterior) {
|
||||
_decoration.shape.paintInterior(canvas, rect, _interiorPaint!, textDirection: textDirection);
|
||||
// When border is filled, the rect is reduced to avoid anti-aliasing
|
||||
// rounding error leaking the background color around the clipped shape.
|
||||
final Rect adjustedRect = _adjustedRectOnOutlinedBorder(rect);
|
||||
_decoration.shape.paintInterior(canvas, adjustedRect, _interiorPaint!, textDirection: textDirection);
|
||||
} else {
|
||||
canvas.drawPath(_outerPath, _interiorPaint!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rect _adjustedRectOnOutlinedBorder(Rect rect) {
|
||||
if (_decoration.shape is OutlinedBorder && _decoration.color != null) {
|
||||
final BorderSide side = (_decoration.shape as OutlinedBorder).side;
|
||||
if (side.color.alpha == 255 && side.style == BorderStyle.solid) {
|
||||
return rect.deflate(side.strokeInset / 2);
|
||||
}
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
DecorationImagePainter? _imagePainter;
|
||||
void _paintImage(Canvas canvas, ImageConfiguration configuration) {
|
||||
if (_decoration.image == null) {
|
||||
|
@ -1790,7 +1790,7 @@ void main() {
|
||||
expect(
|
||||
find.ancestor(of: find.byType(Table), matching: find.byType(Container)),
|
||||
paints..rect(
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, width, height),
|
||||
rect: const Rect.fromLTRB(borderVertical / 2, borderHorizontal / 2, width - borderVertical / 2, height - borderHorizontal / 2),
|
||||
color: backgroundColor,
|
||||
),
|
||||
);
|
||||
|
@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show Image;
|
||||
@ -577,4 +580,64 @@ Future<void> main() async {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/13675
|
||||
testWidgets('Border avoids clipping edges when possible', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
Widget buildWidget(Color color) {
|
||||
final List<Widget> circles = <Widget>[];
|
||||
for (int i = 100; i > 25; i--) {
|
||||
final double radius = i * 2.5;
|
||||
final double angle = i * 0.5;
|
||||
final double x = radius * math.cos(angle);
|
||||
final double y = radius * math.sin(angle);
|
||||
final Widget circle = Positioned(
|
||||
left: 275 - x,
|
||||
top: 275 - y,
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(75),
|
||||
color: Colors.black,
|
||||
border: Border.all(color: color, width: 50),
|
||||
),
|
||||
),
|
||||
);
|
||||
circles.add(circle);
|
||||
}
|
||||
|
||||
return Center(
|
||||
key: key,
|
||||
child: Container(
|
||||
width: 800,
|
||||
height: 800,
|
||||
decoration: const ShapeDecoration(
|
||||
color: Colors.orangeAccent,
|
||||
shape: CircleBorder(
|
||||
side: BorderSide(strokeAlign: BorderSide.strokeAlignOutside),
|
||||
),
|
||||
),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: circles,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildWidget(const Color(0xffffffff)));
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('painting.box_decoration.border.should_be_white.png'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildWidget(const Color(0xfeffffff)));
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('painting.box_decoration.border.show_lines_due_to_opacity.png'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -2,6 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
@ -158,4 +162,65 @@ Future<void> main() async {
|
||||
expect(a.hashCode, equals(b.hashCode));
|
||||
expect(a, equals(b));
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/13675
|
||||
testWidgets('OutlinedBorder avoids clipping edges when possible', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
Widget buildWidget(Color color) {
|
||||
final List<Widget> circles = <Widget>[];
|
||||
for (int i = 100; i > 25; i--) {
|
||||
final double radius = i * 2.5;
|
||||
final double angle = i * 0.5;
|
||||
final double x = radius * math.cos(angle);
|
||||
final double y = radius * math.sin(angle);
|
||||
final Widget circle = Positioned(
|
||||
left: 275 - x,
|
||||
top: 275 - y,
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: ShapeDecoration(
|
||||
color: Colors.black,
|
||||
shape: CircleBorder(
|
||||
side: BorderSide(color: color, width: 50),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
circles.add(circle);
|
||||
}
|
||||
|
||||
return Center(
|
||||
key: key,
|
||||
child: Container(
|
||||
width: 800,
|
||||
height: 800,
|
||||
decoration: const ShapeDecoration(
|
||||
color: Colors.redAccent,
|
||||
shape: CircleBorder(
|
||||
side: BorderSide(strokeAlign: BorderSide.strokeAlignOutside),
|
||||
),
|
||||
),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: circles,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildWidget(const Color(0xffffffff)));
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('painting.shape_decoration.outlined_border.should_be_white.png'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildWidget(const Color(0xfeffffff)));
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('painting.shape_decoration.outlined_border.show_lines_due_to_opacity.png'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user