Unverified Commit 4141946c authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Creating chips custom renderer, updating visual look. (#15596)

This updates the visual look of the Chip class, so that it scales properly in the face of text scale (and label widget size) changes, and bases its height off of the label widget's height, constraining the other widgets it contains to be the same height.

To do this properly, I had to implement a custom render object that will measure the height of the label, so the guts of this class are basically rewritten.

In addition, to allow the circle avatar to increase in size when the chip does, I added minRadius and maxRadius arguments to it, and I updated its color handling to use the light/dark primary colors of the theme in a smart way instead of just using black and white.

Updated and added tests.
parent 2614c7d9
This diff is collapsed.
......@@ -4,7 +4,6 @@
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
import 'theme_data.dart';
......@@ -44,8 +43,8 @@ import 'theme_data.dart';
/// See also:
///
/// * [Chip], for representing users or concepts in long form.
/// * [ListTile], which can combine an icon (such as a [CircleAvatar]) with some
/// text for a fixed height list entry.
/// * [ListTile], which can combine an icon (such as a [CircleAvatar]) with
/// some text for a fixed height list entry.
/// * <https://material.google.com/components/chips.html#chips-contact-chips>
class CircleAvatar extends StatelessWidget {
/// Creates a circle that represents a user.
......@@ -55,8 +54,11 @@ class CircleAvatar extends StatelessWidget {
this.backgroundColor,
this.backgroundImage,
this.foregroundColor,
this.radius: 20.0,
}) : super(key: key);
this.radius,
this.minRadius,
this.maxRadius,
}) : assert(radius == null || (minRadius == null && maxRadius == null)),
super(key: key);
/// The widget below this widget in the tree.
///
......@@ -67,13 +69,18 @@ class CircleAvatar extends StatelessWidget {
/// The color with which to fill the circle. Changing the background
/// color will cause the avatar to animate to the new color.
///
/// If a background color is not specified, the theme's primary color is used.
/// If a [backgroundColor] is not specified, the theme's
/// [ThemeData.primaryColorLight] is used with dark foreground colors, and
/// [ThemeData.primaryColorDark] with light foreground colors.
final Color backgroundColor;
/// The default text color for text in the circle.
///
/// Falls back to white if a background color is specified, or the primary
/// text theme color otherwise.
/// Defaults to the primary text theme color if no [backgroundColor] is
/// specified.
///
/// Defaults to [ThemeData.primaryColorLight] for dark background colors, and
/// [ThemeData.primaryColorDark] for light background colors.
final Color foregroundColor;
/// The background image of the circle. Changing the background
......@@ -85,48 +92,112 @@ class CircleAvatar extends StatelessWidget {
/// The size of the avatar. Changing the radius will cause the
/// avatar to animate to the new size.
///
/// If [radius] is specified, then neither [minRadius] nor [maxRadius] may be
/// specified. Specifying [radius] is equivalent to specifying a [minRadius]
/// and [maxRadius], both with the value of [radius].
///
/// Defaults to 20 logical pixels.
final double radius;
/// The minimum size of the avatar.
///
/// Changing the minRadius may cause the avatar to animate to the new size, if
/// constraints allow.
///
/// If minRadius is specified, then [radius] must not also be specified.
///
/// Defaults to zero.
final double minRadius;
/// The maximum size of the avatar.
///
/// Changing the maxRadius will cause the avatar to animate to the new size,
/// if constraints allow.
///
/// If maxRadius is specified, then [radius] must not also be specified.
///
/// Defaults to [double.infinity].
final double maxRadius;
// The default radius if nothing is specified.
static const double _defaultRadius = 20.0;
// The default min if only the max is specified.
static const double _defaultMinRadius = 0.0;
// The default max if only the min is specified.
static const double _defaultMaxRadius = double.infinity;
double get _minDiameter {
if (radius == null && minRadius == null && maxRadius == null) {
return _defaultRadius * 2.0;
}
return 2.0 * (radius ?? minRadius ?? _defaultMinRadius);
}
double get _maxDiameter {
if (radius == null && minRadius == null && maxRadius == null) {
return _defaultRadius * 2.0;
}
return 2.0 * (radius ?? maxRadius ?? _defaultMaxRadius);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
final ThemeData theme = Theme.of(context);
TextStyle textStyle = theme.primaryTextTheme.title;
if (foregroundColor != null) {
textStyle = textStyle.copyWith(color: foregroundColor);
} else if (backgroundColor != null) {
TextStyle textStyle = theme.primaryTextTheme.title.copyWith(color: foregroundColor);
Color effectiveBackgroundColor = backgroundColor;
if (effectiveBackgroundColor == null) {
switch (ThemeData.estimateBrightnessForColor(textStyle.color)) {
case Brightness.dark:
effectiveBackgroundColor = theme.primaryColorLight;
break;
case Brightness.light:
effectiveBackgroundColor = theme.primaryColorDark;
break;
}
} else if (foregroundColor == null) {
switch (ThemeData.estimateBrightnessForColor(backgroundColor)) {
case Brightness.dark:
textStyle = textStyle.copyWith(color: Colors.white);
textStyle = textStyle.copyWith(color: theme.primaryColorLight);
break;
case Brightness.light:
textStyle = textStyle.copyWith(color: Colors.black);
textStyle = textStyle.copyWith(color: theme.primaryColorDark);
break;
}
}
final double minDiameter = _minDiameter;
final double maxDiameter = _maxDiameter;
return new AnimatedContainer(
width: radius * 2.0,
height: radius * 2.0,
constraints: new BoxConstraints(
minHeight: minDiameter,
minWidth: minDiameter,
maxWidth: maxDiameter,
maxHeight: maxDiameter,
),
duration: kThemeChangeDuration,
decoration: new BoxDecoration(
color: backgroundColor ?? theme.primaryColor,
image: backgroundImage != null ? new DecorationImage(
image: backgroundImage
) : null,
color: effectiveBackgroundColor,
image: backgroundImage != null ? new DecorationImage(image: backgroundImage) : null,
shape: BoxShape.circle,
),
child: child != null ? new Center(
child: new MediaQuery(
// Need to reset the textScaleFactor here so that the
// text doesn't escape the avatar when the textScaleFactor is large.
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: new DefaultTextStyle(
style: textStyle.copyWith(color: foregroundColor),
child: child,
),
)
) : null,
child: child == null
? null
: new Center(
child: new MediaQuery(
// Need to ignore the ambient textScaleFactor here so that the
// text doesn't escape the avatar when the textScaleFactor is large.
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: new IconTheme(
data: theme.iconTheme.copyWith(color: textStyle.color),
child: new DefaultTextStyle(
style: textStyle,
child: child,
),
),
),
),
);
}
}
......@@ -170,9 +170,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
@override
void deactivate() {
super.deactivate();
if (_entry != null)
_controller.reverse();
super.deactivate();
}
@override
......
......@@ -1443,7 +1443,7 @@ abstract class RenderBox extends RenderObject {
/// of those functions, call [markNeedsLayout] instead to schedule a layout of
/// the box.
Size get size {
assert(hasSize);
assert(hasSize, 'RenderBox was not laid out: ${toString()}');
assert(() {
if (_size is _DebugSize) {
final _DebugSize _size = this._size;
......
......@@ -590,7 +590,7 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
/// child, the child will be clipped.
///
/// In debug mode, if the child overflows the box, a warning will be printed on
/// the console, and black and yellow striped areas will appear where theR
/// the console, and black and yellow striped areas will appear where the
/// overflow occurs.
///
/// See also:
......
......@@ -27,7 +27,7 @@ void main() {
expect(decoration.color, equals(backgroundColor));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(Colors.white));
expect(paragraph.text.style.color, equals(new ThemeData.fallback().primaryColorLight));
});
testWidgets('CircleAvatar with light background color', (WidgetTester tester) async {
......@@ -50,7 +50,7 @@ void main() {
expect(decoration.color, equals(backgroundColor));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(Colors.black));
expect(paragraph.text.style.color, equals(new ThemeData.fallback().primaryColorDark));
});
testWidgets('CircleAvatar with foreground color', (WidgetTester tester) async {
......@@ -71,13 +71,13 @@ void main() {
expect(box.size.height, equals(40.0));
final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration;
expect(decoration.color, equals(fallback.primaryColor));
expect(decoration.color, equals(fallback.primaryColorDark));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(foregroundColor));
});
testWidgets('CircleAvatar with theme', (WidgetTester tester) async {
testWidgets('CircleAvatar with light theme', (WidgetTester tester) async {
final ThemeData theme = new ThemeData(
primaryColor: Colors.grey.shade100,
primaryColorBrightness: Brightness.light,
......@@ -96,7 +96,32 @@ void main() {
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration;
expect(decoration.color, equals(theme.primaryColor));
expect(decoration.color, equals(theme.primaryColorLight));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(theme.primaryTextTheme.title.color));
});
testWidgets('CircleAvatar with dark theme', (WidgetTester tester) async {
final ThemeData theme = new ThemeData(
primaryColor: Colors.grey.shade800,
primaryColorBrightness: Brightness.dark,
);
await tester.pumpWidget(
wrap(
child: new Theme(
data: theme,
child: const CircleAvatar(
child: const Text('Z'),
),
),
),
);
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration;
expect(decoration.color, equals(theme.primaryColorDark));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(theme.primaryTextTheme.title.color));
......@@ -144,6 +169,78 @@ void main() {
);
expect(tester.getSize(find.text('Z')), equals(const Size(20.0, 20.0)));
});
testWidgets('CircleAvatar respects minRadius', (WidgetTester tester) async {
final Color backgroundColor = Colors.blue.shade900;
await tester.pumpWidget(
wrap(
child: new UnconstrainedBox(
child: new CircleAvatar(
backgroundColor: backgroundColor,
minRadius: 50.0,
child: const Text('Z'),
),
),
),
);
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
expect(box.size.width, equals(100.0));
expect(box.size.height, equals(100.0));
final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration;
expect(decoration.color, equals(backgroundColor));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(new ThemeData.fallback().primaryColorLight));
});
testWidgets('CircleAvatar respects maxRadius', (WidgetTester tester) async {
final Color backgroundColor = Colors.blue.shade900;
await tester.pumpWidget(
wrap(
child: new CircleAvatar(
backgroundColor: backgroundColor,
maxRadius: 50.0,
child: const Text('Z'),
),
),
);
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
expect(box.size.width, equals(100.0));
expect(box.size.height, equals(100.0));
final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration;
expect(decoration.color, equals(backgroundColor));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(new ThemeData.fallback().primaryColorLight));
});
testWidgets('CircleAvatar respects setting both minRadius and maxRadius', (WidgetTester tester) async {
final Color backgroundColor = Colors.blue.shade900;
await tester.pumpWidget(
wrap(
child: new CircleAvatar(
backgroundColor: backgroundColor,
maxRadius: 50.0,
minRadius: 50.0,
child: const Text('Z'),
),
),
);
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
expect(box.size.width, equals(100.0));
expect(box.size.height, equals(100.0));
final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration;
expect(decoration.color, equals(backgroundColor));
final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(new ThemeData.fallback().primaryColorLight));
});
}
Widget wrap({ Widget child }) {
......
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