Unverified Commit 444b13b8 authored by Dan Field's avatar Dan Field Committed by GitHub

Colored box and container optimization (#50979)

parent c75db983
...@@ -7071,3 +7071,57 @@ class _StatefulBuilderState extends State<StatefulBuilder> { ...@@ -7071,3 +7071,57 @@ class _StatefulBuilderState extends State<StatefulBuilder> {
@override @override
Widget build(BuildContext context) => widget.builder(context, setState); Widget build(BuildContext context) => widget.builder(context, setState);
} }
/// A widget that paints its area with a specified [Color] and then draws its
/// child on top of that color.
class ColoredBox extends SingleChildRenderObjectWidget {
/// Creates a widget that paints its area with the specified [Color].
///
/// The [color] parameter must not be null.
const ColoredBox({@required this.color, Widget child, Key key})
: assert(color != null),
super(key: key, child: child);
/// The color to paint the background area with.
final Color color;
@override
_RenderColoredBox createRenderObject(BuildContext context) {
return _RenderColoredBox(color: color);
}
@override
void updateRenderObject(BuildContext context, _RenderColoredBox renderObject) {
renderObject.color = color;
}
}
class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
_RenderColoredBox({@required Color color})
: _color = color,
super(behavior: HitTestBehavior.opaque);
/// The fill color for this render object.
///
/// This parameter must not be null.
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (value == _color) {
return;
}
_color = value;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
if (size > Size.zero) {
context.canvas.drawRect(offset & size, Paint()..color = color);
}
if (child != null) {
context.paintChild(child, offset);
}
}
}
...@@ -294,17 +294,16 @@ class Container extends StatelessWidget { ...@@ -294,17 +294,16 @@ class Container extends StatelessWidget {
/// ///
/// The `height` and `width` values include the padding. /// The `height` and `width` values include the padding.
/// ///
/// The `color` argument is a shorthand for `decoration: new /// The `color` and `decoration` arguments cannot both be supplied, since
/// BoxDecoration(color: color)`, which means you cannot supply both a `color` /// it would potentially result in the decoration drawing over the background
/// and a `decoration` argument. If you want to have both a `color` and a /// color. To supply a decoration with a color, use `decoration:
/// `decoration`, you can pass the color as the `color` argument to the /// BoxDecoration(color: color)`.
/// `BoxDecoration`.
Container({ Container({
Key key, Key key,
this.alignment, this.alignment,
this.padding, this.padding,
Color color, this.color,
Decoration decoration, this.decoration,
this.foregroundDecoration, this.foregroundDecoration,
double width, double width,
double height, double height,
...@@ -320,9 +319,8 @@ class Container extends StatelessWidget { ...@@ -320,9 +319,8 @@ class Container extends StatelessWidget {
assert(clipBehavior != null), assert(clipBehavior != null),
assert(color == null || decoration == null, assert(color == null || decoration == null,
'Cannot provide both a color and a decoration\n' 'Cannot provide both a color and a decoration\n'
'The color argument is just a shorthand for "decoration: new BoxDecoration(color: color)".' 'To provide both, use "decoration: BoxDecoration(color: color)".'
), ),
decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null),
constraints = constraints =
(width != null || height != null) (width != null || height != null)
? constraints?.tighten(width: width, height: height) ? constraints?.tighten(width: width, height: height)
...@@ -363,11 +361,20 @@ class Container extends StatelessWidget { ...@@ -363,11 +361,20 @@ class Container extends StatelessWidget {
/// see [Decoration.padding]. /// see [Decoration.padding].
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
/// The color to paint behind the [child].
///
/// This property should be preferred when the background is a simple color.
/// For other cases, such as gradients or images, use the [decoration]
/// property.
///
/// If the [decoration] is used, this property must be null. A background
/// color may still be painted by the [decoration] even if this property is
/// null.
final Color color;
/// The decoration to paint behind the [child]. /// The decoration to paint behind the [child].
/// ///
/// A shorthand for specifying just a solid color is available in the /// Use the [color] property to specify a simple solid color.
/// constructor: set the `color` argument instead of the `decoration`
/// argument.
/// ///
/// The [child] is not clipped to the decoration. To clip a child to the shape /// The [child] is not clipped to the decoration. To clip a child to the shape
/// of a particular [ShapeDecoration], consider using a [ClipPath] widget. /// of a particular [ShapeDecoration], consider using a [ClipPath] widget.
...@@ -423,6 +430,9 @@ class Container extends StatelessWidget { ...@@ -423,6 +430,9 @@ class Container extends StatelessWidget {
if (effectivePadding != null) if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current); current = Padding(padding: effectivePadding, child: current);
if (color != null)
current = ColoredBox(color: color, child: current);
if (decoration != null) if (decoration != null)
current = DecoratedBox(decoration: decoration, child: current); current = DecoratedBox(decoration: decoration, child: current);
......
...@@ -25,7 +25,7 @@ void main() { ...@@ -25,7 +25,7 @@ void main() {
); );
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromBanner(tester);
expect(container.decoration, const BoxDecoration(color: color)); expect(container.color, color);
}); });
testWidgets('Custom content TextStyle respected', (WidgetTester tester) async { testWidgets('Custom content TextStyle respected', (WidgetTester tester) async {
......
...@@ -66,7 +66,7 @@ void main() { ...@@ -66,7 +66,7 @@ void main() {
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, const BoxDecoration(color: Color(0xffffffff))); expect(container.color, const Color(0xffffffff));
// Default value for ThemeData.typography is Typography.material2014() // Default value for ThemeData.typography is Typography.material2014()
expect(content.text.style, Typography.material2014().englishLike.bodyText2.merge(Typography.material2014().black.bodyText2)); expect(content.text.style, Typography.material2014().englishLike.bodyText2.merge(Typography.material2014().black.bodyText2));
}); });
...@@ -92,7 +92,7 @@ void main() { ...@@ -92,7 +92,7 @@ void main() {
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, BoxDecoration(color: bannerTheme.backgroundColor)); expect(container.color, bannerTheme.backgroundColor);
expect(content.text.style, bannerTheme.contentTextStyle); expect(content.text.style, bannerTheme.contentTextStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText)); final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
...@@ -131,7 +131,7 @@ void main() { ...@@ -131,7 +131,7 @@ void main() {
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, const BoxDecoration(color: backgroundColor)); expect(container.color, backgroundColor);
expect(content.text.style, textStyle); expect(content.text.style, textStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText)); final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
...@@ -161,7 +161,7 @@ void main() { ...@@ -161,7 +161,7 @@ void main() {
)); ));
final Container container = _getContainerFromBanner(tester); final Container container = _getContainerFromBanner(tester);
expect(container.decoration, BoxDecoration(color: colorScheme.surface)); expect(container.color, colorScheme.surface);
}); });
} }
......
...@@ -148,9 +148,7 @@ void main() { ...@@ -148,9 +148,7 @@ void main() {
scaffoldKey.currentState.openDrawer(); scaffoldKey.currentState.openDrawer();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
BoxDecoration decoration = getScrim().decoration as BoxDecoration; expect(getScrim().color, Colors.black54);
expect(decoration.color, Colors.black54);
expect(decoration.shape, BoxShape.rectangle);
await tester.tap(find.byType(Drawer)); await tester.tap(find.byType(Drawer));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -162,9 +160,7 @@ void main() { ...@@ -162,9 +160,7 @@ void main() {
scaffoldKey.currentState.openDrawer(); scaffoldKey.currentState.openDrawer();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
decoration = getScrim().decoration as BoxDecoration; expect(getScrim().color, const Color(0xFF323232));
expect(decoration.color, const Color(0xFF323232));
expect(decoration.shape, BoxShape.rectangle);
await tester.tap(find.byType(Drawer)); await tester.tap(find.byType(Drawer));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
......
...@@ -73,8 +73,7 @@ void main() { ...@@ -73,8 +73,7 @@ void main() {
} }
Color containerColor() { Color containerColor() {
final BoxDecoration decoration = tester.widget<Container>(find.byKey(primaryContainerKey)).decoration as BoxDecoration; return tester.widget<Container>(find.byKey(primaryContainerKey)).color;
return decoration.color;
} }
await tester.pumpWidget(buildFrame()); await tester.pumpWidget(buildFrame());
...@@ -225,10 +224,9 @@ void main() { ...@@ -225,10 +224,9 @@ void main() {
} }
Color bannerColor() { Color bannerColor() {
final BoxDecoration decoration = tester.widget<Container>( return tester.widget<Container>(
find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first, find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first,
).decoration as BoxDecoration; ).color;
return decoration.color;
} }
TextStyle getTextStyle(String text) { TextStyle getTextStyle(String text) {
......
...@@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:mockito/mockito.dart';
void main() { void main() {
group('PhysicalShape', () { group('PhysicalShape', () {
...@@ -278,6 +279,90 @@ void main() { ...@@ -278,6 +279,90 @@ void main() {
equals('UnconstrainedBox(alignment: topRight, constrainedAxis: horizontal, textDirection: rtl)'), equals('UnconstrainedBox(alignment: topRight, constrainedAxis: horizontal, textDirection: rtl)'),
); );
}); });
group('ColoredBox', () {
_MockCanvas mockCanvas;
_MockPaintingContext mockContext;
const Color colorToPaint = Color(0xFFABCDEF);
setUp(() {
mockContext = _MockPaintingContext();
mockCanvas = _MockCanvas();
when(mockContext.canvas).thenReturn(mockCanvas);
});
testWidgets('ColoredBox - no size, no child', (WidgetTester tester) async {
await tester.pumpWidget(Flex(
direction: Axis.horizontal,
textDirection: TextDirection.ltr,
children: const <Widget>[
SizedBox.shrink(
child: ColoredBox(color: colorToPaint),
),
],
));
expect(find.byType(ColoredBox), findsOneWidget);
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
renderColoredBox.paint(mockContext, Offset.zero);
verifyNever(mockCanvas.drawRect(any, any));
verifyNever(mockContext.paintChild(any, any));
});
testWidgets('ColoredBox - no size, child', (WidgetTester tester) async {
const ValueKey<int> key = ValueKey<int>(0);
const Widget child = SizedBox.expand(key: key);
await tester.pumpWidget(Flex(
direction: Axis.horizontal,
textDirection: TextDirection.ltr,
children: const <Widget>[
SizedBox.shrink(
child: ColoredBox(color: colorToPaint, child: child),
),
],
));
expect(find.byType(ColoredBox), findsOneWidget);
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
final RenderObject renderSizedBox = tester.renderObject(find.byKey(key));
renderColoredBox.paint(mockContext, Offset.zero);
verifyNever(mockCanvas.drawRect(any, any));
verify(mockContext.paintChild(renderSizedBox, Offset.zero)).called(1);
});
testWidgets('ColoredBox - size, no child', (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);
final List<dynamic> drawRect = verify(mockCanvas.drawRect(captureAny, captureAny)).captured;
expect(drawRect.length, 2);
expect(drawRect[0], const Rect.fromLTWH(0, 0, 800, 600));
expect(drawRect[1].color, colorToPaint);
verifyNever(mockContext.paintChild(any, any));
});
testWidgets('ColoredBox - size, child', (WidgetTester tester) async {
const ValueKey<int> key = ValueKey<int>(0);
const Widget child = SizedBox.expand(key: key);
await tester.pumpWidget(const ColoredBox(color: colorToPaint, child: child));
expect(find.byType(ColoredBox), findsOneWidget);
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
final RenderObject renderSizedBox = tester.renderObject(find.byKey(key));
renderColoredBox.paint(mockContext, Offset.zero);
final List<dynamic> drawRect = verify(mockCanvas.drawRect(captureAny, captureAny)).captured;
expect(drawRect.length, 2);
expect(drawRect[0], const Rect.fromLTWH(0, 0, 800, 600));
expect(drawRect[1].color, colorToPaint);
verify(mockContext.paintChild(renderSizedBox, Offset.zero)).called(1);
});
});
} }
HitsRenderBox hits(RenderBox renderBox) => HitsRenderBox(renderBox); HitsRenderBox hits(RenderBox renderBox) => HitsRenderBox(renderBox);
...@@ -319,3 +404,6 @@ class DoesNotHitRenderBox extends Matcher { ...@@ -319,3 +404,6 @@ class DoesNotHitRenderBox extends Matcher {
).isEmpty; ).isEmpty;
} }
} }
class _MockPaintingContext extends Mock implements PaintingContext {}
class _MockCanvas extends Mock implements Canvas {}
...@@ -259,9 +259,8 @@ void main() { ...@@ -259,9 +259,8 @@ void main() {
), ),
); );
DecoratedBox widget = tester.firstWidget(find.byType(DecoratedBox)); Container widget = tester.firstWidget(find.byType(Container));
BoxDecoration decoration = widget.decoration as BoxDecoration; expect(widget.color, equals(Colors.blue));
expect(decoration.color, equals(Colors.blue));
setState(() { setState(() {
themeData = ThemeData(primarySwatch: Colors.green); themeData = ThemeData(primarySwatch: Colors.green);
...@@ -269,9 +268,8 @@ void main() { ...@@ -269,9 +268,8 @@ void main() {
await tester.pump(); await tester.pump();
widget = tester.firstWidget(find.byType(DecoratedBox)); widget = tester.firstWidget(find.byType(Container));
decoration = widget.decoration as BoxDecoration; expect(widget.color, equals(Colors.green));
expect(decoration.color, equals(Colors.green));
}); });
testWidgets('ListView padding', (WidgetTester tester) async { testWidgets('ListView padding', (WidgetTester tester) async {
......
...@@ -495,7 +495,9 @@ void main() { ...@@ -495,7 +495,9 @@ void main() {
SliverFixedExtentList( SliverFixedExtentList(
itemExtent: 150, itemExtent: 150,
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Container(color: Colors.amber), (BuildContext context, int index) {
return Semantics(label: index.toString(), child: Container(color: Colors.amber));
},
childCount: 5, childCount: 5,
), ),
), ),
...@@ -510,8 +512,12 @@ void main() { ...@@ -510,8 +512,12 @@ void main() {
]; ];
await tester.pumpWidget(boilerplate(slivers, controller: controller)); await tester.pumpWidget(boilerplate(slivers, controller: controller));
const BoxDecoration amberBox = BoxDecoration(color: Colors.amber);
const BoxDecoration blueBox = BoxDecoration(color: Colors.blue); expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsNothing,
);
// Scroll to bottom // Scroll to bottom
controller.jumpTo(controller.position.maxScrollExtent); controller.jumpTo(controller.position.maxScrollExtent);
...@@ -520,8 +526,8 @@ void main() { ...@@ -520,8 +526,8 @@ void main() {
// Check item at the end of the list // Check item at the end of the list
expect(find.byKey(key), findsNothing); expect(find.byKey(key), findsNothing);
expect( expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration, find.bySemanticsLabel('4'),
amberBox, findsOneWidget,
); );
// Overscroll // Overscroll
...@@ -531,16 +537,16 @@ void main() { ...@@ -531,16 +537,16 @@ void main() {
// Check for new item at the end of the now overscrolled list // Check for new item at the end of the now overscrolled list
expect(find.byKey(key), findsOneWidget); expect(find.byKey(key), findsOneWidget);
expect( expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration, find.bySemanticsLabel('4'),
blueBox, findsOneWidget,
); );
// Ensure overscroll retracts to original size after releasing gesture // Ensure overscroll retracts to original size after releasing gesture
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(key), findsNothing); expect(find.byKey(key), findsNothing);
expect( expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration, find.bySemanticsLabel('4'),
amberBox, findsOneWidget,
); );
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
...@@ -758,7 +764,9 @@ void main() { ...@@ -758,7 +764,9 @@ void main() {
SliverFixedExtentList( SliverFixedExtentList(
itemExtent: 150, itemExtent: 150,
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Container(color: Colors.amber), (BuildContext context, int index) {
return Semantics(label: index.toString(), child: Container(color: Colors.amber));
},
childCount: 5, childCount: 5,
), ),
), ),
...@@ -773,7 +781,12 @@ void main() { ...@@ -773,7 +781,12 @@ void main() {
]; ];
await tester.pumpWidget(boilerplate(slivers, controller: controller)); await tester.pumpWidget(boilerplate(slivers, controller: controller));
const BoxDecoration amberBox = BoxDecoration(color: Colors.amber);
expect(find.byKey(key), findsNothing);
expect(
find.bySemanticsLabel('4'),
findsNothing,
);
// Scroll to bottom // Scroll to bottom
controller.jumpTo(controller.position.maxScrollExtent); controller.jumpTo(controller.position.maxScrollExtent);
...@@ -782,8 +795,8 @@ void main() { ...@@ -782,8 +795,8 @@ void main() {
// End of list // End of list
expect(find.byKey(key), findsNothing); expect(find.byKey(key), findsNothing);
expect( expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration, find.bySemanticsLabel('4'),
amberBox, findsOneWidget,
); );
// Overscroll // Overscroll
...@@ -792,8 +805,8 @@ void main() { ...@@ -792,8 +805,8 @@ void main() {
expect(find.byKey(key), findsNothing); expect(find.byKey(key), findsNothing);
expect( expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration, find.bySemanticsLabel('4'),
amberBox, findsOneWidget,
); );
}); });
}); });
......
...@@ -225,8 +225,8 @@ void main() { ...@@ -225,8 +225,8 @@ void main() {
await tester.pump(); await tester.pump();
// Screen is 600px high. Moved bottom item 500px up. It's now at the top. // Screen is 600px high. Moved bottom item 500px up. It's now at the top.
expect(tester.getTopLeft(find.widgetWithText(DecoratedBox, '5')).dy, 0.0); expect(tester.getTopLeft(find.widgetWithText(Container, '5')).dy, 0.0);
expect(tester.getBottomLeft(find.widgetWithText(DecoratedBox, '10')).dy, 600.0); expect(tester.getBottomLeft(find.widgetWithText(Container, '10')).dy, 600.0);
// Stop returning the first 3 items. // Stop returning the first 3 items.
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
...@@ -258,10 +258,10 @@ void main() { ...@@ -258,10 +258,10 @@ void main() {
// Move up by 4 items, meaning item 1 would have been at the top but // Move up by 4 items, meaning item 1 would have been at the top but
// 0 through 3 no longer exist, so item 4, 3 items down, is the first one. // 0 through 3 no longer exist, so item 4, 3 items down, is the first one.
// Item 4 is also shifted to the top. // Item 4 is also shifted to the top.
expect(tester.getTopLeft(find.widgetWithText(DecoratedBox, '4')).dy, 0.0); expect(tester.getTopLeft(find.widgetWithText(Container, '4')).dy, 0.0);
// Because the screen is still 600px, item 9 is now visible at the bottom instead // Because the screen is still 600px, item 9 is now visible at the bottom instead
// of what's supposed to be item 6 had we not re-shifted. // of what's supposed to be item 6 had we not re-shifted.
expect(tester.getBottomLeft(find.widgetWithText(DecoratedBox, '9')).dy, 600.0); expect(tester.getBottomLeft(find.widgetWithText(Container, '9')).dy, 600.0);
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment