Skip to content

Commit 7efa2ae

Browse files
definevvictorsanni
andauthored
Colored box optimization (flutter#176028) (flutter#176073)
This PR fixed visual bug when placing multiple `ColoredBox`s together. (Fixes flutter#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>
1 parent 02d6e8f commit 7efa2ae

File tree

3 files changed

+261
-6
lines changed

3 files changed

+261
-6
lines changed

packages/flutter/lib/src/widgets/basic.dart

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8400,31 +8400,59 @@ class _StatefulBuilderState extends State<StatefulBuilder> {
84008400
/// child on top of that color.
84018401
class ColoredBox extends SingleChildRenderObjectWidget {
84028402
/// Creates a widget that paints its area with the specified [Color].
8403-
const ColoredBox({required this.color, super.child, super.key});
8403+
const ColoredBox({required this.color, this.isAntiAlias = true, super.child, super.key});
84048404

84058405
/// The color to paint the background area with.
84068406
final Color color;
84078407

8408+
/// {@template flutter.widgets.ColoredBox.isAntiAlias}
8409+
/// Whether to apply anti-aliasing when painting the box.
8410+
///
8411+
/// Defaults to `true`.
8412+
///
8413+
/// When `true`, the painted box will have smooth edges. This is crucial for
8414+
/// animations and transformations (such as rotation or scaling) where the
8415+
/// widget's edges may not align perfectly with the physical pixel grid.
8416+
/// Anti-aliasing allows for sub-pixel rendering, which prevents a 'jagged'
8417+
/// appearance during motion and ensures visually smooth transitions.
8418+
///
8419+
/// Set this to `false` for specific use cases where multiple `ColoredBox`
8420+
/// widgets are positioned adjacent to each other to form a larger, seamless
8421+
/// area of solid color. With anti-aliasing enabled (`true`), faint seams or
8422+
/// gaps might appear between the boxes due to the semi-transparent pixels at
8423+
/// their edges. Disabling anti-aliasing ensures that the boxes align perfectly
8424+
/// without such visual artifacts.
8425+
///
8426+
/// See also:
8427+
///
8428+
/// * [Paint.isAntiAlias], the underlying property that this controls.
8429+
/// {@endtemplate}
8430+
final bool isAntiAlias;
8431+
84088432
@override
84098433
RenderObject createRenderObject(BuildContext context) {
8410-
return _RenderColoredBox(color: color);
8434+
return _RenderColoredBox(color: color, isAntiAlias: isAntiAlias);
84118435
}
84128436

84138437
@override
84148438
void updateRenderObject(BuildContext context, RenderObject renderObject) {
8415-
(renderObject as _RenderColoredBox).color = color;
8439+
(renderObject as _RenderColoredBox)
8440+
..color = color
8441+
..isAntiAlias = isAntiAlias;
84168442
}
84178443

84188444
@override
84198445
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
84208446
super.debugFillProperties(properties);
84218447
properties.add(DiagnosticsProperty<Color>('color', color));
8448+
properties.add(DiagnosticsProperty<bool>('isAntiAlias', isAntiAlias, defaultValue: true));
84228449
}
84238450
}
84248451

84258452
class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
8426-
_RenderColoredBox({required Color color})
8453+
_RenderColoredBox({required Color color, required bool isAntiAlias})
84278454
: _color = color,
8455+
_isAntiAlias = isAntiAlias,
84288456
super(behavior: HitTestBehavior.opaque);
84298457

84308458
/// The fill color for this render object.
@@ -8438,14 +8466,29 @@ class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
84388466
markNeedsPaint();
84398467
}
84408468

8469+
bool get isAntiAlias => _isAntiAlias;
8470+
bool _isAntiAlias;
8471+
set isAntiAlias(bool value) {
8472+
if (value == _isAntiAlias) {
8473+
return;
8474+
}
8475+
_isAntiAlias = value;
8476+
markNeedsPaint();
8477+
}
8478+
84418479
@override
84428480
void paint(PaintingContext context, Offset offset) {
84438481
// It's tempting to want to optimize out this `drawRect()` call if the
84448482
// color is transparent (alpha==0), but doing so would be incorrect. See
84458483
// https://github.com/flutter/flutter/pull/72526#issuecomment-749185938 for
84468484
// a good description of why.
84478485
if (size > Size.zero) {
8448-
context.canvas.drawRect(offset & size, Paint()..color = color);
8486+
context.canvas.drawRect(
8487+
offset & size,
8488+
Paint()
8489+
..isAntiAlias = isAntiAlias
8490+
..color = color,
8491+
);
84498492
}
84508493
if (child != null) {
84518494
context.paintChild(child!, offset);

packages/flutter/lib/src/widgets/container.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ class Container extends StatelessWidget {
257257
this.alignment,
258258
this.padding,
259259
this.color,
260+
this.isAntiAlias = true,
260261
this.decoration,
261262
this.foregroundDecoration,
262263
double? width,
@@ -326,6 +327,9 @@ class Container extends StatelessWidget {
326327
/// null.
327328
final Color? color;
328329

330+
/// {@macro flutter.widgets.ColoredBox.isAntiAlias}
331+
final bool isAntiAlias;
332+
329333
/// The decoration to paint behind the [child].
330334
///
331335
/// Use the [color] property to specify a simple solid color.
@@ -398,7 +402,7 @@ class Container extends StatelessWidget {
398402
}
399403

400404
if (color != null) {
401-
current = ColoredBox(color: color!, child: current);
405+
current = ColoredBox(color: color!, isAntiAlias: isAntiAlias, child: current);
402406
}
403407

404408
if (clipBehavior != Clip.none) {

packages/flutter/test/widgets/basic_test.dart

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,214 @@ void main() {
12201220

12211221
expect(properties.properties.first.value, colorToPaint);
12221222
});
1223+
1224+
testWidgets('ColoredBox - default isAntiAlias', (WidgetTester tester) async {
1225+
await tester.pumpWidget(const ColoredBox(color: colorToPaint));
1226+
expect(find.byType(ColoredBox), findsOneWidget);
1227+
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
1228+
1229+
renderColoredBox.paint(mockContext, Offset.zero);
1230+
expect(mockCanvas.paints.single.isAntiAlias, isTrue);
1231+
});
1232+
1233+
testWidgets('ColoredBox - passing isAntiAlias = false', (WidgetTester tester) async {
1234+
await tester.pumpWidget(const ColoredBox(color: colorToPaint, isAntiAlias: false));
1235+
expect(find.byType(ColoredBox), findsOneWidget);
1236+
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
1237+
1238+
renderColoredBox.paint(mockContext, Offset.zero);
1239+
expect(mockCanvas.paints.single.isAntiAlias, isFalse);
1240+
});
1241+
1242+
// This test verifies how `ColoredBox.isAntiAlias` affects rendering.
1243+
// The first row uses `isAntiAlias: true`, showing gaps between the white backgrounds.
1244+
// The second row uses `isAntiAlias: false`, demonstrating no gaps between the white backgrounds.
1245+
// The third row contains three tilted boxes with `isAntiAlias` set to true, false, and false, respectively.
1246+
testWidgets('ColoredBox golden test - anti-aliasing and rotation variations', (
1247+
WidgetTester tester,
1248+
) async {
1249+
await tester.pumpWidget(
1250+
Center(
1251+
child: Directionality(
1252+
textDirection: TextDirection.ltr,
1253+
child: RepaintBoundary(
1254+
child: Padding(
1255+
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
1256+
child: Column(
1257+
mainAxisSize: MainAxisSize.min,
1258+
spacing: 8,
1259+
children: <Widget>[
1260+
// Intentionally 4% larger than the original size to test anti-aliasing
1261+
Transform.scale(
1262+
scale: 1.04,
1263+
child: const ColoredBox(
1264+
color: Colors.orange,
1265+
child: Padding(
1266+
padding: EdgeInsets.all(2),
1267+
child: Row(
1268+
mainAxisSize: MainAxisSize.min,
1269+
crossAxisAlignment: CrossAxisAlignment.end,
1270+
children: <Widget>[
1271+
ColoredBox(
1272+
color: Colors.white,
1273+
child: Padding(
1274+
padding: EdgeInsets.all(4.0),
1275+
child: Text(
1276+
'Short',
1277+
style: TextStyle(fontSize: 16, color: Colors.black),
1278+
),
1279+
),
1280+
),
1281+
ColoredBox(
1282+
color: Colors.white,
1283+
child: Padding(
1284+
padding: EdgeInsets.all(4.0),
1285+
child: Text(
1286+
'Just text ',
1287+
style: TextStyle(fontSize: 14, color: Colors.black),
1288+
),
1289+
),
1290+
),
1291+
ColoredBox(
1292+
color: Colors.white,
1293+
child: Padding(
1294+
padding: EdgeInsets.all(4.0),
1295+
child: Text(
1296+
' Tall text ',
1297+
style: TextStyle(fontSize: 18, color: Colors.black),
1298+
),
1299+
),
1300+
),
1301+
ColoredBox(
1302+
color: Colors.white,
1303+
child: Padding(
1304+
padding: EdgeInsets.all(4.0),
1305+
child: Text(
1306+
'Medium',
1307+
style: TextStyle(fontSize: 32, color: Colors.black),
1308+
),
1309+
),
1310+
),
1311+
],
1312+
),
1313+
),
1314+
),
1315+
),
1316+
Transform.scale(
1317+
scale: 1.04,
1318+
child: const ColoredBox(
1319+
color: Colors.orange,
1320+
isAntiAlias: false,
1321+
child: Padding(
1322+
padding: EdgeInsets.all(2),
1323+
child: Row(
1324+
mainAxisSize: MainAxisSize.min,
1325+
crossAxisAlignment: CrossAxisAlignment.end,
1326+
children: <Widget>[
1327+
ColoredBox(
1328+
color: Colors.white,
1329+
isAntiAlias: false,
1330+
child: Padding(
1331+
padding: EdgeInsets.all(4.0),
1332+
child: Text(
1333+
'Short',
1334+
style: TextStyle(fontSize: 16, color: Colors.black),
1335+
),
1336+
),
1337+
),
1338+
ColoredBox(
1339+
color: Colors.white,
1340+
isAntiAlias: false,
1341+
child: Padding(
1342+
padding: EdgeInsets.all(4.0),
1343+
child: Text(
1344+
'Just text ',
1345+
style: TextStyle(fontSize: 14, color: Colors.black),
1346+
),
1347+
),
1348+
),
1349+
ColoredBox(
1350+
color: Colors.white,
1351+
isAntiAlias: false,
1352+
child: Padding(
1353+
padding: EdgeInsets.all(4.0),
1354+
child: Text(
1355+
' Tall text ',
1356+
style: TextStyle(fontSize: 18, color: Colors.black),
1357+
),
1358+
),
1359+
),
1360+
ColoredBox(
1361+
color: Colors.white,
1362+
isAntiAlias: false,
1363+
child: Padding(
1364+
padding: EdgeInsets.all(4.0),
1365+
child: Text(
1366+
'Medium',
1367+
style: TextStyle(fontSize: 32, color: Colors.black),
1368+
),
1369+
),
1370+
),
1371+
],
1372+
),
1373+
),
1374+
),
1375+
),
1376+
Row(
1377+
mainAxisSize: MainAxisSize.min,
1378+
children: <Widget>[
1379+
SizedBox.square(
1380+
dimension: 80,
1381+
child: Center(
1382+
child: SizedBox.square(
1383+
dimension: 50,
1384+
child: Transform.rotate(
1385+
angle: math.pi / 5,
1386+
child: const ColoredBox(color: Colors.blue),
1387+
),
1388+
),
1389+
),
1390+
),
1391+
SizedBox.square(
1392+
dimension: 80,
1393+
child: Center(
1394+
child: SizedBox.square(
1395+
dimension: 50,
1396+
child: Transform.rotate(
1397+
angle: math.pi / 5,
1398+
child: const ColoredBox(color: Colors.amber, isAntiAlias: false),
1399+
),
1400+
),
1401+
),
1402+
),
1403+
SizedBox.square(
1404+
dimension: 80,
1405+
child: Center(
1406+
child: SizedBox.square(
1407+
dimension: 50,
1408+
child: Transform.rotate(
1409+
angle: math.pi / 5,
1410+
child: Transform.scale(
1411+
scale: 1.2,
1412+
child: const ColoredBox(color: Colors.teal, isAntiAlias: false),
1413+
),
1414+
),
1415+
),
1416+
),
1417+
),
1418+
],
1419+
),
1420+
],
1421+
),
1422+
),
1423+
),
1424+
),
1425+
),
1426+
);
1427+
1428+
await tester.pumpAndSettle();
1429+
await expectLater(find.byType(RepaintBoundary), matchesGoldenFile('basic.ColoredBox.0.png'));
1430+
});
12231431
});
12241432

12251433
testWidgets('Inconsequential golden test', (WidgetTester tester) async {

0 commit comments

Comments
 (0)