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> {
@override
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 {
///
/// The `height` and `width` values include the padding.
///
/// The `color` argument is a shorthand for `decoration: new
/// BoxDecoration(color: color)`, which means you cannot supply both a `color`
/// and a `decoration` argument. If you want to have both a `color` and a
/// `decoration`, you can pass the color as the `color` argument to the
/// `BoxDecoration`.
/// The `color` and `decoration` arguments cannot both be supplied, since
/// it would potentially result in the decoration drawing over the background
/// color. To supply a decoration with a color, use `decoration:
/// BoxDecoration(color: color)`.
Container({
Key key,
this.alignment,
this.padding,
Color color,
Decoration decoration,
this.color,
this.decoration,
this.foregroundDecoration,
double width,
double height,
......@@ -320,9 +319,8 @@ class Container extends StatelessWidget {
assert(clipBehavior != null),
assert(color == null || decoration == null,
'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 =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
......@@ -363,11 +361,20 @@ class Container extends StatelessWidget {
/// see [Decoration.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].
///
/// A shorthand for specifying just a solid color is available in the
/// constructor: set the `color` argument instead of the `decoration`
/// argument.
/// Use the [color] property to specify a simple solid color.
///
/// The [child] is not clipped to the decoration. To clip a child to the shape
/// of a particular [ShapeDecoration], consider using a [ClipPath] widget.
......@@ -423,6 +430,9 @@ class Container extends StatelessWidget {
if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current);
if (color != null)
current = ColoredBox(color: color, child: current);
if (decoration != null)
current = DecoratedBox(decoration: decoration, child: current);
......
......@@ -25,7 +25,7 @@ void main() {
);
final Container container = _getContainerFromBanner(tester);
expect(container.decoration, const BoxDecoration(color: color));
expect(container.color, color);
});
testWidgets('Custom content TextStyle respected', (WidgetTester tester) async {
......
......@@ -66,7 +66,7 @@ void main() {
final Container container = _getContainerFromBanner(tester);
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()
expect(content.text.style, Typography.material2014().englishLike.bodyText2.merge(Typography.material2014().black.bodyText2));
});
......@@ -92,7 +92,7 @@ void main() {
final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, BoxDecoration(color: bannerTheme.backgroundColor));
expect(container.color, bannerTheme.backgroundColor);
expect(content.text.style, bannerTheme.contentTextStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
......@@ -131,7 +131,7 @@ void main() {
final Container container = _getContainerFromBanner(tester);
final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
expect(container.decoration, const BoxDecoration(color: backgroundColor));
expect(container.color, backgroundColor);
expect(content.text.style, textStyle);
final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText));
......@@ -161,7 +161,7 @@ void main() {
));
final Container container = _getContainerFromBanner(tester);
expect(container.decoration, BoxDecoration(color: colorScheme.surface));
expect(container.color, colorScheme.surface);
});
}
......
......@@ -148,9 +148,7 @@ void main() {
scaffoldKey.currentState.openDrawer();
await tester.pumpAndSettle();
BoxDecoration decoration = getScrim().decoration as BoxDecoration;
expect(decoration.color, Colors.black54);
expect(decoration.shape, BoxShape.rectangle);
expect(getScrim().color, Colors.black54);
await tester.tap(find.byType(Drawer));
await tester.pumpAndSettle();
......@@ -162,9 +160,7 @@ void main() {
scaffoldKey.currentState.openDrawer();
await tester.pumpAndSettle();
decoration = getScrim().decoration as BoxDecoration;
expect(decoration.color, const Color(0xFF323232));
expect(decoration.shape, BoxShape.rectangle);
expect(getScrim().color, const Color(0xFF323232));
await tester.tap(find.byType(Drawer));
await tester.pumpAndSettle();
......
......@@ -73,8 +73,7 @@ void main() {
}
Color containerColor() {
final BoxDecoration decoration = tester.widget<Container>(find.byKey(primaryContainerKey)).decoration as BoxDecoration;
return decoration.color;
return tester.widget<Container>(find.byKey(primaryContainerKey)).color;
}
await tester.pumpWidget(buildFrame());
......@@ -225,10 +224,9 @@ void main() {
}
Color bannerColor() {
final BoxDecoration decoration = tester.widget<Container>(
return tester.widget<Container>(
find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first,
).decoration as BoxDecoration;
return decoration.color;
).color;
}
TextStyle getTextStyle(String text) {
......
......@@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:mockito/mockito.dart';
void main() {
group('PhysicalShape', () {
......@@ -278,6 +279,90 @@ void main() {
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);
......@@ -319,3 +404,6 @@ class DoesNotHitRenderBox extends Matcher {
).isEmpty;
}
}
class _MockPaintingContext extends Mock implements PaintingContext {}
class _MockCanvas extends Mock implements Canvas {}
......@@ -259,9 +259,8 @@ void main() {
),
);
DecoratedBox widget = tester.firstWidget(find.byType(DecoratedBox));
BoxDecoration decoration = widget.decoration as BoxDecoration;
expect(decoration.color, equals(Colors.blue));
Container widget = tester.firstWidget(find.byType(Container));
expect(widget.color, equals(Colors.blue));
setState(() {
themeData = ThemeData(primarySwatch: Colors.green);
......@@ -269,9 +268,8 @@ void main() {
await tester.pump();
widget = tester.firstWidget(find.byType(DecoratedBox));
decoration = widget.decoration as BoxDecoration;
expect(decoration.color, equals(Colors.green));
widget = tester.firstWidget(find.byType(Container));
expect(widget.color, equals(Colors.green));
});
testWidgets('ListView padding', (WidgetTester tester) async {
......
......@@ -495,7 +495,9 @@ void main() {
SliverFixedExtentList(
itemExtent: 150,
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,
),
),
......@@ -510,8 +512,12 @@ void main() {
];
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
controller.jumpTo(controller.position.maxScrollExtent);
......@@ -520,8 +526,8 @@ void main() {
// Check item at the end of the list
expect(find.byKey(key), findsNothing);
expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration,
amberBox,
find.bySemanticsLabel('4'),
findsOneWidget,
);
// Overscroll
......@@ -531,16 +537,16 @@ void main() {
// Check for new item at the end of the now overscrolled list
expect(find.byKey(key), findsOneWidget);
expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration,
blueBox,
find.bySemanticsLabel('4'),
findsOneWidget,
);
// Ensure overscroll retracts to original size after releasing gesture
await tester.pumpAndSettle();
expect(find.byKey(key), findsNothing);
expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration,
amberBox,
find.bySemanticsLabel('4'),
findsOneWidget,
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
......@@ -758,7 +764,9 @@ void main() {
SliverFixedExtentList(
itemExtent: 150,
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,
),
),
......@@ -773,7 +781,12 @@ void main() {
];
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
controller.jumpTo(controller.position.maxScrollExtent);
......@@ -782,8 +795,8 @@ void main() {
// End of list
expect(find.byKey(key), findsNothing);
expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration,
amberBox,
find.bySemanticsLabel('4'),
findsOneWidget,
);
// Overscroll
......@@ -792,8 +805,8 @@ void main() {
expect(find.byKey(key), findsNothing);
expect(
tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).last.decoration,
amberBox,
find.bySemanticsLabel('4'),
findsOneWidget,
);
});
});
......
......@@ -225,8 +225,8 @@ void main() {
await tester.pump();
// 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.getBottomLeft(find.widgetWithText(DecoratedBox, '10')).dy, 600.0);
expect(tester.getTopLeft(find.widgetWithText(Container, '5')).dy, 0.0);
expect(tester.getBottomLeft(find.widgetWithText(Container, '10')).dy, 600.0);
// Stop returning the first 3 items.
await tester.pumpWidget(MaterialApp(
......@@ -258,10 +258,10 @@ void main() {
// 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.
// 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
// 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