From 7efa2aed3c01529f45f752d1e5ee6fdf2c135518 Mon Sep 17 00:00:00 2001 From: Bui Dai Duong <62325868+definev@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:59:19 +0700 Subject: [PATCH] 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. [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 --- packages/flutter/lib/src/widgets/basic.dart | 53 ++++- .../flutter/lib/src/widgets/container.dart | 6 +- packages/flutter/test/widgets/basic_test.dart | 208 ++++++++++++++++++ 3 files changed, 261 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 5f3c3c12b92..ca91bc34c72 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -8400,31 +8400,59 @@ class _StatefulBuilderState extends State { /// 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)); + properties.add(DiagnosticsProperty('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); diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index e1c9139d898..d9a1154c7ea 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -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) { diff --git a/packages/flutter/test/widgets/basic_test.dart b/packages/flutter/test/widgets/basic_test.dart index a6f6eed6871..9aa0a01b2cb 100644 --- a/packages/flutter/test/widgets/basic_test.dart +++ b/packages/flutter/test/widgets/basic_test.dart @@ -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: [ + // 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: [ + 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: [ + 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: [ + 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 {