Colored box optimization (#176028) (#176073)

This PR fixed visual bug when placing multiple `ColoredBox`s together.
(Fixes #176028)

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Victor Sanni <victorsanniay@gmail.com>
This commit is contained in:
Bui Dai Duong 2025-11-03 01:59:19 +07:00 committed by GitHub
parent 02d6e8fff6
commit 7efa2aed3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 261 additions and 6 deletions

View File

@ -8400,31 +8400,59 @@ class _StatefulBuilderState extends State<StatefulBuilder> {
/// child on top of that color.
class ColoredBox extends SingleChildRenderObjectWidget {
/// Creates a widget that paints its area with the specified [Color].
const ColoredBox({required this.color, super.child, super.key});
const ColoredBox({required this.color, this.isAntiAlias = true, super.child, super.key});
/// The color to paint the background area with.
final Color color;
/// {@template flutter.widgets.ColoredBox.isAntiAlias}
/// Whether to apply anti-aliasing when painting the box.
///
/// Defaults to `true`.
///
/// When `true`, the painted box will have smooth edges. This is crucial for
/// animations and transformations (such as rotation or scaling) where the
/// widget's edges may not align perfectly with the physical pixel grid.
/// Anti-aliasing allows for sub-pixel rendering, which prevents a 'jagged'
/// appearance during motion and ensures visually smooth transitions.
///
/// Set this to `false` for specific use cases where multiple `ColoredBox`
/// widgets are positioned adjacent to each other to form a larger, seamless
/// area of solid color. With anti-aliasing enabled (`true`), faint seams or
/// gaps might appear between the boxes due to the semi-transparent pixels at
/// their edges. Disabling anti-aliasing ensures that the boxes align perfectly
/// without such visual artifacts.
///
/// See also:
///
/// * [Paint.isAntiAlias], the underlying property that this controls.
/// {@endtemplate}
final bool isAntiAlias;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderColoredBox(color: color);
return _RenderColoredBox(color: color, isAntiAlias: isAntiAlias);
}
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) {
(renderObject as _RenderColoredBox).color = color;
(renderObject as _RenderColoredBox)
..color = color
..isAntiAlias = isAntiAlias;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Color>('color', color));
properties.add(DiagnosticsProperty<bool>('isAntiAlias', isAntiAlias, defaultValue: true));
}
}
class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
_RenderColoredBox({required Color color})
_RenderColoredBox({required Color color, required bool isAntiAlias})
: _color = color,
_isAntiAlias = isAntiAlias,
super(behavior: HitTestBehavior.opaque);
/// The fill color for this render object.
@ -8438,6 +8466,16 @@ class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
markNeedsPaint();
}
bool get isAntiAlias => _isAntiAlias;
bool _isAntiAlias;
set isAntiAlias(bool value) {
if (value == _isAntiAlias) {
return;
}
_isAntiAlias = value;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
// It's tempting to want to optimize out this `drawRect()` call if the
@ -8445,7 +8483,12 @@ class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
// https://github.com/flutter/flutter/pull/72526#issuecomment-749185938 for
// a good description of why.
if (size > Size.zero) {
context.canvas.drawRect(offset & size, Paint()..color = color);
context.canvas.drawRect(
offset & size,
Paint()
..isAntiAlias = isAntiAlias
..color = color,
);
}
if (child != null) {
context.paintChild(child!, offset);

View File

@ -257,6 +257,7 @@ class Container extends StatelessWidget {
this.alignment,
this.padding,
this.color,
this.isAntiAlias = true,
this.decoration,
this.foregroundDecoration,
double? width,
@ -326,6 +327,9 @@ class Container extends StatelessWidget {
/// null.
final Color? color;
/// {@macro flutter.widgets.ColoredBox.isAntiAlias}
final bool isAntiAlias;
/// The decoration to paint behind the [child].
///
/// Use the [color] property to specify a simple solid color.
@ -398,7 +402,7 @@ class Container extends StatelessWidget {
}
if (color != null) {
current = ColoredBox(color: color!, child: current);
current = ColoredBox(color: color!, isAntiAlias: isAntiAlias, child: current);
}
if (clipBehavior != Clip.none) {

View File

@ -1220,6 +1220,214 @@ void main() {
expect(properties.properties.first.value, colorToPaint);
});
testWidgets('ColoredBox - default isAntiAlias', (WidgetTester tester) async {
await tester.pumpWidget(const ColoredBox(color: colorToPaint));
expect(find.byType(ColoredBox), findsOneWidget);
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
renderColoredBox.paint(mockContext, Offset.zero);
expect(mockCanvas.paints.single.isAntiAlias, isTrue);
});
testWidgets('ColoredBox - passing isAntiAlias = false', (WidgetTester tester) async {
await tester.pumpWidget(const ColoredBox(color: colorToPaint, isAntiAlias: false));
expect(find.byType(ColoredBox), findsOneWidget);
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
renderColoredBox.paint(mockContext, Offset.zero);
expect(mockCanvas.paints.single.isAntiAlias, isFalse);
});
// This test verifies how `ColoredBox.isAntiAlias` affects rendering.
// The first row uses `isAntiAlias: true`, showing gaps between the white backgrounds.
// The second row uses `isAntiAlias: false`, demonstrating no gaps between the white backgrounds.
// The third row contains three tilted boxes with `isAntiAlias` set to true, false, and false, respectively.
testWidgets('ColoredBox golden test - anti-aliasing and rotation variations', (
WidgetTester tester,
) async {
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: RepaintBoundary(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: <Widget>[
// Intentionally 4% larger than the original size to test anti-aliasing
Transform.scale(
scale: 1.04,
child: const ColoredBox(
color: Colors.orange,
child: Padding(
padding: EdgeInsets.all(2),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
ColoredBox(
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
'Short',
style: TextStyle(fontSize: 16, color: Colors.black),
),
),
),
ColoredBox(
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
'Just text ',
style: TextStyle(fontSize: 14, color: Colors.black),
),
),
),
ColoredBox(
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
' Tall text ',
style: TextStyle(fontSize: 18, color: Colors.black),
),
),
),
ColoredBox(
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
'Medium',
style: TextStyle(fontSize: 32, color: Colors.black),
),
),
),
],
),
),
),
),
Transform.scale(
scale: 1.04,
child: const ColoredBox(
color: Colors.orange,
isAntiAlias: false,
child: Padding(
padding: EdgeInsets.all(2),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
ColoredBox(
color: Colors.white,
isAntiAlias: false,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
'Short',
style: TextStyle(fontSize: 16, color: Colors.black),
),
),
),
ColoredBox(
color: Colors.white,
isAntiAlias: false,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
'Just text ',
style: TextStyle(fontSize: 14, color: Colors.black),
),
),
),
ColoredBox(
color: Colors.white,
isAntiAlias: false,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
' Tall text ',
style: TextStyle(fontSize: 18, color: Colors.black),
),
),
),
ColoredBox(
color: Colors.white,
isAntiAlias: false,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
'Medium',
style: TextStyle(fontSize: 32, color: Colors.black),
),
),
),
],
),
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox.square(
dimension: 80,
child: Center(
child: SizedBox.square(
dimension: 50,
child: Transform.rotate(
angle: math.pi / 5,
child: const ColoredBox(color: Colors.blue),
),
),
),
),
SizedBox.square(
dimension: 80,
child: Center(
child: SizedBox.square(
dimension: 50,
child: Transform.rotate(
angle: math.pi / 5,
child: const ColoredBox(color: Colors.amber, isAntiAlias: false),
),
),
),
),
SizedBox.square(
dimension: 80,
child: Center(
child: SizedBox.square(
dimension: 50,
child: Transform.rotate(
angle: math.pi / 5,
child: Transform.scale(
scale: 1.2,
child: const ColoredBox(color: Colors.teal, isAntiAlias: false),
),
),
),
),
),
],
),
],
),
),
),
),
),
);
await tester.pumpAndSettle();
await expectLater(find.byType(RepaintBoundary), matchesGoldenFile('basic.ColoredBox.0.png'));
});
});
testWidgets('Inconsequential golden test', (WidgetTester tester) async {