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
...@@ -2,20 +2,33 @@ ...@@ -2,20 +2,33 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'debug.dart'; import 'debug.dart';
import 'feedback.dart'; import 'feedback.dart';
import 'icons.dart'; import 'icons.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'theme.dart';
import 'tooltip.dart'; import 'tooltip.dart';
// Some design constants
const double _kChipHeight = 32.0;
const double _kDeleteIconSize = 18.0;
const int _kTextLabelAlpha = 0xde;
const int _kDeleteIconAlpha = 0xde;
const int _kContainerAlpha = 0x14;
const double _kEdgePadding = 4.0;
/// A material design chip. /// A material design chip.
/// ///
/// Chips represent complex entities in small blocks, such as a contact. /// Chips represent complex entities in small blocks, such as a contact, or a
/// choice.
/// ///
/// Supplying a non-null [onDeleted] callback will cause the chip to include a /// Supplying a non-null [onDeleted] callback will cause the chip to include a
/// button for deleting the chip. /// button for deleting the chip.
...@@ -40,48 +53,46 @@ import 'tooltip.dart'; ...@@ -40,48 +53,46 @@ import 'tooltip.dart';
/// * [CircleAvatar], which shows images or initials of people. /// * [CircleAvatar], which shows images or initials of people.
/// * <https://material.google.com/components/chips.html> /// * <https://material.google.com/components/chips.html>
class Chip extends StatelessWidget { class Chip extends StatelessWidget {
/// Creates a material design chip. /// Creates a material design chip
/// ///
/// * [onDeleted] determines whether the chip has a delete button. This /// The [label] and [border] arguments may not be null.
/// callback runs when the delete button is pressed.
const Chip({ const Chip({
Key key, Key key,
this.avatar, this.avatar,
this.deleteIcon,
@required this.label, @required this.label,
this.onDeleted, this.onDeleted,
TextStyle labelStyle, this.labelStyle,
this.deleteButtonTooltipMessage, this.deleteButtonTooltipMessage,
this.backgroundColor, this.backgroundColor,
this.deleteIconColor, this.deleteIconColor,
this.border: const StadiumBorder(), this.border: const StadiumBorder(),
}) : assert(label != null), }) : assert(label != null),
assert(border != null), assert(border != null),
labelStyle = labelStyle ?? _defaultLabelStyle,
super(key: key); super(key: key);
static const TextStyle _defaultLabelStyle = const TextStyle(
inherit: false,
fontSize: 13.0,
fontWeight: FontWeight.w400,
color: Colors.black87,
textBaseline: TextBaseline.alphabetic,
);
static const double _chipHeight = 32.0;
/// A widget to display prior to the chip's label. /// A widget to display prior to the chip's label.
/// ///
/// Typically a [CircleAvatar] widget. /// Typically a [CircleAvatar] widget.
final Widget avatar; final Widget avatar;
/// The icon displayed when [onDeleted] is non-null.
///
/// This has no effect when [onDeleted] is null since no delete icon will be
/// shown.
///
/// Defaults to an [Icon] widget containing [Icons.cancel].
final Widget deleteIcon;
/// The primary content of the chip. /// The primary content of the chip.
/// ///
/// Typically a [Text] widget. /// Typically a [Text] widget.
final Widget label; final Widget label;
/// Called when the user deletes the chip, e.g., by tapping the delete button. /// Called when the user taps the delete button to delete the chip.
/// ///
/// The delete button is included in the chip only if this callback is non-null. /// This has no effect when [deleteIcon] is null since no delete icon will be
/// shown.
final VoidCallback onDeleted; final VoidCallback onDeleted;
/// The style to be applied to the chip's label. /// The style to be applied to the chip's label.
...@@ -90,7 +101,8 @@ class Chip extends StatelessWidget { ...@@ -90,7 +101,8 @@ class Chip extends StatelessWidget {
/// such as [Text]. /// such as [Text].
final TextStyle labelStyle; final TextStyle labelStyle;
/// Color to be used for the chip's background, the default being grey. /// Color to be used for the chip's background, the default is based on the
/// ambient [IconTheme].
/// ///
/// This color is used as the background of the container that will hold the /// This color is used as the background of the container that will hold the
/// widget's label. /// widget's label.
...@@ -101,83 +113,642 @@ class Chip extends StatelessWidget { ...@@ -101,83 +113,642 @@ class Chip extends StatelessWidget {
/// Defaults to a [StadiumBorder]. /// Defaults to a [StadiumBorder].
final ShapeBorder border; final ShapeBorder border;
/// Color for delete icon, the default being black. /// Color for delete icon. The default is based on the ambient [IconTheme].
///
/// This has no effect when [onDelete] is null since no delete icon will be
/// shown.
final Color deleteIconColor; final Color deleteIconColor;
/// Message to be used for the chip delete button's tooltip. /// Message to be used for the chip delete button's tooltip.
///
/// This has no effect when [onDelete] is null since no delete icon will be
/// shown.
final String deleteButtonTooltipMessage; final String deleteButtonTooltipMessage;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final bool deletable = onDeleted != null; final ThemeData theme = Theme.of(context);
double startPadding = 12.0; return new DefaultTextStyle(
double endPadding = 12.0; overflow: TextOverflow.fade,
textAlign: TextAlign.start,
final List<Widget> children = <Widget>[]; maxLines: 1,
softWrap: false,
if (avatar != null) { style: labelStyle ??
startPadding = 0.0; theme.textTheme.body2.copyWith(
children.add(new ExcludeSemantics( color: theme.primaryColorDark.withAlpha(_kTextLabelAlpha),
child: new Container(
margin: const EdgeInsetsDirectional.only(end: 8.0),
width: _chipHeight,
height: _chipHeight,
child: avatar,
), ),
)); child: new _ChipRenderWidget(
} theme: new _ChipRenderTheme(
label: label,
children.add(new Flexible( avatar: avatar,
child: new DefaultTextStyle( deleteIcon: onDeleted == null
overflow: TextOverflow.ellipsis, ? null
style: labelStyle, : new Tooltip(
child: label,
),
));
if (deletable) {
endPadding = 0.0;
children.add(new GestureDetector(
onTap: Feedback.wrapForTap(onDeleted, context),
child: new Tooltip(
message: deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip, message: deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip,
child: new Container( child: new IconTheme(
padding: const EdgeInsets.symmetric(horizontal: 4.0), data: theme.iconTheme.copyWith(
child: new Icon( color: deleteIconColor ?? theme.iconTheme.color.withAlpha(_kDeleteIconAlpha),
Icons.cancel,
size: 24.0,
color: deleteIconColor ?? Colors.black54,
), ),
child: deleteIcon ?? const Icon(Icons.cancel, size: _kDeleteIconSize),
), ),
), ),
)); container: new Container(
}
return new Semantics(
container: true,
child: new Container(
constraints: const BoxConstraints(minHeight: _chipHeight),
padding: new EdgeInsetsDirectional.only(start: startPadding, end: endPadding),
decoration: new ShapeDecoration( decoration: new ShapeDecoration(
color: backgroundColor ?? Colors.grey.shade300,
shape: border, shape: border,
color: backgroundColor ?? theme.primaryColorDark.withAlpha(_kContainerAlpha),
), ),
child: new Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: new Row(
children: children,
mainAxisSize: MainAxisSize.min,
), ),
padding: const EdgeInsets.all(_kEdgePadding),
labelPadding: const EdgeInsets.symmetric(horizontal: _kEdgePadding),
), ),
key: key,
onDeleted: Feedback.wrapForTap(onDeleted, context),
), ),
); );
} }
} }
class _ChipRenderWidget extends RenderObjectWidget {
const _ChipRenderWidget({
Key key,
@required this.theme,
this.onDeleted,
}) : assert(theme != null),
super(key: key);
final _ChipRenderTheme theme;
final VoidCallback onDeleted;
@override
_RenderChipElement createElement() => new _RenderChipElement(this);
@override
void updateRenderObject(BuildContext context, _RenderChip renderObject) {
renderObject
..theme = theme
..textDirection = Directionality.of(context)
..onDeleted = onDeleted;
}
@override
RenderObject createRenderObject(BuildContext context) {
return new _RenderChip(
theme: theme,
textDirection: Directionality.of(context),
onDeleted: onDeleted,
);
}
}
enum _ChipSlot {
label,
avatar,
deleteIcon,
container,
}
class _RenderChipElement extends RenderObjectElement {
_RenderChipElement(_ChipRenderWidget chip) : super(chip);
final Map<_ChipSlot, Element> slotToChild = <_ChipSlot, Element>{};
final Map<Element, _ChipSlot> childToSlot = <Element, _ChipSlot>{};
@override
_ChipRenderWidget get widget => super.widget;
@override
_RenderChip get renderObject => super.renderObject;
@override
void visitChildren(ElementVisitor visitor) {
slotToChild.values.forEach(visitor);
}
@override
void forgetChild(Element child) {
assert(slotToChild.values.contains(child));
assert(childToSlot.keys.contains(child));
final _ChipSlot slot = childToSlot[child];
childToSlot.remove(child);
slotToChild.remove(slot);
}
void _mountChild(Widget widget, _ChipSlot slot) {
final Element oldChild = slotToChild[slot];
final Element newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
slotToChild.remove(slot);
childToSlot.remove(oldChild);
}
if (newChild != null) {
slotToChild[slot] = newChild;
childToSlot[newChild] = slot;
}
}
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_mountChild(widget.theme.avatar, _ChipSlot.avatar);
_mountChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon);
_mountChild(widget.theme.label, _ChipSlot.label);
_mountChild(widget.theme.container, _ChipSlot.container);
}
void _updateChild(Widget widget, _ChipSlot slot) {
final Element oldChild = slotToChild[slot];
final Element newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
childToSlot.remove(oldChild);
slotToChild.remove(slot);
}
if (newChild != null) {
slotToChild[slot] = newChild;
childToSlot[newChild] = slot;
}
}
@override
void update(_ChipRenderWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_updateChild(widget.theme.label, _ChipSlot.label);
_updateChild(widget.theme.avatar, _ChipSlot.avatar);
_updateChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon);
_updateChild(widget.theme.container, _ChipSlot.container);
}
void _updateRenderObject(RenderObject child, _ChipSlot slot) {
switch (slot) {
case _ChipSlot.avatar:
renderObject.avatar = child;
break;
case _ChipSlot.label:
renderObject.label = child;
break;
case _ChipSlot.deleteIcon:
renderObject.deleteIcon = child;
break;
case _ChipSlot.container:
renderObject.container = child;
break;
}
}
@override
void insertChildRenderObject(RenderObject child, dynamic slotValue) {
assert(child is RenderBox);
assert(slotValue is _ChipSlot);
final _ChipSlot slot = slotValue;
_updateRenderObject(child, slot);
assert(renderObject.childToSlot.keys.contains(child));
assert(renderObject.slotToChild.keys.contains(slot));
}
@override
void removeChildRenderObject(RenderObject child) {
assert(child is RenderBox);
assert(renderObject.childToSlot.keys.contains(child));
_updateRenderObject(null, renderObject.childToSlot[child]);
assert(!renderObject.childToSlot.keys.contains(child));
assert(!renderObject.slotToChild.keys.contains(slot));
}
@override
void moveChildRenderObject(RenderObject child, dynamic slotValue) {
assert(false, 'not reachable');
}
}
class _ChipRenderTheme {
const _ChipRenderTheme({
@required this.avatar,
@required this.label,
@required this.deleteIcon,
@required this.container,
@required this.padding,
@required this.labelPadding,
});
final Widget avatar;
final Widget label;
final Widget deleteIcon;
final Widget container;
final EdgeInsets padding;
final EdgeInsets labelPadding;
@override
bool operator ==(dynamic other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
final _ChipRenderTheme typedOther = other;
return typedOther.avatar == avatar &&
typedOther.label == label &&
typedOther.deleteIcon == deleteIcon &&
typedOther.container == container &&
typedOther.padding == padding &&
typedOther.labelPadding == labelPadding;
}
@override
int get hashCode {
return hashValues(
avatar,
label,
deleteIcon,
container,
padding,
labelPadding,
);
}
}
class _RenderChip extends RenderBox {
_RenderChip({
@required _ChipRenderTheme theme,
@required TextDirection textDirection,
this.onDeleted,
}) : assert(theme != null),
assert(textDirection != null),
_theme = theme,
_textDirection = textDirection {
_tap = new TapGestureRecognizer(debugOwner: this)
..onTapDown = _handleTapDown
..onTap = _handleTap;
}
// Set this to true to have outlines of the tap targets drawn over
// the chip. This should never be checked in while set to 'true'.
static const bool _debugShowTapTargetOutlines = false;
static const EdgeInsets _iconPadding = const EdgeInsets.all(_kEdgePadding);
final Map<_ChipSlot, RenderBox> slotToChild = <_ChipSlot, RenderBox>{};
final Map<RenderBox, _ChipSlot> childToSlot = <RenderBox, _ChipSlot>{};
TapGestureRecognizer _tap;
VoidCallback onDeleted;
Rect _deleteButtonRect;
Rect _actionRect;
Offset _tapDownLocation;
RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _ChipSlot slot) {
if (oldChild != null) {
dropChild(oldChild);
childToSlot.remove(oldChild);
slotToChild.remove(slot);
}
if (newChild != null) {
childToSlot[newChild] = slot;
slotToChild[slot] = newChild;
adoptChild(newChild);
}
return newChild;
}
RenderBox _avatar;
RenderBox get avatar => _avatar;
set avatar(RenderBox value) {
_avatar = _updateChild(_avatar, value, _ChipSlot.avatar);
}
RenderBox _deleteIcon;
RenderBox get deleteIcon => _deleteIcon;
set deleteIcon(RenderBox value) {
_deleteIcon = _updateChild(_deleteIcon, value, _ChipSlot.deleteIcon);
}
RenderBox _label;
RenderBox get label => _label;
set label(RenderBox value) {
_label = _updateChild(_label, value, _ChipSlot.label);
}
RenderBox _container;
RenderBox get container => _container;
set container(RenderBox value) {
_container = _updateChild(_container, value, _ChipSlot.container);
}
_ChipRenderTheme get theme => _theme;
_ChipRenderTheme _theme;
set theme(_ChipRenderTheme value) {
if (_theme == value) {
return;
}
_theme = value;
markNeedsLayout();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
markNeedsLayout();
}
// The returned list is ordered for hit testing.
Iterable<RenderBox> get _children sync* {
if (avatar != null) {
yield avatar;
}
if (label != null) {
yield label;
}
if (deleteIcon != null) {
yield deleteIcon;
}
if (container != null) {
yield container;
}
}
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && deleteIcon != null) {
_tap.addPointer(event);
}
}
void _handleTapDown(TapDownDetails details) {
if (deleteIcon != null) {
_tapDownLocation = globalToLocal(details.globalPosition);
}
}
void _handleTap() {
if (_tapDownLocation == null) {
return;
}
if (deleteIcon != null && onDeleted != null && _deleteButtonRect.contains(_tapDownLocation)) {
onDeleted();
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
for (RenderBox child in _children) {
child.attach(owner);
}
}
@override
void detach() {
super.detach();
for (RenderBox child in _children) {
child.detach();
}
}
@override
void redepthChildren() {
_children.forEach(redepthChild);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
_children.forEach(visitor);
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
void add(RenderBox child, String name) {
if (child != null) {
value.add(child.toDiagnosticsNode(name: name));
}
}
add(avatar, 'avatar');
add(label, 'label');
add(deleteIcon, 'deleteIcon');
add(container, 'container');
return value;
}
@override
bool get sizedByParent => false;
static double _minWidth(RenderBox box, double height) {
return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
}
static double _maxWidth(RenderBox box, double height) {
return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
}
static double _minHeight(RenderBox box, double width) {
return box == null ? 0.0 : box.getMinIntrinsicWidth(width);
}
static Size _boxSize(RenderBox box) => box == null ? Size.zero : box.size;
static BoxParentData _boxParentData(RenderBox box) => box.parentData;
@override
double computeMinIntrinsicWidth(double height) {
// The overall padding isn't affected by missing avatar or delete icon
// because we add the padding regardless to give extra padding for the label
// when they're missing.
final double overallPadding = theme.labelPadding.horizontal + _iconPadding.horizontal * 2.0;
return overallPadding + _minWidth(avatar, height) + _minWidth(label, height) + _minWidth(deleteIcon, height);
}
@override
double computeMaxIntrinsicWidth(double height) {
// The overall padding isn't affected by missing avatar or delete icon
// because we add the padding regardless to give extra padding for the label
// when they're missing.
final double overallPadding = theme.labelPadding.horizontal + _iconPadding.horizontal * 2.0;
return overallPadding + _maxWidth(avatar, height) + _maxWidth(label, height) + _maxWidth(deleteIcon, height);
}
@override
double computeMinIntrinsicHeight(double width) {
// This widget is sized to the height of the label only, as long as it's
// larger than _kChipHeight. The other widgets are sized to match the
// label.
return math.max(_kChipHeight, theme.labelPadding.vertical + _minHeight(label, width));
}
@override
double computeMaxIntrinsicHeight(double width) => computeMinIntrinsicHeight(width);
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
// The baseline of this widget is the baseline of the label.
return label.computeDistanceToActualBaseline(baseline);
}
@override
void performLayout() {
double overallHeight = _kChipHeight;
if (label != null) {
label.layout(constraints.loosen(), parentUsesSize: true);
// Now that we know the height, we can determine how much to shrink the
// constraints by for the "real" layout. Ignored if the constraints are
// infinite.
overallHeight = math.max(overallHeight, _boxSize(label).height);
if (constraints.maxWidth.isFinite) {
final double allPadding = _iconPadding.horizontal * 2.0 + theme.labelPadding.horizontal;
final double iconSizes = (avatar != null ? overallHeight - _iconPadding.vertical : 0.0)
+ (deleteIcon != null ? overallHeight - _iconPadding.vertical : 0.0);
label.layout(
constraints.loosen().copyWith(
maxWidth: math.max(0.0, constraints.maxWidth - iconSizes - allPadding),
),
parentUsesSize: true,
);
}
}
final double labelWidth = theme.labelPadding.horizontal + _boxSize(label).width;
final double iconSize = overallHeight - _iconPadding.vertical;
final BoxConstraints iconConstraints = new BoxConstraints.tightFor(
width: iconSize,
height: iconSize,
);
double avatarWidth = _iconPadding.horizontal;
if (avatar != null) {
avatar.layout(iconConstraints, parentUsesSize: true);
avatarWidth += _boxSize(avatar).width;
}
double deleteIconWidth = _iconPadding.horizontal;
if (deleteIcon != null) {
deleteIcon.layout(iconConstraints, parentUsesSize: true);
deleteIconWidth += _boxSize(deleteIcon).width;
}
final double overallWidth = avatarWidth + labelWidth + deleteIconWidth;
if (container != null) {
final BoxConstraints containerConstraints = new BoxConstraints.tightFor(
height: overallHeight,
width: overallWidth,
);
container.layout(containerConstraints, parentUsesSize: true);
_boxParentData(container).offset = Offset.zero;
}
double centerLayout(RenderBox box, double x) {
_boxParentData(box).offset = new Offset(x, (overallHeight - box.size.height) / 2.0);
return box.size.width;
}
const double left = 0.0;
final double right = overallWidth;
switch (textDirection) {
case TextDirection.rtl:
double start = right - _kEdgePadding;
if (avatar != null) {
start -= centerLayout(avatar, start - avatar.size.width);
}
start -= _iconPadding.left + theme.labelPadding.right;
if (label != null) {
start -= centerLayout(label, start - label.size.width);
}
start -= _iconPadding.right + theme.labelPadding.left;
double deleteButtonWidth = 0.0;
if (deleteIcon != null) {
_deleteButtonRect = new Rect.fromLTWH(
0.0,
0.0,
iconSize + _iconPadding.horizontal,
iconSize + _iconPadding.vertical,
);
deleteButtonWidth = _deleteButtonRect.width;
start -= centerLayout(deleteIcon, start - deleteIcon.size.width);
}
if (avatar != null || label != null) {
_actionRect = new Rect.fromLTWH(
deleteButtonWidth,
0.0,
overallWidth - deleteButtonWidth,
overallHeight,
);
}
break;
case TextDirection.ltr:
double start = left + _kEdgePadding;
if (avatar != null) {
start += centerLayout(avatar, start);
}
start += _iconPadding.right + theme.labelPadding.left;
if (label != null) {
start += centerLayout(label, start);
}
start += _iconPadding.left + theme.labelPadding.right;
if (avatar != null || label != null) {
_actionRect = new Rect.fromLTWH(
0.0,
0.0,
deleteIcon != null ? (start - _kEdgePadding) : overallWidth,
overallHeight,
);
}
if (deleteIcon != null) {
_deleteButtonRect = new Rect.fromLTWH(
start - _kEdgePadding,
0.0,
iconSize + _iconPadding.horizontal,
iconSize + _iconPadding.vertical,
);
centerLayout(deleteIcon, start);
}
break;
}
size = constraints.constrain(new Size(overallWidth, overallHeight));
assert(size.width == constraints.constrainWidth(overallWidth));
assert(size.height == constraints.constrainHeight(overallHeight));
}
@override
void paint(PaintingContext context, Offset offset) {
void doPaint(RenderBox child) {
if (child != null) {
context.paintChild(child, _boxParentData(child).offset + offset);
}
}
assert(!_debugShowTapTargetOutlines ||
() {
// Draws a rect around the tap targets to help with visualizing where
// they really are.
final Paint outlinePaint = new Paint()
..color = const Color(0xff800000)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
if (deleteIcon != null) {
context.canvas.drawRect(_deleteButtonRect.shift(offset), outlinePaint);
}
context.canvas.drawRect(
_actionRect.shift(offset),
outlinePaint..color = const Color(0xff008000),
);
return true;
}());
doPaint(container);
doPaint(avatar);
doPaint(deleteIcon);
doPaint(label);
}
@override
bool hitTestSelf(Offset position) => true;
@override
bool hitTestChildren(HitTestResult result, {@required Offset position}) {
assert(position != null);
for (RenderBox child in _children) {
if (child.hasSize && child.hitTest(result, position: position - _boxParentData(child).offset)) {
return true;
}
}
return false;
}
}
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
...@@ -44,8 +43,8 @@ import 'theme_data.dart'; ...@@ -44,8 +43,8 @@ import 'theme_data.dart';
/// See also: /// See also:
/// ///
/// * [Chip], for representing users or concepts in long form. /// * [Chip], for representing users or concepts in long form.
/// * [ListTile], which can combine an icon (such as a [CircleAvatar]) with some /// * [ListTile], which can combine an icon (such as a [CircleAvatar]) with
/// text for a fixed height list entry. /// some text for a fixed height list entry.
/// * <https://material.google.com/components/chips.html#chips-contact-chips> /// * <https://material.google.com/components/chips.html#chips-contact-chips>
class CircleAvatar extends StatelessWidget { class CircleAvatar extends StatelessWidget {
/// Creates a circle that represents a user. /// Creates a circle that represents a user.
...@@ -55,8 +54,11 @@ class CircleAvatar extends StatelessWidget { ...@@ -55,8 +54,11 @@ class CircleAvatar extends StatelessWidget {
this.backgroundColor, this.backgroundColor,
this.backgroundImage, this.backgroundImage,
this.foregroundColor, this.foregroundColor,
this.radius: 20.0, this.radius,
}) : super(key: key); this.minRadius,
this.maxRadius,
}) : assert(radius == null || (minRadius == null && maxRadius == null)),
super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
...@@ -67,13 +69,18 @@ class CircleAvatar extends StatelessWidget { ...@@ -67,13 +69,18 @@ class CircleAvatar extends StatelessWidget {
/// The color with which to fill the circle. Changing the background /// The color with which to fill the circle. Changing the background
/// color will cause the avatar to animate to the new color. /// 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; final Color backgroundColor;
/// The default text color for text in the circle. /// The default text color for text in the circle.
/// ///
/// Falls back to white if a background color is specified, or the primary /// Defaults to the primary text theme color if no [backgroundColor] is
/// text theme color otherwise. /// specified.
///
/// Defaults to [ThemeData.primaryColorLight] for dark background colors, and
/// [ThemeData.primaryColorDark] for light background colors.
final Color foregroundColor; final Color foregroundColor;
/// The background image of the circle. Changing the background /// The background image of the circle. Changing the background
...@@ -85,48 +92,112 @@ class CircleAvatar extends StatelessWidget { ...@@ -85,48 +92,112 @@ class CircleAvatar extends StatelessWidget {
/// The size of the avatar. Changing the radius will cause the /// The size of the avatar. Changing the radius will cause the
/// avatar to animate to the new size. /// 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. /// Defaults to 20 logical pixels.
final double radius; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
TextStyle textStyle = theme.primaryTextTheme.title; TextStyle textStyle = theme.primaryTextTheme.title.copyWith(color: foregroundColor);
if (foregroundColor != null) { Color effectiveBackgroundColor = backgroundColor;
textStyle = textStyle.copyWith(color: foregroundColor); if (effectiveBackgroundColor == null) {
} else if (backgroundColor != 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)) { switch (ThemeData.estimateBrightnessForColor(backgroundColor)) {
case Brightness.dark: case Brightness.dark:
textStyle = textStyle.copyWith(color: Colors.white); textStyle = textStyle.copyWith(color: theme.primaryColorLight);
break; break;
case Brightness.light: case Brightness.light:
textStyle = textStyle.copyWith(color: Colors.black); textStyle = textStyle.copyWith(color: theme.primaryColorDark);
break; break;
} }
} }
final double minDiameter = _minDiameter;
final double maxDiameter = _maxDiameter;
return new AnimatedContainer( return new AnimatedContainer(
width: radius * 2.0, constraints: new BoxConstraints(
height: radius * 2.0, minHeight: minDiameter,
minWidth: minDiameter,
maxWidth: maxDiameter,
maxHeight: maxDiameter,
),
duration: kThemeChangeDuration, duration: kThemeChangeDuration,
decoration: new BoxDecoration( decoration: new BoxDecoration(
color: backgroundColor ?? theme.primaryColor, color: effectiveBackgroundColor,
image: backgroundImage != null ? new DecorationImage( image: backgroundImage != null ? new DecorationImage(image: backgroundImage) : null,
image: backgroundImage
) : null,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: child != null ? new Center( child: child == null
? null
: new Center(
child: new MediaQuery( child: new MediaQuery(
// Need to reset the textScaleFactor here so that the // Need to ignore the ambient textScaleFactor here so that the
// text doesn't escape the avatar when the textScaleFactor is large. // text doesn't escape the avatar when the textScaleFactor is large.
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: new IconTheme(
data: theme.iconTheme.copyWith(color: textStyle.color),
child: new DefaultTextStyle( child: new DefaultTextStyle(
style: textStyle.copyWith(color: foregroundColor), style: textStyle,
child: child, child: child,
), ),
) ),
) : null, ),
),
); );
} }
} }
...@@ -170,9 +170,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -170,9 +170,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
@override @override
void deactivate() { void deactivate() {
super.deactivate();
if (_entry != null) if (_entry != null)
_controller.reverse(); _controller.reverse();
super.deactivate();
} }
@override @override
......
...@@ -1443,7 +1443,7 @@ abstract class RenderBox extends RenderObject { ...@@ -1443,7 +1443,7 @@ abstract class RenderBox extends RenderObject {
/// of those functions, call [markNeedsLayout] instead to schedule a layout of /// of those functions, call [markNeedsLayout] instead to schedule a layout of
/// the box. /// the box.
Size get size { Size get size {
assert(hasSize); assert(hasSize, 'RenderBox was not laid out: ${toString()}');
assert(() { assert(() {
if (_size is _DebugSize) { if (_size is _DebugSize) {
final _DebugSize _size = this._size; final _DebugSize _size = this._size;
......
...@@ -590,7 +590,7 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox { ...@@ -590,7 +590,7 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
/// child, the child will be clipped. /// child, the child will be clipped.
/// ///
/// In debug mode, if the child overflows the box, a warning will be printed on /// 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. /// overflow occurs.
/// ///
/// See also: /// See also:
......
...@@ -11,9 +11,11 @@ void main() { ...@@ -11,9 +11,11 @@ void main() {
/// Tests that a [Chip] that has its size constrained by its parent is /// Tests that a [Chip] that has its size constrained by its parent is
/// further constraining the size of its child, the label widget. /// further constraining the size of its child, the label widget.
/// Optionally, adding an avatar or delete icon to the chip should not /// Optionally, adding an avatar or delete icon to the chip should not
/// cause the chip or label to exceed its constrained size. /// cause the chip or label to exceed its constrained height.
Future<Null> _testConstrainedLabel(WidgetTester tester, { Future<Null> _testConstrainedLabel(
CircleAvatar avatar, VoidCallback onDeleted, WidgetTester tester, {
CircleAvatar avatar,
VoidCallback onDeleted,
}) async { }) async {
const double labelWidth = 100.0; const double labelWidth = 100.0;
const double labelHeight = 50.0; const double labelHeight = 50.0;
...@@ -55,15 +57,11 @@ void main() { ...@@ -55,15 +57,11 @@ void main() {
testWidgets('Chip control test', (WidgetTester tester) async { testWidgets('Chip control test', (WidgetTester tester) async {
final FeedbackTester feedback = new FeedbackTester(); final FeedbackTester feedback = new FeedbackTester();
final List<String> deletedChipLabels = <String>[]; final List<String> deletedChipLabels = <String>[];
await tester.pumpWidget( await tester.pumpWidget(new MaterialApp(
new MaterialApp(
home: new Material( home: new Material(
child: new Column( child: new Column(children: <Widget>[
children: <Widget>[
new Chip( new Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('A')),
child: const Text('A')
),
label: const Text('Chip A'), label: const Text('Chip A'),
onDeleted: () { onDeleted: () {
deletedChipLabels.add('A'); deletedChipLabels.add('A');
...@@ -71,20 +69,14 @@ void main() { ...@@ -71,20 +69,14 @@ void main() {
deleteButtonTooltipMessage: 'Delete chip A', deleteButtonTooltipMessage: 'Delete chip A',
), ),
new Chip( new Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('B')),
child: const Text('B')
),
label: const Text('Chip B'), label: const Text('Chip B'),
onDeleted: () { onDeleted: () {
deletedChipLabels.add('B'); deletedChipLabels.add('B');
}, },
deleteButtonTooltipMessage: 'Delete chip B', deleteButtonTooltipMessage: 'Delete chip B',
), ),
] ]))));
)
)
)
);
expect(tester.widget(find.byTooltip('Delete chip A')), isNotNull); expect(tester.widget(find.byTooltip('Delete chip A')), isNotNull);
expect(tester.widget(find.byTooltip('Delete chip B')), isNotNull); expect(tester.widget(find.byTooltip('Delete chip B')), isNotNull);
...@@ -107,17 +99,17 @@ void main() { ...@@ -107,17 +99,17 @@ void main() {
feedback.dispose(); feedback.dispose();
}); });
testWidgets('Chip does not constrain size of label widget if it does not exceed ' testWidgets(
'Chip does not constrain size of label widget if it does not exceed '
'the available space', (WidgetTester tester) async { 'the available space', (WidgetTester tester) async {
const double labelWidth = 50.0; const double labelWidth = 50.0;
const double labelHeight = 30.0; const double labelHeight = 30.0;
final Key labelKey = new UniqueKey(); final Key labelKey = new UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(
new Directionality( new Material(
textDirection: TextDirection.ltr, child: new MaterialApp(
child: new Material( home: new Center(
child: new Center(
child: new Container( child: new Container(
width: 500.0, width: 500.0,
height: 500.0, height: 500.0,
...@@ -143,22 +135,23 @@ void main() { ...@@ -143,22 +135,23 @@ void main() {
expect(labelSize.height, labelHeight); expect(labelSize.height, labelHeight);
}); });
testWidgets('Chip constrains the size of the label widget when it exceeds the ' testWidgets(
'Chip constrains the size of the label widget when it exceeds the '
'available space', (WidgetTester tester) async { 'available space', (WidgetTester tester) async {
await _testConstrainedLabel(tester); await _testConstrainedLabel(tester);
}); });
testWidgets('Chip constrains the size of the label widget when it exceeds the ' testWidgets(
'Chip constrains the size of the label widget when it exceeds the '
'available space and the avatar is present', (WidgetTester tester) async { 'available space and the avatar is present', (WidgetTester tester) async {
await _testConstrainedLabel( await _testConstrainedLabel(
tester, tester,
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('A')),
child: const Text('A')
),
); );
}); });
testWidgets('Chip constrains the size of the label widget when it exceeds the ' testWidgets(
'Chip constrains the size of the label widget when it exceeds the '
'available space and the delete icon is present', (WidgetTester tester) async { 'available space and the delete icon is present', (WidgetTester tester) async {
await _testConstrainedLabel( await _testConstrainedLabel(
tester, tester,
...@@ -166,13 +159,12 @@ void main() { ...@@ -166,13 +159,12 @@ void main() {
); );
}); });
testWidgets('Chip constrains the size of the label widget when it exceeds the ' testWidgets(
'Chip constrains the size of the label widget when it exceeds the '
'available space and both avatar and delete icons are present', (WidgetTester tester) async { 'available space and both avatar and delete icons are present', (WidgetTester tester) async {
await _testConstrainedLabel( await _testConstrainedLabel(
tester, tester,
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('A')),
child: const Text('A')
),
onDeleted: () {}, onDeleted: () {},
); );
}); });
...@@ -228,7 +220,7 @@ void main() { ...@@ -228,7 +220,7 @@ void main() {
return new Material( return new Material(
child: new Center( child: new Center(
child: new Chip( child: new Chip(
onDeleted: () { }, onDeleted: () {},
label: const Text('ABC'), label: const Text('ABC'),
), ),
), ),
...@@ -276,15 +268,11 @@ void main() { ...@@ -276,15 +268,11 @@ void main() {
child: new Column( child: new Column(
children: const <Widget>[ children: const <Widget>[
const Chip( const Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('A')),
child: const Text('A')
),
label: const Text('Chip A'), label: const Text('Chip A'),
), ),
const Chip( const Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('B')),
child: const Text('B')
),
label: const Text('Chip B'), label: const Text('Chip B'),
), ),
], ],
...@@ -297,20 +285,14 @@ void main() { ...@@ -297,20 +285,14 @@ void main() {
// https://github.com/flutter/flutter/issues/12357 // https://github.com/flutter/flutter/issues/12357
expect( expect(
tester.getSize(find.text('Chip A')), tester.getSize(find.text('Chip A')),
anyOf(const Size(79.0, 13.0), const Size(78.0, 13.0)), anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)),
); );
expect( expect(
tester.getSize(find.text('Chip B')), tester.getSize(find.text('Chip B')),
anyOf(const Size(79.0, 13.0), const Size(78.0, 13.0)), anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)),
);
expect(
tester.getSize(find.byType(Chip).first),
anyOf(const Size(131.0, 32.0), const Size(130.0, 32.0))
);
expect(
tester.getSize(find.byType(Chip).last),
anyOf(const Size(131.0, 32.0), const Size(130.0, 32.0))
); );
expect(tester.getSize(find.byType(Chip).first), anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0)));
expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0)));
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
...@@ -320,15 +302,11 @@ void main() { ...@@ -320,15 +302,11 @@ void main() {
child: new Column( child: new Column(
children: const <Widget>[ children: const <Widget>[
const Chip( const Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('A')),
child: const Text('A')
),
label: const Text('Chip A'), label: const Text('Chip A'),
), ),
const Chip( const Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('B')),
child: const Text('B')
),
label: const Text('Chip B'), label: const Text('Chip B'),
), ),
], ],
...@@ -340,12 +318,12 @@ void main() { ...@@ -340,12 +318,12 @@ void main() {
// TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs. // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
// https://github.com/flutter/flutter/issues/12357 // https://github.com/flutter/flutter/issues/12357
expect(tester.getSize(find.text('Chip A')), anyOf(const Size(234.0, 39.0), const Size(235.0, 39.0))); expect(tester.getSize(find.text('Chip A')), anyOf(const Size(252.0, 42.0), const Size(251.0, 42.0)));
expect(tester.getSize(find.text('Chip B')), anyOf(const Size(234.0, 39.0), const Size(235.0, 39.0))); expect(tester.getSize(find.text('Chip B')), anyOf(const Size(252.0, 42.0), const Size(251.0, 42.0)));
expect(tester.getSize(find.byType(Chip).first).width, anyOf(286.0, 287.0)); expect(tester.getSize(find.byType(Chip).first).width, anyOf(310.0, 309.0));
expect(tester.getSize(find.byType(Chip).first).height, equals(39.0)); expect(tester.getSize(find.byType(Chip).first).height, equals(42.0));
expect(tester.getSize(find.byType(Chip).last).width, anyOf(286.0, 287.0)); expect(tester.getSize(find.byType(Chip).last).width, anyOf(310.0, 309.0));
expect(tester.getSize(find.byType(Chip).last).height, equals(39.0)); expect(tester.getSize(find.byType(Chip).last).height, equals(42.0));
// Check that individual text scales are taken into account. // Check that individual text scales are taken into account.
await tester.pumpWidget( await tester.pumpWidget(
...@@ -354,15 +332,11 @@ void main() { ...@@ -354,15 +332,11 @@ void main() {
child: new Column( child: new Column(
children: const <Widget>[ children: const <Widget>[
const Chip( const Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('A')),
child: const Text('A')
),
label: const Text('Chip A', textScaleFactor: 3.0), label: const Text('Chip A', textScaleFactor: 3.0),
), ),
const Chip( const Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('B')),
child: const Text('B')
),
label: const Text('Chip B'), label: const Text('Chip B'),
), ),
], ],
...@@ -373,11 +347,11 @@ void main() { ...@@ -373,11 +347,11 @@ void main() {
// TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs. // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
// https://github.com/flutter/flutter/issues/12357 // https://github.com/flutter/flutter/issues/12357
expect(tester.getSize(find.text('Chip A')), anyOf(const Size(234.0, 39.0), const Size(235.0, 39.0))); expect(tester.getSize(find.text('Chip A')), anyOf(const Size(252.0, 42.0), const Size(251.0, 42.0)));
expect(tester.getSize(find.text('Chip B')), anyOf(const Size(78.0, 13.0), const Size(79.0, 13.0))); expect(tester.getSize(find.text('Chip B')), anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)));
expect(tester.getSize(find.byType(Chip).first).width, anyOf(286.0, 287.0)); expect(tester.getSize(find.byType(Chip).first).width, anyOf(310.0, 309.0));
expect(tester.getSize(find.byType(Chip).first).height, equals(39.0)); expect(tester.getSize(find.byType(Chip).first).height, equals(42.0));
expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(130.0, 32.0), const Size(131.0, 32.0))); expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0)));
}); });
testWidgets('Labels can be non-text widgets', (WidgetTester tester) async { testWidgets('Labels can be non-text widgets', (WidgetTester tester) async {
...@@ -389,15 +363,11 @@ void main() { ...@@ -389,15 +363,11 @@ void main() {
child: new Column( child: new Column(
children: <Widget>[ children: <Widget>[
new Chip( new Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('A')),
child: const Text('A')
),
label: new Text('Chip A', key: keyA), label: new Text('Chip A', key: keyA),
), ),
new Chip( new Chip(
avatar: const CircleAvatar( avatar: const CircleAvatar(child: const Text('B')),
child: const Text('B')
),
label: new Container(key: keyB, width: 10.0, height: 10.0), label: new Container(key: keyB, width: 10.0, height: 10.0),
), ),
], ],
...@@ -410,18 +380,16 @@ void main() { ...@@ -410,18 +380,16 @@ void main() {
// https://github.com/flutter/flutter/issues/12357 // https://github.com/flutter/flutter/issues/12357
expect( expect(
tester.getSize(find.byKey(keyA)), tester.getSize(find.byKey(keyA)),
anyOf(const Size(79.0, 13.0), const Size(78.0, 13.0)), anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)),
); );
expect(tester.getSize(find.byKey(keyB)), const Size(10.0, 10.0)); expect(tester.getSize(find.byKey(keyB)), const Size(10.0, 10.0));
expect( expect(
tester.getSize(find.byType(Chip).first), tester.getSize(find.byType(Chip).first),
anyOf(const Size(131.0, 32.0), const Size(130.0, 32.0)), anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0)),
); );
expect(tester.getSize(find.byType(Chip).last), const Size(62.0, 32.0)); expect(tester.getSize(find.byType(Chip).last), const Size(58.0, 32.0));
}); });
testWidgets('Chip padding - LTR', (WidgetTester tester) async { testWidgets('Chip padding - LTR', (WidgetTester tester) async {
final GlobalKey keyA = new GlobalKey(); final GlobalKey keyA = new GlobalKey();
final GlobalKey keyB = new GlobalKey(); final GlobalKey keyB = new GlobalKey();
...@@ -442,8 +410,8 @@ void main() { ...@@ -442,8 +410,8 @@ void main() {
child: new Center( child: new Center(
child: new Chip( child: new Chip(
avatar: new Placeholder(key: keyA), avatar: new Placeholder(key: keyA),
label: new Placeholder(key: keyB), label: new Container(key: keyB, width: 40.0, height: 40.0,),
onDeleted: () { }, onDeleted: () {},
), ),
), ),
); );
...@@ -454,12 +422,12 @@ void main() { ...@@ -454,12 +422,12 @@ void main() {
), ),
), ),
); );
expect(tester.getTopLeft(find.byKey(keyA)), const Offset(0.0, 284.0)); expect(tester.getTopLeft(find.byKey(keyA)), const Offset(340.0, 284.0));
expect(tester.getBottomRight(find.byKey(keyA)), const Offset(32.0, 316.0)); expect(tester.getBottomRight(find.byKey(keyA)), const Offset(372.0, 316.0));
expect(tester.getTopLeft(find.byKey(keyB)), const Offset(40.0, 0.0)); expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.0));
expect(tester.getBottomRight(find.byKey(keyB)), const Offset(768.0, 600.0)); expect(tester.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.0));
expect(tester.getTopLeft(find.byType(Icon)), const Offset(772.0, 288.0)); expect(tester.getTopLeft(find.byType(Icon)), const Offset(428.0, 284.0));
expect(tester.getBottomRight(find.byType(Icon)), const Offset(796.0, 312.0)); expect(tester.getBottomRight(find.byType(Icon)), const Offset(460.0, 316.0));
}); });
testWidgets('Chip padding - RTL', (WidgetTester tester) async { testWidgets('Chip padding - RTL', (WidgetTester tester) async {
...@@ -482,8 +450,8 @@ void main() { ...@@ -482,8 +450,8 @@ void main() {
child: new Center( child: new Center(
child: new Chip( child: new Chip(
avatar: new Placeholder(key: keyA), avatar: new Placeholder(key: keyA),
label: new Placeholder(key: keyB), label: new Container(key: keyB, width: 40.0, height: 40.0,),
onDeleted: () { }, onDeleted: () {},
), ),
), ),
); );
...@@ -494,11 +462,12 @@ void main() { ...@@ -494,11 +462,12 @@ void main() {
), ),
), ),
); );
expect(tester.getTopRight(find.byKey(keyA)), const Offset(800.0 - 0.0, 284.0));
expect(tester.getBottomLeft(find.byKey(keyA)), const Offset(800.0 - 32.0, 316.0)); expect(tester.getTopLeft(find.byKey(keyA)), const Offset(428.0, 284.0));
expect(tester.getTopRight(find.byKey(keyB)), const Offset(800.0 - 40.0, 0.0)); expect(tester.getBottomRight(find.byKey(keyA)), const Offset(460.0, 316.0));
expect(tester.getBottomLeft(find.byKey(keyB)), const Offset(800.0 - 768.0, 600.0)); expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.0));
expect(tester.getTopRight(find.byType(Icon)), const Offset(800.0 - 772.0, 288.0)); expect(tester.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.0));
expect(tester.getBottomLeft(find.byType(Icon)), const Offset(800.0 - 796.0, 312.0)); expect(tester.getTopLeft(find.byType(Icon)), const Offset(340.0, 284.0));
expect(tester.getBottomRight(find.byType(Icon)), const Offset(372.0, 316.0));
}); });
} }
...@@ -27,7 +27,7 @@ void main() { ...@@ -27,7 +27,7 @@ void main() {
expect(decoration.color, equals(backgroundColor)); expect(decoration.color, equals(backgroundColor));
final RenderParagraph paragraph = tester.renderObject(find.text('Z')); 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 { testWidgets('CircleAvatar with light background color', (WidgetTester tester) async {
...@@ -50,7 +50,7 @@ void main() { ...@@ -50,7 +50,7 @@ void main() {
expect(decoration.color, equals(backgroundColor)); expect(decoration.color, equals(backgroundColor));
final RenderParagraph paragraph = tester.renderObject(find.text('Z')); 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 { testWidgets('CircleAvatar with foreground color', (WidgetTester tester) async {
...@@ -71,13 +71,13 @@ void main() { ...@@ -71,13 +71,13 @@ void main() {
expect(box.size.height, equals(40.0)); expect(box.size.height, equals(40.0));
final RenderDecoratedBox child = box.child; final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration; 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')); final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(foregroundColor)); 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( final ThemeData theme = new ThemeData(
primaryColor: Colors.grey.shade100, primaryColor: Colors.grey.shade100,
primaryColorBrightness: Brightness.light, primaryColorBrightness: Brightness.light,
...@@ -96,7 +96,32 @@ void main() { ...@@ -96,7 +96,32 @@ void main() {
final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar)); final RenderConstrainedBox box = tester.renderObject(find.byType(CircleAvatar));
final RenderDecoratedBox child = box.child; final RenderDecoratedBox child = box.child;
final BoxDecoration decoration = child.decoration; 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')); final RenderParagraph paragraph = tester.renderObject(find.text('Z'));
expect(paragraph.text.style.color, equals(theme.primaryTextTheme.title.color)); expect(paragraph.text.style.color, equals(theme.primaryTextTheme.title.color));
...@@ -144,6 +169,78 @@ void main() { ...@@ -144,6 +169,78 @@ void main() {
); );
expect(tester.getSize(find.text('Z')), equals(const Size(20.0, 20.0))); 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 }) { 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