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.
diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart
index cfdc230..6b9934d 100644
--- a/packages/flutter/lib/src/material/chip.dart
+++ b/packages/flutter/lib/src/material/chip.dart
@@ -2,20 +2,33 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:flutter/foundation.dart';
-import 'package:flutter/widgets.dart';
-import 'package:flutter/painting.dart';
+import 'dart:math' as math;
-import 'colors.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/painting.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+
import 'debug.dart';
import 'feedback.dart';
import 'icons.dart';
import 'material_localizations.dart';
+import 'theme.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.
///
-/// 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
/// button for deleting the chip.
@@ -40,48 +53,46 @@
/// * [CircleAvatar], which shows images or initials of people.
/// * <https://material.google.com/components/chips.html>
class Chip extends StatelessWidget {
- /// Creates a material design chip.
+ /// Creates a material design chip
///
- /// * [onDeleted] determines whether the chip has a delete button. This
- /// callback runs when the delete button is pressed.
+ /// The [label] and [border] arguments may not be null.
const Chip({
Key key,
this.avatar,
+ this.deleteIcon,
@required this.label,
this.onDeleted,
- TextStyle labelStyle,
+ this.labelStyle,
this.deleteButtonTooltipMessage,
this.backgroundColor,
this.deleteIconColor,
this.border: const StadiumBorder(),
- }) : assert(label != null),
- assert(border != null),
- labelStyle = labelStyle ?? _defaultLabelStyle,
- 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;
+ }) : assert(label != null),
+ assert(border != null),
+ super(key: key);
/// A widget to display prior to the chip's label.
///
/// Typically a [CircleAvatar] widget.
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.
///
/// Typically a [Text] widget.
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;
/// The style to be applied to the chip's label.
@@ -90,7 +101,8 @@
/// such as [Text].
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
/// widget's label.
@@ -101,83 +113,642 @@
/// Defaults to a [StadiumBorder].
final ShapeBorder border;
- /// Color for delete icon, the default being black.
- ///
- /// This has no effect when [onDelete] is null since no delete icon will be
- /// shown.
+ /// Color for delete icon. The default is based on the ambient [IconTheme].
final Color deleteIconColor;
/// 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;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
- final bool deletable = onDeleted != null;
- double startPadding = 12.0;
- double endPadding = 12.0;
-
- final List<Widget> children = <Widget>[];
-
- if (avatar != null) {
- startPadding = 0.0;
- children.add(new ExcludeSemantics(
- child: new Container(
- margin: const EdgeInsetsDirectional.only(end: 8.0),
- width: _chipHeight,
- height: _chipHeight,
- child: avatar,
- ),
- ));
- }
-
- children.add(new Flexible(
- child: new DefaultTextStyle(
- overflow: TextOverflow.ellipsis,
- style: labelStyle,
- 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,
- child: new Container(
- padding: const EdgeInsets.symmetric(horizontal: 4.0),
- child: new Icon(
- Icons.cancel,
- size: 24.0,
- color: deleteIconColor ?? Colors.black54,
+ final ThemeData theme = Theme.of(context);
+ return new DefaultTextStyle(
+ overflow: TextOverflow.fade,
+ textAlign: TextAlign.start,
+ maxLines: 1,
+ softWrap: false,
+ style: labelStyle ??
+ theme.textTheme.body2.copyWith(
+ color: theme.primaryColorDark.withAlpha(_kTextLabelAlpha),
+ ),
+ child: new _ChipRenderWidget(
+ theme: new _ChipRenderTheme(
+ label: label,
+ avatar: avatar,
+ deleteIcon: onDeleted == null
+ ? null
+ : new Tooltip(
+ message: deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip,
+ child: new IconTheme(
+ data: theme.iconTheme.copyWith(
+ color: deleteIconColor ?? theme.iconTheme.color.withAlpha(_kDeleteIconAlpha),
+ ),
+ child: deleteIcon ?? const Icon(Icons.cancel, size: _kDeleteIconSize),
+ ),
+ ),
+ container: new Container(
+ decoration: new ShapeDecoration(
+ shape: border,
+ color: backgroundColor ?? theme.primaryColorDark.withAlpha(_kContainerAlpha),
),
),
+ padding: const EdgeInsets.all(_kEdgePadding),
+ labelPadding: const EdgeInsets.symmetric(horizontal: _kEdgePadding),
),
- ));
- }
-
- return new Semantics(
- container: true,
- child: new Container(
- constraints: const BoxConstraints(minHeight: _chipHeight),
- padding: new EdgeInsetsDirectional.only(start: startPadding, end: endPadding),
- decoration: new ShapeDecoration(
- color: backgroundColor ?? Colors.grey.shade300,
- shape: border,
- ),
- child: new Center(
- widthFactor: 1.0,
- heightFactor: 1.0,
- child: new Row(
- children: children,
- mainAxisSize: MainAxisSize.min,
- ),
- ),
+ key: key,
+ onDeleted: Feedback.wrapForTap(onDeleted, context),
),
);
}
-}
\ No newline at end of file
+}
+
+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;
+ }
+}
diff --git a/packages/flutter/lib/src/material/circle_avatar.dart b/packages/flutter/lib/src/material/circle_avatar.dart
index af9b94c..e8d6aa2 100644
--- a/packages/flutter/lib/src/material/circle_avatar.dart
+++ b/packages/flutter/lib/src/material/circle_avatar.dart
@@ -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 @@
/// 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 @@
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 @@
/// 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 @@
/// 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) {
- switch (ThemeData.estimateBrightnessForColor(backgroundColor)) {
+ TextStyle textStyle = theme.primaryTextTheme.title.copyWith(color: foregroundColor);
+ Color effectiveBackgroundColor = backgroundColor;
+ if (effectiveBackgroundColor == null) {
+ switch (ThemeData.estimateBrightnessForColor(textStyle.color)) {
case Brightness.dark:
- textStyle = textStyle.copyWith(color: Colors.white);
+ effectiveBackgroundColor = theme.primaryColorLight;
break;
case Brightness.light:
- textStyle = textStyle.copyWith(color: Colors.black);
+ effectiveBackgroundColor = theme.primaryColorDark;
+ break;
+ }
+ } else if (foregroundColor == null) {
+ switch (ThemeData.estimateBrightnessForColor(backgroundColor)) {
+ case Brightness.dark:
+ textStyle = textStyle.copyWith(color: theme.primaryColorLight);
+ break;
+ case Brightness.light:
+ 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,
+ ),
+ ),
+ ),
+ ),
);
}
}
diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart
index 85edc3a..600452e 100644
--- a/packages/flutter/lib/src/material/tooltip.dart
+++ b/packages/flutter/lib/src/material/tooltip.dart
@@ -170,9 +170,9 @@
@override
void deactivate() {
+ super.deactivate();
if (_entry != null)
_controller.reverse();
- super.deactivate();
}
@override
diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart
index d1143f6..a669785 100644
--- a/packages/flutter/lib/src/rendering/box.dart
+++ b/packages/flutter/lib/src/rendering/box.dart
@@ -1443,7 +1443,7 @@
/// 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;
diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart
index 7004536..29bf327 100644
--- a/packages/flutter/lib/src/rendering/shifted_box.dart
+++ b/packages/flutter/lib/src/rendering/shifted_box.dart
@@ -590,7 +590,7 @@
/// 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:
diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart
index 32de24a..0934133 100644
--- a/packages/flutter/test/material/chip_test.dart
+++ b/packages/flutter/test/material/chip_test.dart
@@ -11,9 +11,11 @@
/// Tests that a [Chip] that has its size constrained by its parent is
/// further constraining the size of its child, the label widget.
/// Optionally, adding an avatar or delete icon to the chip should not
- /// cause the chip or label to exceed its constrained size.
- Future<Null> _testConstrainedLabel(WidgetTester tester, {
- CircleAvatar avatar, VoidCallback onDeleted,
+ /// cause the chip or label to exceed its constrained height.
+ Future<Null> _testConstrainedLabel(
+ WidgetTester tester, {
+ CircleAvatar avatar,
+ VoidCallback onDeleted,
}) async {
const double labelWidth = 100.0;
const double labelHeight = 50.0;
@@ -55,36 +57,26 @@
testWidgets('Chip control test', (WidgetTester tester) async {
final FeedbackTester feedback = new FeedbackTester();
final List<String> deletedChipLabels = <String>[];
- await tester.pumpWidget(
- new MaterialApp(
+ await tester.pumpWidget(new MaterialApp(
home: new Material(
- child: new Column(
- children: <Widget>[
- new Chip(
- avatar: const CircleAvatar(
- child: const Text('A')
- ),
- label: const Text('Chip A'),
- onDeleted: () {
- deletedChipLabels.add('A');
- },
- deleteButtonTooltipMessage: 'Delete chip A',
- ),
- new Chip(
- avatar: const CircleAvatar(
- child: const Text('B')
- ),
- label: const Text('Chip B'),
- onDeleted: () {
- deletedChipLabels.add('B');
- },
- deleteButtonTooltipMessage: 'Delete chip B',
- ),
- ]
- )
- )
- )
- );
+ child: new Column(children: <Widget>[
+ new Chip(
+ avatar: const CircleAvatar(child: const Text('A')),
+ label: const Text('Chip A'),
+ onDeleted: () {
+ deletedChipLabels.add('A');
+ },
+ deleteButtonTooltipMessage: 'Delete chip A',
+ ),
+ new Chip(
+ avatar: const CircleAvatar(child: const Text('B')),
+ label: const Text('Chip B'),
+ onDeleted: () {
+ deletedChipLabels.add('B');
+ },
+ deleteButtonTooltipMessage: 'Delete chip B',
+ ),
+ ]))));
expect(tester.widget(find.byTooltip('Delete chip A')), isNotNull);
expect(tester.widget(find.byTooltip('Delete chip B')), isNotNull);
@@ -107,17 +99,17 @@
feedback.dispose();
});
- testWidgets('Chip does not constrain size of label widget if it does not exceed '
- 'the available space', (WidgetTester tester) async {
+ testWidgets(
+ 'Chip does not constrain size of label widget if it does not exceed '
+ 'the available space', (WidgetTester tester) async {
const double labelWidth = 50.0;
const double labelHeight = 30.0;
final Key labelKey = new UniqueKey();
await tester.pumpWidget(
- new Directionality(
- textDirection: TextDirection.ltr,
- child: new Material(
- child: new Center(
+ new Material(
+ child: new MaterialApp(
+ home: new Center(
child: new Container(
width: 500.0,
height: 500.0,
@@ -143,36 +135,36 @@
expect(labelSize.height, labelHeight);
});
- testWidgets('Chip constrains the size of the label widget when it exceeds the '
- 'available space', (WidgetTester tester) async {
+ testWidgets(
+ 'Chip constrains the size of the label widget when it exceeds the '
+ 'available space', (WidgetTester tester) async {
await _testConstrainedLabel(tester);
});
- testWidgets('Chip constrains the size of the label widget when it exceeds the '
- 'available space and the avatar is present', (WidgetTester tester) async {
+ testWidgets(
+ 'Chip constrains the size of the label widget when it exceeds the '
+ 'available space and the avatar is present', (WidgetTester tester) async {
await _testConstrainedLabel(
tester,
- avatar: const CircleAvatar(
- child: const Text('A')
- ),
+ avatar: const CircleAvatar(child: const Text('A')),
);
});
- testWidgets('Chip constrains the size of the label widget when it exceeds the '
- 'available space and the delete icon is present', (WidgetTester tester) async {
+ testWidgets(
+ 'Chip constrains the size of the label widget when it exceeds the '
+ 'available space and the delete icon is present', (WidgetTester tester) async {
await _testConstrainedLabel(
tester,
onDeleted: () {},
);
});
- 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 {
+ 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 {
await _testConstrainedLabel(
tester,
- avatar: const CircleAvatar(
- child: const Text('A')
- ),
+ avatar: const CircleAvatar(child: const Text('A')),
onDeleted: () {},
);
});
@@ -228,7 +220,7 @@
return new Material(
child: new Center(
child: new Chip(
- onDeleted: () { },
+ onDeleted: () {},
label: const Text('ABC'),
),
),
@@ -276,15 +268,11 @@
child: new Column(
children: const <Widget>[
const Chip(
- avatar: const CircleAvatar(
- child: const Text('A')
- ),
+ avatar: const CircleAvatar(child: const Text('A')),
label: const Text('Chip A'),
),
const Chip(
- avatar: const CircleAvatar(
- child: const Text('B')
- ),
+ avatar: const CircleAvatar(child: const Text('B')),
label: const Text('Chip B'),
),
],
@@ -297,20 +285,14 @@
// https://github.com/flutter/flutter/issues/12357
expect(
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(
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(
new MaterialApp(
@@ -320,15 +302,11 @@
child: new Column(
children: const <Widget>[
const Chip(
- avatar: const CircleAvatar(
- child: const Text('A')
- ),
+ avatar: const CircleAvatar(child: const Text('A')),
label: const Text('Chip A'),
),
const Chip(
- avatar: const CircleAvatar(
- child: const Text('B')
- ),
+ avatar: const CircleAvatar(child: const Text('B')),
label: const Text('Chip B'),
),
],
@@ -340,12 +318,12 @@
// TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
// 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 B')), anyOf(const Size(234.0, 39.0), const Size(235.0, 39.0)));
- expect(tester.getSize(find.byType(Chip).first).width, anyOf(286.0, 287.0));
- expect(tester.getSize(find.byType(Chip).first).height, equals(39.0));
- expect(tester.getSize(find.byType(Chip).last).width, anyOf(286.0, 287.0));
- expect(tester.getSize(find.byType(Chip).last).height, equals(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(252.0, 42.0), const Size(251.0, 42.0)));
+ expect(tester.getSize(find.byType(Chip).first).width, anyOf(310.0, 309.0));
+ expect(tester.getSize(find.byType(Chip).first).height, equals(42.0));
+ expect(tester.getSize(find.byType(Chip).last).width, anyOf(310.0, 309.0));
+ expect(tester.getSize(find.byType(Chip).last).height, equals(42.0));
// Check that individual text scales are taken into account.
await tester.pumpWidget(
@@ -354,15 +332,11 @@
child: new Column(
children: const <Widget>[
const Chip(
- avatar: const CircleAvatar(
- child: const Text('A')
- ),
+ avatar: const CircleAvatar(child: const Text('A')),
label: const Text('Chip A', textScaleFactor: 3.0),
),
const Chip(
- avatar: const CircleAvatar(
- child: const Text('B')
- ),
+ avatar: const CircleAvatar(child: const Text('B')),
label: const Text('Chip B'),
),
],
@@ -373,11 +347,11 @@
// TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
// 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 B')), anyOf(const Size(78.0, 13.0), const Size(79.0, 13.0)));
- expect(tester.getSize(find.byType(Chip).first).width, anyOf(286.0, 287.0));
- expect(tester.getSize(find.byType(Chip).first).height, equals(39.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.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(84.0, 14.0), const Size(83.0, 14.0)));
+ expect(tester.getSize(find.byType(Chip).first).width, anyOf(310.0, 309.0));
+ expect(tester.getSize(find.byType(Chip).first).height, equals(42.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 {
@@ -389,15 +363,11 @@
child: new Column(
children: <Widget>[
new Chip(
- avatar: const CircleAvatar(
- child: const Text('A')
- ),
+ avatar: const CircleAvatar(child: const Text('A')),
label: new Text('Chip A', key: keyA),
),
new Chip(
- avatar: const CircleAvatar(
- child: const Text('B')
- ),
+ avatar: const CircleAvatar(child: const Text('B')),
label: new Container(key: keyB, width: 10.0, height: 10.0),
),
],
@@ -410,18 +380,16 @@
// https://github.com/flutter/flutter/issues/12357
expect(
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.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 {
final GlobalKey keyA = new GlobalKey();
final GlobalKey keyB = new GlobalKey();
@@ -442,8 +410,8 @@
child: new Center(
child: new Chip(
avatar: new Placeholder(key: keyA),
- label: new Placeholder(key: keyB),
- onDeleted: () { },
+ label: new Container(key: keyB, width: 40.0, height: 40.0,),
+ onDeleted: () {},
),
),
);
@@ -454,12 +422,12 @@
),
),
);
- expect(tester.getTopLeft(find.byKey(keyA)), const Offset(0.0, 284.0));
- expect(tester.getBottomRight(find.byKey(keyA)), const Offset(32.0, 316.0));
- expect(tester.getTopLeft(find.byKey(keyB)), const Offset(40.0, 0.0));
- expect(tester.getBottomRight(find.byKey(keyB)), const Offset(768.0, 600.0));
- expect(tester.getTopLeft(find.byType(Icon)), const Offset(772.0, 288.0));
- expect(tester.getBottomRight(find.byType(Icon)), const Offset(796.0, 312.0));
+ expect(tester.getTopLeft(find.byKey(keyA)), const Offset(340.0, 284.0));
+ expect(tester.getBottomRight(find.byKey(keyA)), const Offset(372.0, 316.0));
+ expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.0));
+ expect(tester.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.0));
+ expect(tester.getTopLeft(find.byType(Icon)), const Offset(428.0, 284.0));
+ expect(tester.getBottomRight(find.byType(Icon)), const Offset(460.0, 316.0));
});
testWidgets('Chip padding - RTL', (WidgetTester tester) async {
@@ -482,8 +450,8 @@
child: new Center(
child: new Chip(
avatar: new Placeholder(key: keyA),
- label: new Placeholder(key: keyB),
- onDeleted: () { },
+ label: new Container(key: keyB, width: 40.0, height: 40.0,),
+ onDeleted: () {},
),
),
);
@@ -494,11 +462,12 @@
),
),
);
- 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.getTopRight(find.byKey(keyB)), const Offset(800.0 - 40.0, 0.0));
- expect(tester.getBottomLeft(find.byKey(keyB)), const Offset(800.0 - 768.0, 600.0));
- expect(tester.getTopRight(find.byType(Icon)), const Offset(800.0 - 772.0, 288.0));
- expect(tester.getBottomLeft(find.byType(Icon)), const Offset(800.0 - 796.0, 312.0));
+
+ expect(tester.getTopLeft(find.byKey(keyA)), const Offset(428.0, 284.0));
+ expect(tester.getBottomRight(find.byKey(keyA)), const Offset(460.0, 316.0));
+ expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.0));
+ expect(tester.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.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));
});
}
diff --git a/packages/flutter/test/material/circle_avatar_test.dart b/packages/flutter/test/material/circle_avatar_test.dart
index 4f71c55..5de7770 100644
--- a/packages/flutter/test/material/circle_avatar_test.dart
+++ b/packages/flutter/test/material/circle_avatar_test.dart
@@ -27,7 +27,7 @@
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 @@
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 @@
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 @@
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 @@
);
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 }) {