Unverified Commit 7a6a65a5 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Adding all the different types of chips, along with their complex behaviors (#16039)

This implements various different chip types: There are now the InputChip, ChoiceChip, FilterChip, ActionChip, and the original vanilla Chip, all of which have different uses. They can be customized in a number of ways. See their docs for more information.

Also fixes #16083
parent 68c77e37
...@@ -10,9 +10,11 @@ import 'package:flutter/painting.dart'; ...@@ -10,9 +10,11 @@ import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'debug.dart'; import 'debug.dart';
import 'feedback.dart';
import 'icons.dart'; import 'icons.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'theme.dart'; import 'theme.dart';
import 'tooltip.dart'; import 'tooltip.dart';
...@@ -20,144 +22,1435 @@ import 'tooltip.dart'; ...@@ -20,144 +22,1435 @@ import 'tooltip.dart';
// Some design constants // Some design constants
const double _kChipHeight = 32.0; const double _kChipHeight = 32.0;
const double _kDeleteIconSize = 18.0; const double _kDeleteIconSize = 18.0;
const int _kTextLabelAlpha = 0xde; const int _kTextLabelAlpha = 0xde; // 87%
const int _kDeleteIconAlpha = 0xde; const double _kDeleteIconOpacity = 0.87;
const int _kContainerAlpha = 0x14; const EdgeInsetsGeometry _kDefaultPadding = const EdgeInsets.all(4.0);
const double _kEdgePadding = 4.0; const EdgeInsetsGeometry _kDefaultLabelPadding = const EdgeInsets.symmetric(horizontal: 8.0);
const EdgeInsetsGeometry _kDefaultAvatarPadding = EdgeInsets.zero;
const EdgeInsetsGeometry _kDefaultDeleteIconPadding = EdgeInsets.zero;
const int _kDisabledAlpha = 0x5e; // 36%
const double _kCheckmarkStrokeWidth = 2.0;
const double _kPressElevation = 8.0;
const Duration _kSelectDuration = const Duration(milliseconds: 195);
const Duration _kCheckmarkDuration = const Duration(milliseconds: 150);
const Duration _kCheckmarkReverseDuration = const Duration(milliseconds: 50);
const Duration _kDrawerDuration = const Duration(milliseconds: 150);
const Duration _kReverseDrawerDuration = const Duration(milliseconds: 100);
const Duration _kDisableDuration = const Duration(milliseconds: 75);
const Color _kSelectScrimColor = const Color(0x60191919);
const Color _kDefaultSelectedColor = const Color(0x30000000); // 19% black
const Color _kDefaultBackgroundColor = const Color(0x14000000); // 8% black
const Color _kDefaultDisabledColor = const Color(0x08000000); // 3% black
const Icon _kDefaultDeleteIcon = const Icon(Icons.cancel, size: _kDeleteIconSize);
/// An interface defining the base attributes for a material design chip.
///
/// Chips are compact elements that represent an attribute, text, entity, or
/// action.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
/// * [Chip], a chip that displays information and can be deleted.
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
/// * [ActionChip], represents an action related to primary content.
/// * <https://material.google.com/components/chips.html>
abstract class ChipAttributes {
/// The primary content of the chip.
///
/// Typically a [Text] widget.
Widget get label;
/// A widget to display prior to the chip's label.
///
/// Typically a [CircleAvatar] widget.
Widget get avatar;
/// The style to be applied to the chip's label.
///
/// This only has an effect on widgets that respect the [DefaultTextStyle],
/// such as [Text].
TextStyle get labelStyle;
/// The border to draw around the chip.
///
/// Defaults to a [StadiumBorder]. Must not be null.
ShapeBorder get border;
/// Color to be used for the unselected, enabled chip's background.
///
/// The default is light grey.
Color get backgroundColor;
/// The padding between the contents of the chip and the outside [border].
///
/// Defaults to 4 logical pixels on all sides.
EdgeInsetsGeometry get padding;
/// The padding around the [label] widget.
///
/// By default, this is 4 logical pixels at the beginning and the end of the
/// label, and zero on top and bottom.
EdgeInsetsGeometry get labelPadding;
/// The padding around the [avatar] widget.
///
/// By default, this is zero on all sides.
EdgeInsetsGeometry get avatarPadding;
}
/// An interface for material design chips that can be deleted.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
/// * [Chip], a chip that displays information and can be deleted.
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * <https://material.google.com/components/chips.html>
abstract class DeletableChipAttributes {
/// The icon displayed when [onDeleted] is set.
///
/// Defaults to an [Icon] widget set to use [Icons.cancel].
Widget get deleteIcon;
/// Called when the user taps the [deleteIcon] to delete the chip.
///
/// If null, the delete button will not appear on the chip.
///
/// The chip will not automatically remove itself: this just tells the app
/// that the user tapped the delete button. In order to delete the chip, you
/// have to do something like the following:
///
/// ## Sample code
///
/// ```dart
/// class Actor {
/// const Actor(this.name, this.initials);
/// final String name;
/// final String initials;
/// }
///
/// class CastList extends StatefulWidget {
/// @override
/// State createState() => new CastListState();
/// }
///
/// class CastListState extends State<CastList> {
/// final List<Actor> _cast = <Actor>[
/// const Actor('Aaron Burr', 'AB'),
/// const Actor('Alexander Hamilton', 'AH'),
/// const Actor('Eliza Hamilton', 'EH'),
/// const Actor('James Madison', 'JM'),
/// ];
///
/// Iterable<Widget> get actorWidgets sync* {
/// for (Actor actor in _cast) {
/// yield new Padding(
/// padding: const EdgeInsets.all(4.0),
/// child: new Chip(
/// avatar: new CircleAvatar(child: new Text(actor.initials)),
/// label: new Text(actor.name),
/// onDeleted: () {
/// setState(() {
/// _cast.removeWhere((Actor entry) {
/// return entry.name == actor.name;
/// });
/// });
/// },
/// ),
/// );
/// }
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return new Wrap(
/// children: actorWidgets.toList(),
/// );
/// }
/// }
/// ```
VoidCallback get onDeleted;
/// The [Color] for the delete icon. The default is based on the ambient
/// [IconTheme.color].
Color get deleteIconColor;
/// The message to be used for the chip's delete button tooltip.
String get deleteButtonTooltipMessage;
/// The padding around the [deleteIcon] widget.
EdgeInsetsGeometry get deleteIconPadding;
}
/// An interface for material design chips that can be selected.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
/// * <https://material.google.com/components/chips.html>
abstract class SelectableChipAttributes {
/// Whether or not this chip is selected.
///
/// If [onSelected] is not null, this value will be used to determine if the
/// select check mark will be shown or not.
///
/// Must not be null. Defaults to false.
bool get selected;
/// Called when the chip should change between selected and deselected states.
///
/// When the chip is tapped, then the [onSelected] callback, if set, will be
/// applied to `!selected` (see [selected]).
///
/// The chip passes the new value to the callback but does not actually
/// change state until the parent widget rebuilds the chip with the new
/// value.
///
/// The callback provided to [onSelected] should update the state of the
/// parent [StatefulWidget] using the [State.setState] method, so that the
/// parent gets rebuilt.
///
/// The [onSelected] and [TappableChipAttributes.onPressed] callbacks must not
/// both be specified at the same time.
///
/// ## Sample code
///
/// ```dart
/// class Wood extends StatefulWidget {
/// @override
/// State<StatefulWidget> createState() => new WoodState();
/// }
///
/// class WoodState extends State<Wood> {
/// bool _useChisel = false;
///
/// @override
/// Widget build(BuildContext context) {
/// return new InputChip(
/// label: const Text('Use Chisel'),
/// selected: _useChisel,
/// onSelected: (bool newValue) {
/// setState(() {
/// _useChisel = newValue;
/// });
/// },
/// );
/// }
/// }
/// ```
ValueChanged<bool> get onSelected;
/// Color to be used for the chip's background, indicating that it is
/// selected.
///
/// The chip is selected when [selected] is true.
Color get selectedColor;
/// Tooltip string to be used for the body area (where the label and avatar
/// are) of the chip.
String get tooltip;
}
/// An interface for material design chips that can be enabled and disabled.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
/// * <https://material.google.com/components/chips.html>
abstract class DisabledChipAttributes {
/// Whether or not this chip is enabled for input.
///
/// If this is true, but all of the user action callbacks are null (i.e.
/// [SelectableChipAttributes.onSelected], [TappableChipAttributes.onPressed],
/// and [DeletableChipAttributes.onDelete]), then the
/// control will still be shown as disabled.
///
/// This is typically used if you want the chip to be disabled, but also show
/// a delete button.
///
/// For classes which don't have this as a constructor argument, [isEnabled]
/// returns true if their user action callback is set.
///
/// Defaults to true. Cannot be null.
bool get isEnabled;
/// Color to be used for the chip's background indicating that it is disabled.
///
/// The chip is disabled when [isEnabled] is false, or all three of
/// [SelectableChipAttributes.onSelected], [TappableChipAttributes.onPressed],
/// and [DeletableChipAttributes.onDelete] are null.
///
/// It defaults to [Colors.black38].
Color get disabledColor;
}
/// An interface for material design chips that can be tapped.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
/// * [ActionChip], represents an action related to primary content.
/// * <https://material.google.com/components/chips.html>
abstract class TappableChipAttributes {
/// Called when the user taps the chip.
///
/// If [onPressed] is set, then this callback will be called when the user
/// taps on the label or avatar parts of the chip. If [onPressed] is null,
/// then the chip will be disabled.
///
/// ## Sample code
///
/// ```dart
/// class Blacksmith extends StatelessWidget {
/// void startHammering() {
/// print('bang bang bang');
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return new InputChip(
/// label: const Text('Apply Hammer'),
/// onPressed: startHammering,
/// );
/// }
/// }
/// ```
VoidCallback get onPressed;
/// Tooltip string to be used for the body area (where the label and avatar
/// are) of the chip.
String get tooltip;
}
/// A material design chip.
///
/// Chips are compact elements that represent an attribute, text, entity, or
/// action.
///
/// Supplying a non-null [onDeleted] callback will cause the chip to include a
/// button for deleting the chip.
///
/// Requires one of its ancestors to be a [Material] widget. The [label],
/// [deleteIcon] and [border] arguments must not be null.
///
/// ## Sample code
///
/// ```dart
/// new Chip(
/// avatar: new CircleAvatar(
/// backgroundColor: Colors.grey.shade800,
/// child: new Text('AB'),
/// ),
/// label: new Text('Aaron Burr'),
/// )
/// ```
///
/// See also:
///
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
/// * [ActionChip], represents an action related to primary content.
/// * [CircleAvatar], which shows images or initials of entities.
/// * [Wrap], A widget that displays its children in multiple horizontal or
/// vertical runs.
/// * <https://material.google.com/components/chips.html>
class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttributes {
/// Creates a material design chip.
///
/// The [label], [deleteIcon] and [border] arguments must not be null.
const Chip({
Key key,
this.avatar,
@required this.label,
this.labelStyle,
this.labelPadding,
this.avatarPadding,
this.deleteIconPadding,
this.deleteIcon,
this.onDeleted,
this.deleteIconColor,
this.deleteButtonTooltipMessage,
this.border: const StadiumBorder(),
this.backgroundColor,
this.padding,
}) : assert(label != null),
assert(border != null),
super(key: key);
@override
final Widget avatar;
@override
final Widget label;
@override
final TextStyle labelStyle;
@override
final EdgeInsetsGeometry labelPadding;
@override
final EdgeInsetsGeometry avatarPadding;
@override
final ShapeBorder border;
@override
final Color backgroundColor;
@override
final EdgeInsetsGeometry padding;
@override
final EdgeInsetsGeometry deleteIconPadding;
@override
final Widget deleteIcon;
@override
final VoidCallback onDeleted;
@override
final Color deleteIconColor;
@override
final String deleteButtonTooltipMessage;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new RawChip(
avatar: avatar,
label: label,
labelStyle: labelStyle,
labelPadding: labelPadding,
avatarPadding: avatarPadding,
deleteIconPadding: deleteIconPadding,
deleteIcon: deleteIcon,
onDeleted: onDeleted,
deleteIconColor: deleteIconColor,
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
tapEnabled: false,
border: border,
backgroundColor: backgroundColor,
padding: padding,
isEnabled: true,
);
}
}
/// A material design input chip.
///
/// Input chips represent a complex piece of information, such as an entity
/// (person, place, or thing) or conversational text, in a compact form.
///
/// Input chips can be made selectable by setting [onSelected], deletable by
/// setting [onDeleted], and pressable like a button with [onPressed]. They have
/// a [label], and they can have a leading icon (see [avatar]) and a trailing
/// icon ([deleteIcon]). Colors and padding can be customized.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// Input chips work together with other UI elements. They can appear:
///
/// * In a [Wrap] widget.
/// * In a horizontally scrollable list, like a [ListView] whose
/// scrollDirection is [Axis.horizontal].
///
/// ## Sample code
///
/// ```dart
/// new InputChip(
/// avatar: new CircleAvatar(
/// backgroundColor: Colors.grey.shade800,
/// child: new Text('AB'),
/// ),
/// label: new Text('Aaron Burr'),
/// onPressed: () {
/// print('I am the one thing in life.');
/// }
/// )
/// ```
///
/// See also:
///
/// * [Chip], a chip that displays information and can be deleted.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
/// * [ActionChip], represents an action related to primary content.
/// * [CircleAvatar], which shows images or initials of people.
/// * [Wrap], A widget that displays its children in multiple horizontal or
/// vertical runs.
/// * <https://material.google.com/components/chips.html>
class InputChip extends StatelessWidget
implements
ChipAttributes,
DeletableChipAttributes,
SelectableChipAttributes,
DisabledChipAttributes,
TappableChipAttributes {
/// Creates an [InputChip].
///
/// The [onPressed] and [onSelected] callbacks must not both be specified at
/// the same time.
///
/// The [label], [isEnabled], [selected], and [border] arguments must not be
/// null.
const InputChip({
Key key,
this.avatar,
@required this.label,
this.labelStyle,
this.labelPadding,
this.avatarPadding,
this.deleteIconPadding,
this.selected: false,
this.isEnabled: true,
this.onSelected,
this.deleteIcon,
this.onDeleted,
this.deleteIconColor,
this.deleteButtonTooltipMessage,
this.onPressed,
this.disabledColor,
this.selectedColor,
this.tooltip,
this.border: const StadiumBorder(),
this.backgroundColor,
this.padding,
}) : assert(selected != null),
assert(isEnabled != null),
assert(label != null),
assert(border != null),
super(key: key);
@override
final Widget avatar;
@override
final Widget label;
@override
final TextStyle labelStyle;
@override
final EdgeInsetsGeometry labelPadding;
@override
final EdgeInsetsGeometry avatarPadding;
@override
final EdgeInsetsGeometry deleteIconPadding;
@override
final bool selected;
@override
final bool isEnabled;
@override
final ValueChanged<bool> onSelected;
@override
final Widget deleteIcon;
@override
final VoidCallback onDeleted;
@override
final Color deleteIconColor;
@override
final String deleteButtonTooltipMessage;
@override
final VoidCallback onPressed;
@override
final Color disabledColor;
@override
final Color selectedColor;
@override
final String tooltip;
@override
final ShapeBorder border;
@override
final Color backgroundColor;
@override
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new RawChip(
avatar: avatar,
label: label,
labelStyle: labelStyle,
labelPadding: labelPadding,
avatarPadding: avatarPadding,
deleteIconPadding: deleteIconPadding,
deleteIcon: deleteIcon,
onDeleted: onDeleted,
deleteIconColor: deleteIconColor,
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
onSelected: onSelected,
onPressed: onPressed,
selected: selected,
tapEnabled: true,
disabledColor: disabledColor,
selectedColor: selectedColor,
tooltip: tooltip,
border: border,
backgroundColor: backgroundColor,
padding: padding,
isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null),
);
}
}
/// A material design choice chip.
///
/// [ChoiceChip]s represent a single choice from a set. Choice chips contain
/// related descriptive text or categories.
///
/// Requires one of its ancestors to be a [Material] widget. The [selected],
/// [label] and [border] arguments must not be null.
///
/// ## Sample code
///
/// ```dart
/// class MyThreeOptions extends StatefulWidget {
/// @override
/// _MyThreeOptionsState createState() => new _MyThreeOptionsState();
/// }
///
/// class _MyThreeOptionsState extends State<MyThreeOptions> {
/// int _value = 1;
///
/// @override
/// Widget build(BuildContext context) {
/// return new Wrap(
/// children: new List<Widget>.generate(
/// 3,
/// (int index) {
/// return new ChoiceChip(
/// label: new Text('Item $index'),
/// selected: _value == index,
/// onSelected: (bool selected) {
/// _value = selected ? index : null;
/// },
/// );
/// },
/// ).toList(),
/// );
/// }
/// }
/// ```
///
/// See also:
///
/// * [Chip], a chip that displays information and can be deleted.
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
/// * [ActionChip], represents an action related to primary content.
/// * [CircleAvatar], which shows images or initials of people.
/// * [Wrap], A widget that displays its children in multiple horizontal or
/// vertical runs.
/// * <https://material.google.com/components/chips.html>
class ChoiceChip extends StatelessWidget
implements
ChipAttributes,
SelectableChipAttributes,
DisabledChipAttributes {
const ChoiceChip({
Key key,
this.avatar,
@required this.label,
this.labelStyle,
this.labelPadding,
this.avatarPadding,
this.onSelected,
@required this.selected,
this.selectedColor,
this.disabledColor,
this.tooltip,
this.border: const StadiumBorder(),
this.backgroundColor,
this.padding,
}) : assert(selected != null),
assert(label != null),
assert(border != null),
super(key: key);
@override
final Widget avatar;
@override
final Widget label;
@override
final TextStyle labelStyle;
@override
final EdgeInsetsGeometry labelPadding;
@override
final EdgeInsetsGeometry avatarPadding;
@override
final ValueChanged<bool> onSelected;
@override
final bool selected;
@override
final Color disabledColor;
@override
final Color selectedColor;
@override
final String tooltip;
@override
final ShapeBorder border;
@override
final Color backgroundColor;
@override
final EdgeInsetsGeometry padding;
@override
bool get isEnabled => onSelected != null;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new RawChip(
avatar: avatar,
label: label,
labelStyle: labelStyle,
labelPadding: labelPadding,
avatarPadding: avatarPadding,
onSelected: onSelected,
selected: selected,
showCheckmark: false,
onDeleted: null,
tooltip: tooltip,
border: border,
disabledColor: disabledColor,
selectedColor: selectedColor,
backgroundColor: backgroundColor,
padding: padding,
isEnabled: isEnabled,
);
}
}
/// A material design filter chip.
///
/// Filter chips use tags or descriptive words as a way to filter content.
///
/// Filter chips are a good alternative to [Checkbox] or [Switch] widgets.
/// Unlike these alternatives, filter chips allow for clearly delineated and
/// exposed options in a compact area.
///
/// Requires one of its ancestors to be a [Material] widget. The [label]
/// and [border] arguments must not be null.
///
/// ## Sample code
///
/// ```dart
/// class ActorFilterEntry {
/// const ActorFilterEntry(this.name, this.initials);
/// final String name;
/// final String initials;
/// }
///
/// class CastFilter extends StatefulWidget {
/// @override
/// State createState() => new CastFilterState();
/// }
///
/// class CastFilterState extends State<CastFilter> {
/// final List<ActorFilterEntry> _cast = <ActorFilterEntry>[
/// const ActorFilterEntry('Aaron Burr', 'AB'),
/// const ActorFilterEntry('Alexander Hamilton', 'AH'),
/// const ActorFilterEntry('Eliza Hamilton', 'EH'),
/// const ActorFilterEntry('James Madison', 'JM'),
/// ];
/// List<String> _filters = <String>[];
///
/// Iterable<Widget> get actorWidgets sync* {
/// for (ActorFilterEntry actor in _cast) {
/// yield new Padding(
/// padding: const EdgeInsets.all(4.0),
/// child: new FilterChip(
/// avatar: new CircleAvatar(child: new Text(actor.initials)),
/// label: new Text(actor.name),
/// selected: _filters.contains(actor.name),
/// onSelected: (bool value) {
/// setState(() {
/// if (value) {
/// _filters.add(actor.name);
/// } else {
/// _filters.removeWhere((String name) {
/// return name == actor.name;
/// });
/// }
/// });
/// },
/// ),
/// );
/// }
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Column(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// new Wrap(
/// children: actorWidgets.toList(),
/// ),
/// new Text('Look for: ${_filters.join(', ')}'),
/// ],
/// );
/// }
/// }
/// ```
///
/// See also:
///
/// * [Chip], a chip that displays information and can be deleted.
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [ActionChip], represents an action related to primary content.
/// * [CircleAvatar], which shows images or initials of people.
/// * [Wrap], A widget that displays its children in multiple horizontal or
/// vertical runs.
/// * <https://material.google.com/components/chips.html>
class FilterChip extends StatelessWidget
implements
ChipAttributes,
SelectableChipAttributes,
DisabledChipAttributes {
const FilterChip({
Key key,
this.avatar,
@required this.label,
this.labelStyle,
this.labelPadding,
this.avatarPadding,
this.selected: false,
@required this.onSelected,
this.disabledColor,
this.selectedColor,
this.tooltip,
this.border: const StadiumBorder(),
this.backgroundColor,
this.padding,
}) : assert(selected != null),
assert(label != null),
assert(border != null),
super(key: key);
@override
final Widget avatar;
@override
final Widget label;
@override
final TextStyle labelStyle;
@override
final EdgeInsetsGeometry labelPadding;
@override
final EdgeInsetsGeometry avatarPadding;
@override
final bool selected;
@override
final ValueChanged<bool> onSelected;
@override
final Color disabledColor;
@override
final Color selectedColor;
@override
final String tooltip;
@override
final ShapeBorder border;
@override
final Color backgroundColor;
@override
final EdgeInsetsGeometry padding;
@override
bool get isEnabled => onSelected != null;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new RawChip(
avatar: avatar,
label: label,
labelStyle: labelStyle,
labelPadding: labelPadding,
avatarPadding: avatarPadding,
onSelected: onSelected,
selected: selected,
tooltip: tooltip,
border: border,
backgroundColor: backgroundColor,
disabledColor: disabledColor,
selectedColor: selectedColor,
padding: padding,
isEnabled: isEnabled,
);
}
}
/// A material design action chip.
///
/// Action chips are a set of options which trigger an action related to primary
/// content. Action chips should appear dynamically and contextually in a UI.
///
/// Action chips can be tapped to trigger an action or show progress and
/// confirmation.
///
/// Action chips are displayed after primary content, such as below a card or
/// persistently at the bottom of a screen.
///
/// The material button widgets, [RaisedButton], [FlatButton], [OutlineButton]
/// are an alternative to action chips, which should appear statically and
/// consistently in a UI.
///
/// Requires one of its ancestors to be a [Material] widget. The [onPressed],
/// [label] and [border] arguments must not be null.
///
/// ## Sample code
///
/// ```dart
/// new ActionChip(
/// avatar: new CircleAvatar(
/// backgroundColor: Colors.grey.shade800,
/// child: new Text('AB'),
/// ),
/// label: new Text('Aaron Burr'),
/// onPressed: () {
/// print("If you stand for nothing, Burr, what’ll you fall for?");
/// }
/// )
/// ```
///
/// See also:
///
/// * [Chip], a chip that displays information and can be deleted.
/// * [InputChip], a chip that represents a complex piece of information, such
/// as an entity (person, place, or thing) or conversational text, in a
/// compact form.
/// * [ChoiceChip], allows a single selection from a set of options. Choice
/// chips contain related descriptive text or categories.
/// * [CircleAvatar], which shows images or initials of people.
/// * [Wrap], A widget that displays its children in multiple horizontal or
/// vertical runs.
/// * <https://material.google.com/components/chips.html>
class ActionChip extends StatelessWidget implements ChipAttributes, TappableChipAttributes {
const ActionChip({
Key key,
this.avatar,
@required this.label,
this.labelStyle,
this.labelPadding,
this.avatarPadding,
@required this.onPressed,
this.tooltip,
this.border: const StadiumBorder(),
this.backgroundColor,
this.padding,
}) : assert(label != null),
assert(border != null),
assert(onPressed != null),
super(key: key);
@override
final Widget avatar;
@override
final Widget label;
@override
final TextStyle labelStyle;
@override
final EdgeInsetsGeometry labelPadding;
@override
final EdgeInsetsGeometry avatarPadding;
@override
final VoidCallback onPressed;
@override
final String tooltip;
@override
final ShapeBorder border;
@override
final Color backgroundColor;
@override
final EdgeInsetsGeometry padding;
/// A material design chip. @override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new RawChip(
avatar: avatar,
label: label,
onPressed: onPressed,
tooltip: tooltip,
labelStyle: labelStyle,
backgroundColor: backgroundColor,
border: border,
padding: padding,
labelPadding: labelPadding,
avatarPadding: avatarPadding,
isEnabled: true,
);
}
}
/// A raw material design chip.
/// ///
/// Chips represent complex entities in small blocks, such as a contact, or a /// This serves as the basis for all of the chip widget types to aggregate.
/// choice. /// It is typically not created directly, one of the other chip types
/// that are appropriate for the use case are used instead:
/// ///
/// Supplying a non-null [onDeleted] callback will cause the chip to include a /// * [Chip] a simple chip that can only display information and be deleted.
/// button for deleting the chip. /// * [InputChip] represents a complex piece of information, such as an entity
/// (person, place, or thing) or conversational text, in a compact form.
/// * [ChoiceChip] allows a single selection from a set of options.
/// * [FilterChip] a chip that uses tags or descriptive words as a way to
/// filter content.
/// * [ActionChip]s display a set of actions related to primary content.
/// ///
/// Requires one of its ancestors to be a [Material] widget. The [label] /// Raw chips are typically only used if you want to create your own custom chip
/// and [border] arguments must not be null. /// type.
/// ///
/// ## Sample code /// Raw chips can be selected by setting [onSelected], deleted by setting
/// [onDeleted], and pushed like a button with [onPressed]. They have a [label],
/// and they can have a leading icon (see [avatar]) and a trailing icon
/// ([deleteIcon]). Colors and padding can be customized.
/// ///
/// ```dart /// Requires one of its ancestors to be a [Material] widget.
/// new Chip(
/// avatar: new CircleAvatar(
/// backgroundColor: Colors.grey.shade800,
/// child: new Text('AB'),
/// ),
/// label: new Text('Aaron Burr'),
/// )
/// ```
/// ///
/// See also: /// See also:
/// ///
/// * [CircleAvatar], which shows images or initials of people. /// * [CircleAvatar], which shows images or initials of people.
/// * [Wrap], A widget that displays its children in multiple horizontal or
/// vertical runs.
/// * <https://material.google.com/components/chips.html> /// * <https://material.google.com/components/chips.html>
class Chip extends StatelessWidget { class RawChip extends StatefulWidget
/// Creates a material design chip. implements
ChipAttributes,
DeletableChipAttributes,
SelectableChipAttributes,
DisabledChipAttributes,
TappableChipAttributes {
/// Creates a RawChip
/// ///
/// The [label] and [border] arguments may not be null. /// The [onPressed] and [onSelected] callbacks must not both be specified at
const Chip({ /// the same time.
///
/// The [label], [isEnabled], and [border] arguments must not be null.
const RawChip({
Key key, Key key,
this.avatar, this.avatar,
this.deleteIcon,
@required this.label, @required this.label,
this.onDeleted,
this.labelStyle, this.labelStyle,
this.deleteButtonTooltipMessage, EdgeInsetsGeometry padding,
this.backgroundColor, EdgeInsetsGeometry labelPadding,
EdgeInsetsGeometry avatarPadding,
EdgeInsetsGeometry deleteIconPadding,
Widget deleteIcon,
this.onDeleted,
this.deleteIconColor, this.deleteIconColor,
this.border: const StadiumBorder(), this.deleteButtonTooltipMessage,
this.onPressed,
this.onSelected,
this.tapEnabled: true,
this.selected,
this.showCheckmark: true,
this.isEnabled: true,
Color disabledColor,
Color selectedColor,
this.tooltip,
@required this.border,
Color backgroundColor,
}) : assert(label != null), }) : assert(label != null),
assert(border != null), assert(border != null),
assert(isEnabled != null),
padding = padding ?? _kDefaultPadding,
labelPadding = labelPadding ?? _kDefaultLabelPadding,
avatarPadding = avatarPadding ?? _kDefaultAvatarPadding,
deleteIconPadding = deleteIconPadding ?? _kDefaultDeleteIconPadding,
deleteIcon = deleteIcon ?? _kDefaultDeleteIcon,
disabledColor = disabledColor ?? _kDefaultDisabledColor,
selectedColor = selectedColor ?? _kDefaultSelectedColor,
backgroundColor = backgroundColor ?? _kDefaultBackgroundColor,
super(key: key); super(key: key);
/// A widget to display prior to the chip's label. @override
///
/// Typically a [CircleAvatar] widget.
final Widget avatar; final Widget avatar;
@override
final Widget label;
@override
final TextStyle labelStyle;
@override
final EdgeInsetsGeometry labelPadding;
@override
final EdgeInsetsGeometry avatarPadding;
@override
final EdgeInsetsGeometry deleteIconPadding;
@override
final Widget deleteIcon;
@override
final VoidCallback onDeleted;
@override
final Color deleteIconColor;
@override
final String deleteButtonTooltipMessage;
@override
final ValueChanged<bool> onSelected;
@override
final VoidCallback onPressed;
@override
final bool selected;
@override
final bool isEnabled;
@override
final Color disabledColor;
@override
final Color selectedColor;
@override
final String tooltip;
@override
final ShapeBorder border;
@override
final Color backgroundColor;
@override
final EdgeInsetsGeometry padding;
/// The icon displayed when [onDeleted] is non-null. /// Whether or not to show a check mark when [selected] is true.
/// ///
/// This has no effect when [onDeleted] is null since no delete icon will be /// For instance, the [ChoiceChip] sets this to false so that it can be
/// shown. /// be selected without showing the check mark.
/// ///
/// Defaults to an [Icon] widget containing [Icons.cancel]. /// Defaults to true.
final Widget deleteIcon; final bool showCheckmark;
/// The primary content of the chip. /// If set, this indicates that the chip should be disabled if all of the
/// tap callbacks ([onSelected], [onPressed]) are null.
/// ///
/// Typically a [Text] widget. /// For example, the [Chip] class sets this to false because it can't be
final Widget label; /// disabled, even if no callbacks are set on it, since it is used for
/// displaying information only.
/// Called when the user taps the delete button to delete the chip.
/// ///
/// This has no effect when [deleteIcon] is null since no delete icon will be /// Defaults to true.
/// shown. final bool tapEnabled;
final VoidCallback onDeleted;
/// The style to be applied to the chip's label. @override
/// _RawChipState createState() => new _RawChipState();
/// This only has effect on widgets that respect the [DefaultTextStyle], }
/// such as [Text].
final TextStyle labelStyle;
/// Color to be used for the chip's background, the default is based on the class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip> {
/// ambient [IconTheme]. static const Duration pressedAnimationDuration = const Duration(milliseconds: 75);
///
/// This color is used as the background of the container that will hold the AnimationController selectController;
/// widget's label. AnimationController avatarDrawerController;
final Color backgroundColor; AnimationController deleteDrawerController;
AnimationController enableController;
Animation<double> checkmarkAnimation;
Animation<double> avatarDrawerAnimation;
Animation<double> deleteDrawerAnimation;
Animation<double> enableAnimation;
Animation<double> selectionFade;
static final Tween<double> pressedShadowTween = new Tween<double>(
begin: 0.0,
end: _kPressElevation,
);
bool get hasDeleteButton => widget.onDeleted != null;
bool get hasAvatar => widget.avatar != null;
bool get canTap {
return widget.isEnabled
&& widget.tapEnabled
&& (widget.onPressed != null || widget.onSelected != null);
}
/// The border to draw around the chip. bool _isTapping = false;
/// bool get isTapping => !canTap ? false : _isTapping;
/// Defaults to a [StadiumBorder].
final ShapeBorder border;
/// Color for delete icon. The default is based on the ambient [IconTheme]. @override
final Color deleteIconColor; void initState() {
assert(widget.onSelected == null || widget.onPressed == null);
super.initState();
selectController = new AnimationController(
duration: _kSelectDuration,
value: widget.selected == true ? 1.0 : 0.0,
vsync: this,
);
selectionFade = new CurvedAnimation(
parent: selectController,
curve: Curves.fastOutSlowIn,
);
avatarDrawerController = new AnimationController(
duration: _kDrawerDuration,
value: hasAvatar || widget.selected == true ? 1.0 : 0.0,
vsync: this,
);
deleteDrawerController = new AnimationController(
duration: _kDrawerDuration,
value: hasDeleteButton ? 1.0 : 0.0,
vsync: this,
);
enableController = new AnimationController(
duration: _kDisableDuration,
value: widget.isEnabled ? 1.0 : 0.0,
vsync: this,
);
/// Message to be used for the chip delete button's tooltip. // These will delay the start of some animations, and/or reduce their
final String deleteButtonTooltipMessage; // length compared to the overall select animation, using Intervals.
final double checkmarkPercentage = _kCheckmarkDuration.inMilliseconds /
_kSelectDuration.inMilliseconds;
final double checkmarkReversePercentage = _kCheckmarkReverseDuration.inMilliseconds /
_kSelectDuration.inMilliseconds;
final double avatarDrawerReversePercentage = _kReverseDrawerDuration.inMilliseconds /
_kSelectDuration.inMilliseconds;
checkmarkAnimation = new CurvedAnimation(
parent: selectController,
curve: new Interval(1.0 - checkmarkPercentage, 1.0, curve: Curves.fastOutSlowIn),
reverseCurve: new Interval(
1.0 - checkmarkReversePercentage,
1.0,
curve: Curves.fastOutSlowIn,
),
);
deleteDrawerAnimation = new CurvedAnimation(
parent: deleteDrawerController,
curve: Curves.fastOutSlowIn,
);
avatarDrawerAnimation = new CurvedAnimation(
parent: avatarDrawerController,
curve: Curves.fastOutSlowIn,
reverseCurve: new Interval(
1.0 - avatarDrawerReversePercentage,
1.0,
curve: Curves.fastOutSlowIn,
),
);
enableAnimation = new CurvedAnimation(
parent: enableController,
curve: Curves.fastOutSlowIn,
);
}
@override
void dispose() {
selectController.dispose();
avatarDrawerController.dispose();
deleteDrawerController.dispose();
enableController.dispose();
super.dispose();
}
void _handleTapDown(TapDownDetails details) {
if (!canTap) {
return;
}
setState(() {
_isTapping = true;
});
}
void _handleTapCancel() {
if (!canTap) {
return;
}
setState(() {
_isTapping = false;
});
}
void _handleTap() {
if (!canTap) {
return;
}
setState(() {
_isTapping = false;
});
// Only one of these can be set, so only one will be called.
widget.onSelected?.call(!widget.selected);
widget.onPressed?.call();
}
/// Picks between three different colors, depending upon the state of two
/// different animations.
Color get backgroundColor {
final ColorTween backgroundTween = new ColorTween(
begin: widget.disabledColor,
end: widget.backgroundColor,
);
final ColorTween selectTween = new ColorTween(
begin: backgroundTween.evaluate(enableController),
end: widget.selectedColor,
);
return selectTween.evaluate(selectionFade);
}
@override
void didUpdateWidget(RawChip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isEnabled != widget.isEnabled) {
setState(() {
if (widget.isEnabled) {
enableController.forward();
} else {
enableController.reverse();
}
});
}
if (oldWidget.avatar != widget.avatar || oldWidget.selected != widget.selected) {
setState(() {
if (hasAvatar || widget.selected == true) {
avatarDrawerController.forward();
} else {
avatarDrawerController.reverse();
}
});
}
if (oldWidget.selected != widget.selected) {
setState(() {
if (widget.selected == true) {
selectController.forward();
} else {
selectController.reverse();
}
});
}
if (oldWidget.onDeleted != widget.onDeleted) {
setState(() {
if (hasDeleteButton) {
deleteDrawerController.forward();
} else {
deleteDrawerController.reverse();
}
});
}
}
Widget _wrapWithTooltip(Widget child, String tooltip, VoidCallback callback) {
if (child == null || callback == null || tooltip == null) {
return child;
}
return new Tooltip(
message: tooltip,
child: child,
);
}
Widget _buildDeleteIcon(BuildContext context, ThemeData theme) {
if (!hasDeleteButton) {
return null;
}
return _wrapWithTooltip(
new InkResponse(
onTap: widget.isEnabled ? widget.onDeleted : null,
child: new IconTheme(
data: theme.iconTheme.copyWith(
color: widget.deleteIconColor ?? theme.iconTheme.color,
opacity: _kDeleteIconOpacity,
),
child: widget.deleteIcon,
),
),
widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip,
widget.onDeleted,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
return new DefaultTextStyle( final TextDirection textDirection = Directionality.of(context);
overflow: TextOverflow.fade, return new Material(
textAlign: TextAlign.start, elevation: isTapping ? _kPressElevation : 0.0,
maxLines: 1, animationDuration: pressedAnimationDuration,
softWrap: false, shape: widget.border,
style: labelStyle ?? child: new InkResponse(
theme.textTheme.body2.copyWith( onTap: canTap ? _handleTap : null,
color: theme.primaryColorDark.withAlpha(_kTextLabelAlpha), onTapDown: canTap ? _handleTapDown : null,
), onTapCancel: canTap ? _handleTapCancel : null,
child: new _ChipRenderWidget( child: new AnimatedBuilder(
theme: new _ChipRenderTheme( animation: new Listenable.merge(<Listenable>[selectController, enableController]),
label: label, builder: (BuildContext context, Widget child) {
avatar: avatar, return new Container(
deleteIcon: onDeleted == null decoration: new ShapeDecoration(
? null shape: widget.border,
: new Tooltip( color: backgroundColor,
message: deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip, ),
child: new IconTheme( child: child,
data: theme.iconTheme.copyWith( );
color: deleteIconColor ?? theme.iconTheme.color.withAlpha(_kDeleteIconAlpha), },
), child: _wrapWithTooltip(
child: deleteIcon ?? const Icon(Icons.cancel, size: _kDeleteIconSize), new _ChipRenderWidget(
theme: new _ChipRenderTheme(
label: new DefaultTextStyle(
overflow: TextOverflow.fade,
textAlign: TextAlign.start,
maxLines: 1,
softWrap: false,
style: widget.labelStyle ??
theme.textTheme.body2.copyWith(
color: Colors.black.withAlpha(_kTextLabelAlpha),
),
child: widget.label,
),
avatar: new AnimatedChildSwitcher(
child: widget.avatar,
duration: _kDrawerDuration,
switchInCurve: Curves.fastOutSlowIn,
), ),
deleteIcon: new AnimatedChildSwitcher(
child: _buildDeleteIcon(context, theme),
duration: _kDrawerDuration,
switchInCurve: Curves.fastOutSlowIn,
),
padding: widget.padding?.resolve(textDirection),
labelPadding: widget.labelPadding?.resolve(textDirection),
avatarPadding: widget.avatarPadding?.resolve(textDirection),
deleteIconPadding: widget.deleteIconPadding?.resolve(textDirection),
showAvatar: hasAvatar,
showCheckmark: widget.showCheckmark,
canTapBody: canTap,
), ),
container: new Container( value: widget.selected,
decoration: new ShapeDecoration( checkmarkAnimation: checkmarkAnimation,
shape: border, enableAnimation: enableAnimation,
color: backgroundColor ?? theme.primaryColorDark.withAlpha(_kContainerAlpha), avatarDrawerAnimation: avatarDrawerAnimation,
), deleteDrawerAnimation: deleteDrawerAnimation,
), isEnabled: widget.isEnabled,
padding: const EdgeInsets.all(_kEdgePadding), ),
labelPadding: const EdgeInsets.symmetric(horizontal: _kEdgePadding), widget.tooltip,
widget.onPressed),
), ),
key: key,
onDeleted: Feedback.wrapForTap(onDeleted, context),
), ),
); );
} }
...@@ -167,12 +1460,22 @@ class _ChipRenderWidget extends RenderObjectWidget { ...@@ -167,12 +1460,22 @@ class _ChipRenderWidget extends RenderObjectWidget {
const _ChipRenderWidget({ const _ChipRenderWidget({
Key key, Key key,
@required this.theme, @required this.theme,
this.onDeleted, this.value,
this.isEnabled,
this.checkmarkAnimation,
this.avatarDrawerAnimation,
this.deleteDrawerAnimation,
this.enableAnimation,
}) : assert(theme != null), }) : assert(theme != null),
super(key: key); super(key: key);
final _ChipRenderTheme theme; final _ChipRenderTheme theme;
final VoidCallback onDeleted; final bool value;
final bool isEnabled;
final Animation<double> checkmarkAnimation;
final Animation<double> avatarDrawerAnimation;
final Animation<double> deleteDrawerAnimation;
final Animation<double> enableAnimation;
@override @override
_RenderChipElement createElement() => new _RenderChipElement(this); _RenderChipElement createElement() => new _RenderChipElement(this);
...@@ -182,7 +1485,12 @@ class _ChipRenderWidget extends RenderObjectWidget { ...@@ -182,7 +1485,12 @@ class _ChipRenderWidget extends RenderObjectWidget {
renderObject renderObject
..theme = theme ..theme = theme
..textDirection = Directionality.of(context) ..textDirection = Directionality.of(context)
..onDeleted = onDeleted; ..value = value
..isEnabled = isEnabled
..checkmarkAnimation = checkmarkAnimation
..avatarDrawerAnimation = avatarDrawerAnimation
..deleteDrawerAnimation = deleteDrawerAnimation
..enableAnimation = enableAnimation;
} }
@override @override
...@@ -190,7 +1498,12 @@ class _ChipRenderWidget extends RenderObjectWidget { ...@@ -190,7 +1498,12 @@ class _ChipRenderWidget extends RenderObjectWidget {
return new _RenderChip( return new _RenderChip(
theme: theme, theme: theme,
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
onDeleted: onDeleted, value: value,
isEnabled: isEnabled,
checkmarkAnimation: checkmarkAnimation,
avatarDrawerAnimation: avatarDrawerAnimation,
deleteDrawerAnimation: deleteDrawerAnimation,
enableAnimation: enableAnimation,
); );
} }
} }
...@@ -199,7 +1512,6 @@ enum _ChipSlot { ...@@ -199,7 +1512,6 @@ enum _ChipSlot {
label, label,
avatar, avatar,
deleteIcon, deleteIcon,
container,
} }
class _RenderChipElement extends RenderObjectElement { class _RenderChipElement extends RenderObjectElement {
...@@ -247,7 +1559,6 @@ class _RenderChipElement extends RenderObjectElement { ...@@ -247,7 +1559,6 @@ class _RenderChipElement extends RenderObjectElement {
_mountChild(widget.theme.avatar, _ChipSlot.avatar); _mountChild(widget.theme.avatar, _ChipSlot.avatar);
_mountChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon); _mountChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon);
_mountChild(widget.theme.label, _ChipSlot.label); _mountChild(widget.theme.label, _ChipSlot.label);
_mountChild(widget.theme.container, _ChipSlot.container);
} }
void _updateChild(Widget widget, _ChipSlot slot) { void _updateChild(Widget widget, _ChipSlot slot) {
...@@ -270,7 +1581,6 @@ class _RenderChipElement extends RenderObjectElement { ...@@ -270,7 +1581,6 @@ class _RenderChipElement extends RenderObjectElement {
_updateChild(widget.theme.label, _ChipSlot.label); _updateChild(widget.theme.label, _ChipSlot.label);
_updateChild(widget.theme.avatar, _ChipSlot.avatar); _updateChild(widget.theme.avatar, _ChipSlot.avatar);
_updateChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon); _updateChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon);
_updateChild(widget.theme.container, _ChipSlot.container);
} }
void _updateRenderObject(RenderObject child, _ChipSlot slot) { void _updateRenderObject(RenderObject child, _ChipSlot slot) {
...@@ -284,9 +1594,6 @@ class _RenderChipElement extends RenderObjectElement { ...@@ -284,9 +1594,6 @@ class _RenderChipElement extends RenderObjectElement {
case _ChipSlot.deleteIcon: case _ChipSlot.deleteIcon:
renderObject.deleteIcon = child; renderObject.deleteIcon = child;
break; break;
case _ChipSlot.container:
renderObject.container = child;
break;
} }
} }
...@@ -320,17 +1627,25 @@ class _ChipRenderTheme { ...@@ -320,17 +1627,25 @@ class _ChipRenderTheme {
@required this.avatar, @required this.avatar,
@required this.label, @required this.label,
@required this.deleteIcon, @required this.deleteIcon,
@required this.container,
@required this.padding, @required this.padding,
@required this.labelPadding, @required this.labelPadding,
@required this.avatarPadding,
@required this.deleteIconPadding,
@required this.showAvatar,
@required this.showCheckmark,
@required this.canTapBody,
}); });
final Widget avatar; final Widget avatar;
final Widget label; final Widget label;
final Widget deleteIcon; final Widget deleteIcon;
final Widget container;
final EdgeInsets padding; final EdgeInsets padding;
final EdgeInsets labelPadding; final EdgeInsets labelPadding;
final EdgeInsets avatarPadding;
final EdgeInsets deleteIconPadding;
final bool showAvatar;
final bool showCheckmark;
final bool canTapBody;
@override @override
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
...@@ -341,12 +1656,16 @@ class _ChipRenderTheme { ...@@ -341,12 +1656,16 @@ class _ChipRenderTheme {
return false; return false;
} }
final _ChipRenderTheme typedOther = other; final _ChipRenderTheme typedOther = other;
return typedOther.avatar == avatar && return typedOther.avatar == avatar
typedOther.label == label && && typedOther.label == label
typedOther.deleteIcon == deleteIcon && && typedOther.deleteIcon == deleteIcon
typedOther.container == container && && typedOther.padding == padding
typedOther.padding == padding && && typedOther.labelPadding == labelPadding
typedOther.labelPadding == labelPadding; && typedOther.avatarPadding == avatarPadding
&& typedOther.deleteIconPadding == deleteIconPadding
&& typedOther.showAvatar == showAvatar
&& typedOther.showCheckmark == showCheckmark
&& typedOther.canTapBody == canTapBody;
} }
@override @override
...@@ -355,9 +1674,13 @@ class _ChipRenderTheme { ...@@ -355,9 +1674,13 @@ class _ChipRenderTheme {
avatar, avatar,
label, label,
deleteIcon, deleteIcon,
container,
padding, padding,
labelPadding, labelPadding,
avatarPadding,
deleteIconPadding,
showAvatar,
showCheckmark,
canTapBody,
); );
} }
} }
...@@ -366,30 +1689,33 @@ class _RenderChip extends RenderBox { ...@@ -366,30 +1689,33 @@ class _RenderChip extends RenderBox {
_RenderChip({ _RenderChip({
@required _ChipRenderTheme theme, @required _ChipRenderTheme theme,
@required TextDirection textDirection, @required TextDirection textDirection,
this.onDeleted, this.value,
this.isEnabled,
this.checkmarkAnimation,
this.avatarDrawerAnimation,
this.deleteDrawerAnimation,
this.enableAnimation,
}) : assert(theme != null), }) : assert(theme != null),
assert(textDirection != null), assert(textDirection != null),
_theme = theme, _theme = theme,
_textDirection = textDirection { _textDirection = textDirection {
_tap = new TapGestureRecognizer(debugOwner: this) checkmarkAnimation.addListener(markNeedsPaint);
..onTapDown = _handleTapDown avatarDrawerAnimation.addListener(markNeedsLayout);
..onTap = _handleTap; deleteDrawerAnimation.addListener(markNeedsLayout);
enableAnimation.addListener(markNeedsPaint);
} }
// 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<_ChipSlot, RenderBox> slotToChild = <_ChipSlot, RenderBox>{};
final Map<RenderBox, _ChipSlot> childToSlot = <RenderBox, _ChipSlot>{}; final Map<RenderBox, _ChipSlot> childToSlot = <RenderBox, _ChipSlot>{};
TapGestureRecognizer _tap; bool value;
bool isEnabled;
VoidCallback onDeleted; Rect deleteButtonRect;
Rect _deleteButtonRect; Rect pressRect;
Rect _actionRect; Animation<double> checkmarkAnimation;
Offset _tapDownLocation; Animation<double> avatarDrawerAnimation;
Animation<double> deleteDrawerAnimation;
Animation<double> enableAnimation;
RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _ChipSlot slot) { RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _ChipSlot slot) {
if (oldChild != null) { if (oldChild != null) {
...@@ -423,12 +1749,6 @@ class _RenderChip extends RenderBox { ...@@ -423,12 +1749,6 @@ class _RenderChip extends RenderBox {
_label = _updateChild(_label, value, _ChipSlot.label); _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 get theme => _theme;
_ChipRenderTheme _theme; _ChipRenderTheme _theme;
set theme(_ChipRenderTheme value) { set theme(_ChipRenderTheme value) {
...@@ -460,33 +1780,10 @@ class _RenderChip extends RenderBox { ...@@ -460,33 +1780,10 @@ class _RenderChip extends RenderBox {
if (deleteIcon != null) { if (deleteIcon != null) {
yield deleteIcon; 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() { bool get isDrawingCheckmark => theme.showCheckmark && !(checkmarkAnimation?.isDismissed ?? !value);
if (_tapDownLocation == null) { bool get deleteIconShowing => !deleteDrawerAnimation.isDismissed;
return;
}
if (deleteIcon != null && onDeleted != null && _deleteButtonRect.contains(_tapDownLocation)) {
onDeleted();
}
}
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
...@@ -526,7 +1823,6 @@ class _RenderChip extends RenderBox { ...@@ -526,7 +1823,6 @@ class _RenderChip extends RenderBox {
add(avatar, 'avatar'); add(avatar, 'avatar');
add(label, 'label'); add(label, 'label');
add(deleteIcon, 'deleteIcon'); add(deleteIcon, 'deleteIcon');
add(container, 'container');
return value; return value;
} }
...@@ -542,11 +1838,13 @@ class _RenderChip extends RenderBox { ...@@ -542,11 +1838,13 @@ class _RenderChip extends RenderBox {
} }
static double _minHeight(RenderBox box, double width) { static double _minHeight(RenderBox box, double width) {
return box == null ? 0.0 : box.getMinIntrinsicWidth(width); return box == null ? 0.0 : box.getMinIntrinsicHeight(width);
} }
static Size _boxSize(RenderBox box) => box == null ? Size.zero : box.size; static Size _boxSize(RenderBox box) => box == null ? Size.zero : box.size;
static Rect _boxRect(RenderBox box) => box == null ? Rect.zero : _boxParentData(box).offset & box.size;
static BoxParentData _boxParentData(RenderBox box) => box.parentData; static BoxParentData _boxParentData(RenderBox box) => box.parentData;
@override @override
...@@ -554,25 +1852,34 @@ class _RenderChip extends RenderBox { ...@@ -554,25 +1852,34 @@ class _RenderChip extends RenderBox {
// The overall padding isn't affected by missing avatar or delete icon // 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 // because we add the padding regardless to give extra padding for the label
// when they're missing. // when they're missing.
final double overallPadding = theme.labelPadding.horizontal + _iconPadding.horizontal * 2.0; final double overallPadding = theme.padding.horizontal +
return overallPadding + _minWidth(avatar, height) + _minWidth(label, height) + _minWidth(deleteIcon, height); theme.labelPadding.horizontal +
theme.deleteIconPadding.horizontal +
theme.avatarPadding.horizontal;
return overallPadding +
_minWidth(avatar, height) +
_minWidth(label, height) +
_minWidth(deleteIcon, height);
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
// The overall padding isn't affected by missing avatar or delete icon final double overallPadding = theme.padding.vertical +
// because we add the padding regardless to give extra padding for the label theme.labelPadding.horizontal +
// when they're missing. theme.deleteIconPadding.horizontal +
final double overallPadding = theme.labelPadding.horizontal + _iconPadding.horizontal * 2.0; theme.avatarPadding.horizontal;
return overallPadding + _maxWidth(avatar, height) + _maxWidth(label, height) + _maxWidth(deleteIcon, height); return overallPadding +
_maxWidth(avatar, height) +
_maxWidth(label, height) +
_maxWidth(deleteIcon, height);
} }
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
// This widget is sized to the height of the label only, as long as it's return math.max(
// larger than _kChipHeight. The other widgets are sized to match the _kChipHeight,
// label. theme.padding.vertical + theme.labelPadding.vertical + _minHeight(label, width),
return math.max(_kChipHeight, theme.labelPadding.vertical + _minHeight(label, width)); );
} }
@override @override
...@@ -584,142 +1891,343 @@ class _RenderChip extends RenderBox { ...@@ -584,142 +1891,343 @@ class _RenderChip extends RenderBox {
return label.computeDistanceToActualBaseline(baseline); return label.computeDistanceToActualBaseline(baseline);
} }
@override Size _layoutLabel(double iconSizes, Size size) {
void performLayout() { // Now that we know the label height and the width of the icons, we can
double overallHeight = _kChipHeight; // determine how much to shrink the width constraints for the "real" layout.
if (label != null) { if (constraints.maxWidth.isFinite) {
label.layout(constraints.loosen(), parentUsesSize: true); label.layout(
// Now that we know the height, we can determine how much to shrink the constraints.loosen().copyWith(
// constraints by for the "real" layout. Ignored if the constraints are maxWidth: math.max(
// infinite. 0.0,
overallHeight = math.max(overallHeight, _boxSize(label).height); constraints.maxWidth - iconSizes - theme.labelPadding.horizontal,
if (constraints.maxWidth.isFinite) { ),
final double allPadding = _iconPadding.horizontal * 2.0 + theme.labelPadding.horizontal; maxHeight: size.height),
final double iconSizes = (avatar != null ? overallHeight - _iconPadding.vertical : 0.0) parentUsesSize: true,
+ (deleteIcon != null ? overallHeight - _iconPadding.vertical : 0.0); );
label.layout( } else {
constraints.loosen().copyWith( label.layout(new BoxConstraints.tight(size), parentUsesSize: true);
maxWidth: math.max(0.0, constraints.maxWidth - iconSizes - allPadding),
),
parentUsesSize: true,
);
}
} }
final double labelWidth = theme.labelPadding.horizontal + _boxSize(label).width; final Size rawSize = _boxSize(label);
final double iconSize = overallHeight - _iconPadding.vertical; return new Size(
final BoxConstraints iconConstraints = new BoxConstraints.tightFor( rawSize.width + theme.labelPadding.horizontal,
width: iconSize, rawSize.height + theme.labelPadding.vertical,
height: iconSize,
); );
double avatarWidth = _iconPadding.horizontal; }
if (avatar != null) {
avatar.layout(iconConstraints, parentUsesSize: true); Size _layoutAvatar(BoxConstraints contentConstraints, double contentSize) {
avatarWidth += _boxSize(avatar).width; final double requestedSize = math.max(0.0, contentSize - theme.avatarPadding.vertical);
final BoxConstraints avatarConstraints = new BoxConstraints.tightFor(
width: requestedSize,
height: requestedSize,
);
avatar.layout(avatarConstraints, parentUsesSize: true);
if (!theme.showCheckmark && !theme.showAvatar) {
return new Size(0.0, contentSize);
} }
double deleteIconWidth = _iconPadding.horizontal; double avatarWidth = theme.avatarPadding.horizontal;
if (deleteIcon != null) { double avatarHeight = theme.avatarPadding.vertical;
deleteIcon.layout(iconConstraints, parentUsesSize: true); final Size avatarBoxSize = _boxSize(avatar);
deleteIconWidth += _boxSize(deleteIcon).width; if (theme.showAvatar) {
avatarWidth += avatarDrawerAnimation.value * avatarBoxSize.width;
} else {
avatarWidth += avatarDrawerAnimation.value * contentSize;
} }
final double overallWidth = avatarWidth + labelWidth + deleteIconWidth; avatarHeight += avatarBoxSize.height;
return new Size(avatarWidth, avatarHeight);
}
if (container != null) { Size _layoutDeleteIcon(BoxConstraints contentConstraints, double contentSize) {
final BoxConstraints containerConstraints = new BoxConstraints.tightFor( final double requestedSize = math.max(0.0, contentSize - theme.deleteIconPadding.vertical);
height: overallHeight, final BoxConstraints deleteIconConstraints = new BoxConstraints.tightFor(
width: overallWidth, width: requestedSize,
); height: requestedSize,
container.layout(containerConstraints, parentUsesSize: true); );
_boxParentData(container).offset = Offset.zero; deleteIcon.layout(deleteIconConstraints, parentUsesSize: true);
if (!deleteIconShowing) {
return new Size(0.0, contentSize);
} }
double deleteIconWidth = theme.deleteIconPadding.horizontal;
double deleteIconHeight = theme.deleteIconPadding.vertical;
final Size boxSize = _boxSize(deleteIcon);
deleteIconWidth += deleteDrawerAnimation.value * boxSize.width;
deleteIconHeight += boxSize.height;
return new Size(deleteIconWidth, deleteIconHeight);
}
double centerLayout(RenderBox box, double x) { @override
_boxParentData(box).offset = new Offset(x, (overallHeight - box.size.height) / 2.0); void performLayout() {
return box.size.width; final BoxConstraints contentConstraints = constraints.loosen();
} // Find out the height of the label within the constraints.
label.layout(contentConstraints, parentUsesSize: true);
final double contentSize = math.max(
_kChipHeight - theme.padding.vertical + theme.labelPadding.vertical,
_boxSize(label).height + theme.labelPadding.vertical,
);
final Size avatarSize = _layoutAvatar(contentConstraints, contentSize);
final Size deleteIconSize = _layoutDeleteIcon(contentConstraints, contentSize);
Size labelSize = new Size(_boxSize(label).width, contentSize);
labelSize = _layoutLabel(avatarSize.width + deleteIconSize.width, labelSize);
// This is the overall size of the content: it doesn't include
// theme.padding, that is added in at the end.
final Size overallSize = new Size(
avatarSize.width + labelSize.width + deleteIconSize.width,
contentSize,
);
// Now we have all of the dimensions. Place the children where they belong.
const double left = 0.0; const double left = 0.0;
final double right = overallWidth; final double right = overallSize.width;
Offset centerLayout(Size boxSize, double x) {
assert(contentSize >= boxSize.height);
Offset boxOffset;
switch (textDirection) {
case TextDirection.rtl:
boxOffset = new Offset(x - boxSize.width, (contentSize - boxSize.height) / 2.0);
break;
case TextDirection.ltr:
boxOffset = new Offset(x, (contentSize - boxSize.height) / 2.0);
break;
}
return boxOffset;
}
// These are the offsets to the upper left corners of the boxes (including
// the child's padding) containing the children, for each child, but not
// including the overall padding.
Offset avatarOffset = Offset.zero;
Offset labelOffset = Offset.zero;
Offset deleteIconOffset = Offset.zero;
switch (textDirection) { switch (textDirection) {
case TextDirection.rtl: case TextDirection.rtl:
double start = right - _kEdgePadding; double start = right;
if (avatar != null) { if (theme.showCheckmark || theme.showAvatar) {
start -= centerLayout(avatar, start - avatar.size.width); avatarOffset = centerLayout(avatarSize, start);
} start -= avatarSize.width;
start -= _iconPadding.left + theme.labelPadding.right;
if (label != null) {
start -= centerLayout(label, start - label.size.width);
} }
start -= _iconPadding.right + theme.labelPadding.left; labelOffset = centerLayout(labelSize, start);
double deleteButtonWidth = 0.0; start -= labelSize.width;
if (deleteIcon != null) { if (deleteIconShowing) {
_deleteButtonRect = new Rect.fromLTWH( deleteButtonRect = new Rect.fromLTWH(
0.0, 0.0,
0.0, 0.0,
iconSize + _iconPadding.horizontal, deleteIconSize.width + theme.padding.right,
iconSize + _iconPadding.vertical, overallSize.height + theme.padding.vertical,
); );
deleteButtonWidth = _deleteButtonRect.width; deleteIconOffset = centerLayout(deleteIconSize, start);
start -= centerLayout(deleteIcon, start - deleteIcon.size.width); } else {
deleteButtonRect = Rect.zero;
} }
if (avatar != null || label != null) { start -= deleteIconSize.width;
_actionRect = new Rect.fromLTWH( if (theme.canTapBody) {
deleteButtonWidth, pressRect = new Rect.fromLTWH(
deleteButtonRect.width,
0.0, 0.0,
overallWidth - deleteButtonWidth, overallSize.width - deleteButtonRect.width + theme.padding.horizontal,
overallHeight, overallSize.height + theme.padding.vertical,
); );
} else {
pressRect = Rect.zero;
} }
break; break;
case TextDirection.ltr: case TextDirection.ltr:
double start = left + _kEdgePadding; double start = left;
if (avatar != null) { if (theme.showCheckmark || theme.showAvatar) {
start += centerLayout(avatar, start); avatarOffset = centerLayout(avatarSize, start - _boxSize(avatar).width + avatarSize.width);
start += avatarSize.width;
} }
start += _iconPadding.right + theme.labelPadding.left; labelOffset = centerLayout(labelSize, start);
if (label != null) { start += labelSize.width;
start += centerLayout(label, start); if (theme.canTapBody) {
} pressRect = new Rect.fromLTWH(
start += _iconPadding.left + theme.labelPadding.right;
if (avatar != null || label != null) {
_actionRect = new Rect.fromLTWH(
0.0, 0.0,
0.0, 0.0,
deleteIcon != null ? (start - _kEdgePadding) : overallWidth, deleteIconShowing
overallHeight, ? start + theme.padding.left
: overallSize.width + theme.padding.horizontal,
overallSize.height + theme.padding.vertical,
); );
} else {
pressRect = Rect.zero;
} }
if (deleteIcon != null) { start -= _boxSize(deleteIcon).width - deleteIconSize.width;
_deleteButtonRect = new Rect.fromLTWH( if (deleteIconShowing) {
start - _kEdgePadding, deleteIconOffset = centerLayout(deleteIconSize, start);
deleteButtonRect = new Rect.fromLTWH(
start + theme.padding.left,
0.0, 0.0,
iconSize + _iconPadding.horizontal, deleteIconSize.width + theme.padding.right,
iconSize + _iconPadding.vertical, overallSize.height + theme.padding.vertical,
); );
centerLayout(deleteIcon, start); } else {
deleteButtonRect = Rect.zero;
} }
//assert(start + deleteIconSize.width == overallSize.width);
break; break;
} }
// Center the label vertically.
labelOffset = labelOffset +
new Offset(
0.0,
((labelSize.height - theme.labelPadding.vertical) - _boxSize(label).height) / 2.0,
);
_boxParentData(avatar).offset = theme.padding.topLeft + avatarOffset + theme.avatarPadding.topLeft;
_boxParentData(label).offset = theme.padding.topLeft + labelOffset + theme.labelPadding.topLeft;
_boxParentData(deleteIcon).offset = theme.padding.topLeft + deleteIconOffset + theme.deleteIconPadding.topLeft;
final Size paddedSize = new Size(
overallSize.width + theme.padding.horizontal,
overallSize.height + theme.padding.vertical,
);
size = constraints.constrain(paddedSize);
assert(
size.height == constraints.constrainHeight(paddedSize.height),
"Constrained height ${size.height} doesn't match expected height "
'${constraints.constrainWidth(paddedSize.height)}');
assert(
size.width == constraints.constrainWidth(paddedSize.width),
"Constrained width ${size.width} doesn't match expected width "
'${constraints.constrainWidth(paddedSize.width)}');
}
size = constraints.constrain(new Size(overallWidth, overallHeight)); static final ColorTween enableTween = new ColorTween(
assert(size.width == constraints.constrainWidth(overallWidth)); begin: Colors.white.withAlpha(_kDisabledAlpha),
assert(size.height == constraints.constrainHeight(overallHeight)); end: Colors.white,
);
static final ColorTween selectionScrimTween = new ColorTween(
begin: Colors.transparent,
end: _kSelectScrimColor,
);
Color get _disabledColor {
if (enableAnimation == null || enableAnimation.isCompleted) {
return Colors.white;
}
return enableTween.evaluate(enableAnimation);
} }
@override void _paintCheck(Canvas canvas, Offset origin, double size) {
void paint(PaintingContext context, Offset offset) { Color paintColor = theme.showAvatar ? Colors.white : Colors.black87;
void doPaint(RenderBox child) { final ColorTween fadeTween = new ColorTween(begin: Colors.transparent, end: paintColor);
if (child != null) {
paintColor = checkmarkAnimation.status == AnimationStatus.reverse
? fadeTween.evaluate(checkmarkAnimation)
: paintColor;
final Paint paint = new Paint()
..color = paintColor
..style = PaintingStyle.stroke
..strokeWidth = _kCheckmarkStrokeWidth * (avatar != null ? avatar.size.height / 24.0 : 1.0);
final double t = checkmarkAnimation.status == AnimationStatus.reverse
? 1.0
: checkmarkAnimation.value;
if (t == 0.0) {
// Nothing to draw.
return;
}
assert(t > 0.0 && t <= 1.0);
// As t goes from 0.0 to 1.0, animate the two check mark strokes from the
// short side to the long side.
final Path path = new Path();
final Offset start = new Offset(size * 0.15, size * 0.45);
final Offset mid = new Offset(size * 0.4, size * 0.7);
final Offset end = new Offset(size * 0.85, size * 0.25);
if (t < 0.5) {
final double strokeT = t * 2.0;
final Offset drawMid = Offset.lerp(start, mid, strokeT);
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy);
} else {
final double strokeT = (t - 0.5) * 2.0;
final Offset drawEnd = Offset.lerp(mid, end, strokeT);
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
}
canvas.drawPath(path, paint);
}
void _paintSelectionOverlay(PaintingContext context, Offset offset) {
if (isDrawingCheckmark) {
final Rect avatarRect = _boxRect(avatar).shift(offset);
final Paint darkenPaint = new Paint()
..color = selectionScrimTween.evaluate(checkmarkAnimation)
..blendMode = BlendMode.srcATop;
context.canvas.drawRect(avatarRect, darkenPaint);
// Need to make the check mark be a little smaller than the avatar.
final double checkSize = avatar.size.height * 0.75;
final Offset checkOffset = _boxParentData(avatar).offset +
new Offset(avatar.size.height * 0.125, avatar.size.height * 0.125);
_paintCheck(context.canvas, offset + checkOffset, checkSize);
}
}
void _paintAvatar(PaintingContext context, Offset offset) {
void paintWithOverlay(PaintingContext context, Offset offset) {
context.paintChild(avatar, _boxParentData(avatar).offset + offset);
_paintSelectionOverlay(context, offset);
}
if (theme.showAvatar == false && avatarDrawerAnimation.isDismissed) {
return;
}
final int disabledColorAlpha = _disabledColor.alpha;
if (needsCompositing) {
context.pushLayer(new OpacityLayer(alpha: disabledColorAlpha), paintWithOverlay, offset);
} else {
if (disabledColorAlpha != 0xff) {
context.canvas.saveLayer(
_boxRect(avatar).shift(offset).inflate(20.0),
new Paint()..color = _disabledColor,
);
}
paintWithOverlay(context, offset);
if (disabledColorAlpha != 0xff) {
context.canvas.restore();
}
}
}
void _paintChild(PaintingContext context, Offset offset, RenderBox child, bool isEnabled) {
if (child == null) {
return;
}
final int disabledColorAlpha = _disabledColor.alpha;
if (!enableAnimation.isCompleted) {
if (needsCompositing) {
context.pushLayer(
new OpacityLayer(alpha: disabledColorAlpha),
(PaintingContext context, Offset offset) {
context.paintChild(child, _boxParentData(child).offset + offset);
},
offset,
);
} else {
final Rect childRect = _boxRect(child).shift(offset);
context.canvas.saveLayer(childRect.inflate(20.0), new Paint()..color = _disabledColor);
context.paintChild(child, _boxParentData(child).offset + offset); context.paintChild(child, _boxParentData(child).offset + offset);
context.canvas.restore();
} }
} else {
context.paintChild(child, _boxParentData(child).offset + offset);
} }
}
doPaint(container); @override
doPaint(avatar); void paint(PaintingContext context, Offset offset) {
doPaint(deleteIcon); _paintAvatar(context, offset);
doPaint(label); if (deleteIconShowing) {
_paintChild(context, offset, deleteIcon, isEnabled);
}
_paintChild(context, offset, label, isEnabled);
} }
// 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;
@override @override
void debugPaint(PaintingContext context, Offset offset) { void debugPaint(PaintingContext context, Offset offset) {
assert(!_debugShowTapTargetOutlines || assert(!_debugShowTapTargetOutlines ||
...@@ -730,11 +2238,11 @@ class _RenderChip extends RenderBox { ...@@ -730,11 +2238,11 @@ class _RenderChip extends RenderBox {
..color = const Color(0xff800000) ..color = const Color(0xff800000)
..strokeWidth = 1.0 ..strokeWidth = 1.0
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
if (deleteIcon != null) { if (deleteIconShowing) {
context.canvas.drawRect(_deleteButtonRect.shift(offset), outlinePaint); context.canvas.drawRect(deleteButtonRect.shift(offset), outlinePaint);
} }
context.canvas.drawRect( context.canvas.drawRect(
_actionRect.shift(offset), pressRect.shift(offset),
outlinePaint..color = const Color(0xff008000), outlinePaint..color = const Color(0xff008000),
); );
return true; return true;
...@@ -742,11 +2250,16 @@ class _RenderChip extends RenderBox { ...@@ -742,11 +2250,16 @@ class _RenderChip extends RenderBox {
} }
@override @override
bool hitTestSelf(Offset position) => true; bool hitTestSelf(Offset position) => deleteButtonRect.contains(position) || pressRect.contains(position);
@override @override
bool hitTestChildren(HitTestResult result, {@required Offset position}) { bool hitTestChildren(HitTestResult result, {@required Offset position}) {
assert(position != null); assert(position != null);
if (deleteIcon != null && deleteButtonRect.contains(position)) {
// This simulates a position at the center of the delete icon if the hit
// on the chip is inside of the delete area.
return deleteIcon.hitTest(result, position: (Offset.zero & _boxSize(deleteIcon)).center);
}
for (RenderBox child in _children) { for (RenderBox child in _children) {
if (child.hasSize && child.hitTest(result, position: position - _boxParentData(child).offset)) { if (child.hasSize && child.hitTest(result, position: position - _boxParentData(child).offset)) {
return true; return true;
......
...@@ -1820,9 +1820,10 @@ abstract class RenderBox extends RenderObject { ...@@ -1820,9 +1820,10 @@ abstract class RenderBox extends RenderObject {
/// absorbs the hit (preventing objects below this one from being hit). /// absorbs the hit (preventing objects below this one from being hit).
/// Returns false if the hit can continue to other objects below this one. /// Returns false if the hit can continue to other objects below this one.
/// ///
/// The caller is responsible for transforming [position] into the local /// The caller is responsible for transforming [position] from global
/// coordinate space of the callee. The callee is responsible for checking /// coordinates to its location relative to the origin of this [RenderBox].
/// whether the given position is within its bounds. /// This [RenderBox] is responsible for checking whether the given position is
/// within its bounds.
/// ///
/// Hit testing requires layout to be up-to-date but does not require painting /// Hit testing requires layout to be up-to-date but does not require painting
/// to be up-to-date. That means a render object can rely upon [performLayout] /// to be up-to-date. That means a render object can rely upon [performLayout]
...@@ -1870,6 +1871,11 @@ abstract class RenderBox extends RenderObject { ...@@ -1870,6 +1871,11 @@ abstract class RenderBox extends RenderObject {
/// Override this method if this render object can be hit even if its /// Override this method if this render object can be hit even if its
/// children were not hit. /// children were not hit.
/// ///
/// The caller is responsible for transforming [position] from global
/// coordinates to its location relative to the origin of this [RenderBox].
/// This [RenderBox] is responsible for checking whether the given position is
/// within its bounds.
///
/// Used by [hitTest]. If you override [hitTest] and do not call this /// Used by [hitTest]. If you override [hitTest] and do not call this
/// function, then you don't need to implement this function. /// function, then you don't need to implement this function.
@protected @protected
...@@ -1882,6 +1888,11 @@ abstract class RenderBox extends RenderObject { ...@@ -1882,6 +1888,11 @@ abstract class RenderBox extends RenderObject {
/// hit tests at locations where children overlap hit the child that is /// hit tests at locations where children overlap hit the child that is
/// visually "on top" (i.e., paints later). /// visually "on top" (i.e., paints later).
/// ///
/// The caller is responsible for transforming [position] from global
/// coordinates to its location relative to the origin of this [RenderBox].
/// This [RenderBox] is responsible for checking whether the given position is
/// within its bounds.
///
/// Used by [hitTest]. If you override [hitTest] and do not call this /// Used by [hitTest]. If you override [hitTest] and do not call this
/// function, then you don't need to implement this function. /// function, then you don't need to implement this function.
@protected @protected
......
...@@ -2,58 +2,76 @@ ...@@ -2,58 +2,76 @@
// 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 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
void main() { Finder findRenderChipElement() {
/// Tests that a [Chip] that has its size constrained by its parent is return find.byElementPredicate((Element e) => '${e.runtimeType}' == '_RenderChipElement');
/// 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 height.
Future<Null> _testConstrainedLabel(
WidgetTester tester, {
CircleAvatar avatar,
VoidCallback onDeleted,
}) async {
const double labelWidth = 100.0;
const double labelHeight = 50.0;
const double chipParentWidth = 75.0;
const double chipParentHeight = 25.0;
final Key labelKey = new UniqueKey();
await tester.pumpWidget( dynamic getRenderChip(WidgetTester tester) {
new MaterialApp( if (!tester.any(findRenderChipElement())) {
home: new Material( return null;
child: new Center( }
child: new Container( final Element element = tester.element(findRenderChipElement());
width: chipParentWidth, return element.renderObject;
height: chipParentHeight, }
child: new Chip(
avatar: avatar, double getSelectProgress(WidgetTester tester) => getRenderChip(tester)?.checkmarkAnimation?.value;
label: new Container( double getAvatarDrawerProgress(WidgetTester tester) => getRenderChip(tester)?.avatarDrawerAnimation?.value;
key: labelKey, double getDeleteDrawerProgress(WidgetTester tester) => getRenderChip(tester)?.deleteDrawerAnimation?.value;
width: labelWidth, double getEnableProgress(WidgetTester tester) => getRenderChip(tester)?.enableAnimation?.value;
height: labelHeight,
), /// Tests that a [Chip] that has its size constrained by its parent is
onDeleted: onDeleted, /// 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 height.
Future<Null> _testConstrainedLabel(
WidgetTester tester, {
CircleAvatar avatar,
VoidCallback onDeleted,
}) async {
const double labelWidth = 100.0;
const double labelHeight = 50.0;
const double chipParentWidth = 75.0;
const double chipParentHeight = 25.0;
final Key labelKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Center(
child: new Container(
width: chipParentWidth,
height: chipParentHeight,
child: new Chip(
avatar: avatar,
label: new Container(
key: labelKey,
width: labelWidth,
height: labelHeight,
), ),
onDeleted: onDeleted,
), ),
), ),
), ),
), ),
); ),
);
final Size labelSize = tester.getSize(find.byKey(labelKey)); final Size labelSize = tester.getSize(find.byKey(labelKey));
expect(labelSize.width, lessThan(chipParentWidth)); expect(labelSize.width, lessThan(chipParentWidth));
expect(labelSize.height, lessThanOrEqualTo(chipParentHeight)); expect(labelSize.height, lessThanOrEqualTo(chipParentHeight));
final Size chipSize = tester.getSize(find.byType(Chip)); final Size chipSize = tester.getSize(find.byType(Chip));
expect(chipSize.width, chipParentWidth); expect(chipSize.width, chipParentWidth);
expect(chipSize.height, chipParentHeight); expect(chipSize.height, chipParentHeight);
} }
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>[];
...@@ -182,7 +200,7 @@ void main() { ...@@ -182,7 +200,7 @@ void main() {
), ),
), ),
); );
expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0)); expect(tester.getSize(find.byType(Text)), const Size(40.0, 24.0));
expect(tester.getSize(find.byType(Chip)), const Size(64.0, 32.0)); expect(tester.getSize(find.byType(Chip)), const Size(64.0, 32.0));
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
...@@ -212,17 +230,17 @@ void main() { ...@@ -212,17 +230,17 @@ void main() {
expect(tester.getSize(find.byType(Chip)), const Size(800.0, 32.0)); expect(tester.getSize(find.byType(Chip)), const Size(800.0, 32.0));
}); });
testWidgets('Chip supports RTL', (WidgetTester tester) async { testWidgets('Chip elements are ordered horizontally for locale', (WidgetTester tester) async {
final UniqueKey iconKey = new UniqueKey();
final Widget test = new Overlay( final Widget test = new Overlay(
initialEntries: <OverlayEntry>[ initialEntries: <OverlayEntry>[
new OverlayEntry( new OverlayEntry(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
child: new Center( child: new Chip(
child: new Chip( deleteIcon: new Icon(Icons.delete, key: iconKey),
onDeleted: () {}, onDeleted: () {},
label: const Text('ABC'), label: const Text('ABC'),
),
), ),
); );
}, },
...@@ -243,8 +261,8 @@ void main() { ...@@ -243,8 +261,8 @@ void main() {
), ),
), ),
); );
expect(tester.getCenter(find.text('ABC')).dx, greaterThan(tester.getCenter(find.byType(Icon)).dx)); await tester.pumpAndSettle(const Duration(milliseconds: 500));
expect(tester.getCenter(find.text('ABC')).dx, greaterThan(tester.getCenter(find.byKey(iconKey)).dx));
await tester.pumpWidget( await tester.pumpWidget(
new Localizations( new Localizations(
locale: const Locale('en', 'US'), locale: const Locale('en', 'US'),
...@@ -258,7 +276,8 @@ void main() { ...@@ -258,7 +276,8 @@ void main() {
), ),
), ),
); );
expect(tester.getCenter(find.text('ABC')).dx, lessThan(tester.getCenter(find.byType(Icon)).dx)); await tester.pumpAndSettle(const Duration(milliseconds: 500));
expect(tester.getCenter(find.text('ABC')).dx, lessThan(tester.getCenter(find.byKey(iconKey)).dx));
}); });
testWidgets('Chip responds to textScaleFactor', (WidgetTester tester) async { testWidgets('Chip responds to textScaleFactor', (WidgetTester tester) async {
...@@ -320,10 +339,10 @@ void main() { ...@@ -320,10 +339,10 @@ void main() {
// https://github.com/flutter/flutter/issues/12357 // https://github.com/flutter/flutter/issues/12357
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 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.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).width, anyOf(318.0, 319.0));
expect(tester.getSize(find.byType(Chip).first).height, equals(42.0)); expect(tester.getSize(find.byType(Chip).first).height, equals(50.0));
expect(tester.getSize(find.byType(Chip).last).width, anyOf(310.0, 309.0)); expect(tester.getSize(find.byType(Chip).last).width, anyOf(318.0, 319.0));
expect(tester.getSize(find.byType(Chip).last).height, equals(42.0)); expect(tester.getSize(find.byType(Chip).last).height, equals(50.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(
...@@ -349,8 +368,8 @@ void main() { ...@@ -349,8 +368,8 @@ void main() {
// https://github.com/flutter/flutter/issues/12357 // https://github.com/flutter/flutter/issues/12357
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 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.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).width, anyOf(318.0, 319.0));
expect(tester.getSize(find.byType(Chip).first).height, equals(42.0)); expect(tester.getSize(find.byType(Chip).first).height, equals(50.0));
expect(tester.getSize(find.byType(Chip).last), 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)));
}); });
...@@ -390,6 +409,47 @@ void main() { ...@@ -390,6 +409,47 @@ void main() {
expect(tester.getSize(find.byType(Chip).last), const Size(58.0, 32.0)); expect(tester.getSize(find.byType(Chip).last), const Size(58.0, 32.0));
}); });
testWidgets('Avatars can be non-circle avatar widgets', (WidgetTester tester) async {
final Key keyA = new GlobalKey();
await tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Column(
children: <Widget>[
new Chip(
avatar: new Container(key: keyA, width: 20.0, height: 20.0),
label: const Text('Chip A'),
),
],
),
),
),
);
expect(tester.getSize(find.byKey(keyA)), equals(const Size(20.0, 20.0)));
});
testWidgets('Delete icons can be non-icon widgets', (WidgetTester tester) async {
final Key keyA = new GlobalKey();
await tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Column(
children: <Widget>[
new Chip(
deleteIcon: new Container(key: keyA, width: 20.0, height: 20.0),
label: const Text('Chip A'),
onDeleted: () {},
),
],
),
),
),
);
expect(tester.getSize(find.byKey(keyA)), equals(const Size(20.0, 20.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();
...@@ -410,7 +470,11 @@ void main() { ...@@ -410,7 +470,11 @@ 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 Container(key: keyB, width: 40.0, height: 40.0,), label: new Container(
key: keyB,
width: 40.0,
height: 40.0,
),
onDeleted: () {}, onDeleted: () {},
), ),
), ),
...@@ -422,12 +486,12 @@ void main() { ...@@ -422,12 +486,12 @@ void main() {
), ),
), ),
); );
expect(tester.getTopLeft(find.byKey(keyA)), const Offset(340.0, 284.0)); expect(tester.getTopLeft(find.byKey(keyA)), const Offset(332.0, 280.0));
expect(tester.getBottomRight(find.byKey(keyA)), const Offset(372.0, 316.0)); expect(tester.getBottomRight(find.byKey(keyA)), const Offset(372.0, 320.0));
expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.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.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.0));
expect(tester.getTopLeft(find.byType(Icon)), const Offset(428.0, 284.0)); expect(tester.getTopLeft(find.byType(Icon)), const Offset(439.0, 291.0));
expect(tester.getBottomRight(find.byType(Icon)), const Offset(460.0, 316.0)); expect(tester.getBottomRight(find.byType(Icon)), const Offset(457.0, 309.0));
}); });
testWidgets('Chip padding - RTL', (WidgetTester tester) async { testWidgets('Chip padding - RTL', (WidgetTester tester) async {
...@@ -450,7 +514,11 @@ void main() { ...@@ -450,7 +514,11 @@ 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 Container(key: keyB, width: 40.0, height: 40.0,), label: new Container(
key: keyB,
width: 40.0,
height: 40.0,
),
onDeleted: () {}, onDeleted: () {},
), ),
), ),
...@@ -463,11 +531,466 @@ void main() { ...@@ -463,11 +531,466 @@ void main() {
), ),
); );
expect(tester.getTopLeft(find.byKey(keyA)), const Offset(428.0, 284.0)); expect(tester.getTopLeft(find.byKey(keyA)), const Offset(428.0, 280.0));
expect(tester.getBottomRight(find.byKey(keyA)), const Offset(460.0, 316.0)); expect(tester.getBottomRight(find.byKey(keyA)), const Offset(468.0, 320.0));
expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.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.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.0));
expect(tester.getTopLeft(find.byType(Icon)), const Offset(340.0, 284.0)); expect(tester.getTopLeft(find.byType(Icon)), const Offset(343.0, 291.0));
expect(tester.getBottomRight(find.byType(Icon)), const Offset(372.0, 316.0)); expect(tester.getBottomRight(find.byType(Icon)), const Offset(361.0, 309.0));
});
testWidgets('Avatar drawer works as expected on RawChip', (WidgetTester tester) async {
final GlobalKey labelKey = new GlobalKey();
Future<Null> pushChip({Widget avatar}) async {
return tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Wrap(
children: <Widget>[
new RawChip(
avatar: avatar,
label: new Text('Chip', key: labelKey),
border: const StadiumBorder(),
),
],
),
),
),
);
}
// No avatar
await pushChip();
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0)));
final GlobalKey avatarKey = new GlobalKey();
// Add an avatar
await pushChip(
avatar: new Container(
key: avatarKey,
color: const Color(0xff000000),
width: 40.0,
height: 40.0,
),
);
// Avatar drawer should start out closed.
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0)));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(-20.0, 4.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
await tester.pump(const Duration(milliseconds: 20));
// Avatar drawer should start expanding.
expect(tester.getSize(find.byType(RawChip)).width, closeTo(81.2, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(-18.8, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(13.2, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(86.7, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(-13.3, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(18.6, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(94.7, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(-5.3, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(26.7, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(99.5, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(-0.5, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(31.5, 0.1));
// Wait for being done with animation, and make sure it didn't change
// height.
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0)));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 4.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 9.0)));
// Remove the avatar again
await pushChip();
// Avatar drawer should start out open.
expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0)));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 4.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 9.0)));
await tester.pump(const Duration(milliseconds: 20));
// Avatar drawer should start contracting.
expect(tester.getSize(find.byType(RawChip)).width, closeTo(102.9, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(2.9, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(34.9, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(98.0, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(-2.0, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(30.0, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(84.1, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(-15.9, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(16.1, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(80.0, 0.1));
expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(avatarKey)).dx, closeTo(-20.0, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)).dx, closeTo(12.0, 0.1));
// Wait for being done with animation, make sure it didn't change
// height, and make sure that the avatar is no longer drawn.
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
expect(find.byKey(avatarKey), findsNothing);
});
testWidgets('Delete button drawer works as expected on RawChip', (WidgetTester tester) async {
final UniqueKey labelKey = new UniqueKey();
final UniqueKey deleteButtonKey = new UniqueKey();
bool wasDeleted = false;
Future<Null> pushChip({bool deletable: false}) async {
return tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Wrap(
children: <Widget>[
new StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return new RawChip(
onDeleted: deletable
? () {
setState(() {
wasDeleted = true;
});
}
: null,
deleteIcon: new Container(width: 40.0, height: 40.0, key: deleteButtonKey),
label: new Text('Chip', key: labelKey),
border: const StadiumBorder(),
);
}),
],
),
),
),
);
}
// No delete button
await pushChip();
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0)));
// Add a delete button
await pushChip(deletable: true);
// Delete button drawer should start out closed.
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0)));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(52.0, 4.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
await tester.pump(const Duration(milliseconds: 20));
// Delete button drawer should start expanding.
expect(tester.getSize(find.byType(RawChip)).width, closeTo(81.2, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(53.2, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(86.7, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(58.7, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(94.7, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(66.7, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(99.5, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(71.5, 0.1));
// Wait for being done with animation, and make sure it didn't change
// height.
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0)));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 4.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
// Test the tap work for the delete button, but not the rest of the chip.
expect(wasDeleted, isFalse);
await tester.tap(find.byKey(labelKey));
expect(wasDeleted, isFalse);
await tester.tap(find.byKey(deleteButtonKey));
expect(wasDeleted, isTrue);
// Remove the delete button again
await pushChip();
// Delete button drawer should start out open.
expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0)));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 4.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
await tester.pump(const Duration(milliseconds: 20));
// Delete button drawer should start contracting.
expect(tester.getSize(find.byType(RawChip)).width, closeTo(103.8, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(75.8, 0.1));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(102.9, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(74.9, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(101.0, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(73.0, 0.1));
await tester.pump(const Duration(milliseconds: 20));
expect(tester.getSize(find.byType(RawChip)).width, closeTo(97.5, 0.1));
expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(69.5, 0.1));
// Wait for being done with animation, make sure it didn't change
// height, and make sure that the delete button is no longer drawn.
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0)));
expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0)));
expect(find.byKey(deleteButtonKey), findsNothing);
});
testWidgets('Selection with avatar works as expected on RawChip', (WidgetTester tester) async {
bool selected = false;
final UniqueKey labelKey = new UniqueKey();
Future<Null> pushChip({Widget avatar, bool selectable: false}) async {
return tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Wrap(
children: <Widget>[
new StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return new RawChip(
avatar: avatar,
onSelected: selectable != null
? (bool value) {
setState(() {
selected = value;
});
}
: null,
selected: selected,
label: new Text('Chip', key: labelKey),
border: const StadiumBorder(),
showCheckmark: true,
tapEnabled: true,
isEnabled: true,
);
}),
],
),
),
),
);
}
// With avatar, but not selectable.
final UniqueKey avatarKey = new UniqueKey();
await pushChip(
avatar: new Container(width: 40.0, height: 40.0, key: avatarKey),
);
expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0)));
// Turn on selection.
await pushChip(
avatar: new Container(width: 40.0, height: 40.0, key: avatarKey),
selectable: true,
);
await tester.pumpAndSettle();
// Simulate a tap on the label to select the chip.
await tester.tap(find.byKey(labelKey));
expect(selected, equals(true));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(getSelectProgress(tester), closeTo(0.002, 0.01));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 50));
expect(getSelectProgress(tester), closeTo(0.54, 0.01));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 100));
expect(getSelectProgress(tester), equals(1.0));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pumpAndSettle();
// Simulate another tap on the label to deselect the chip.
await tester.tap(find.byKey(labelKey));
expect(selected, equals(false));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
await tester.pump();
await tester.pump(const Duration(milliseconds: 20));
expect(getSelectProgress(tester), closeTo(0.875, 0.01));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 20));
expect(getSelectProgress(tester), closeTo(0.13, 0.01));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 100));
expect(getSelectProgress(tester), equals(0.0));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
});
testWidgets('Selection without avatar works as expected on RawChip', (WidgetTester tester) async {
bool selected = false;
final UniqueKey labelKey = new UniqueKey();
Future<Null> pushChip({bool selectable: false}) async {
return tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Wrap(
children: <Widget>[
new StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return new RawChip(
onSelected: selectable != null
? (bool value) {
setState(() {
selected = value;
});
}
: null,
selected: selected,
label: new Text('Chip', key: labelKey),
border: const StadiumBorder(),
showCheckmark: true,
tapEnabled: true,
isEnabled: true,
);
}),
],
),
),
),
);
}
// Without avatar, but not selectable.
await pushChip();
expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0)));
// Turn on selection.
await pushChip(selectable: true);
await tester.pumpAndSettle();
// Simulate a tap on the label to select the chip.
await tester.tap(find.byKey(labelKey));
expect(selected, equals(true));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(getSelectProgress(tester), closeTo(0.002, 0.01));
expect(getAvatarDrawerProgress(tester), closeTo(0.459, 0.01));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 50));
expect(getSelectProgress(tester), closeTo(0.54, 0.01));
expect(getAvatarDrawerProgress(tester), closeTo(0.92, 0.01));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 100));
expect(getSelectProgress(tester), equals(1.0));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pumpAndSettle();
// Simulate another tap on the label to deselect the chip.
await tester.tap(find.byKey(labelKey));
expect(selected, equals(false));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
await tester.pump();
await tester.pump(const Duration(milliseconds: 20));
expect(getSelectProgress(tester), closeTo(0.875, 0.01));
expect(getAvatarDrawerProgress(tester), closeTo(0.96, 0.01));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 20));
expect(getSelectProgress(tester), closeTo(0.13, 0.01));
expect(getAvatarDrawerProgress(tester), closeTo(0.75, 0.01));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 100));
expect(getSelectProgress(tester), equals(0.0));
expect(getAvatarDrawerProgress(tester), equals(0.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
});
testWidgets('Activation works as expected on RawChip', (WidgetTester tester) async {
bool selected = false;
final UniqueKey labelKey = new UniqueKey();
Future<Null> pushChip({Widget avatar, bool selectable: false}) async {
return tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new Wrap(
children: <Widget>[
new StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return new RawChip(
avatar: avatar,
onSelected: selectable != null
? (bool value) {
setState(() {
selected = value;
});
}
: null,
selected: selected,
label: new Text('Chip', key: labelKey),
border: const StadiumBorder(),
showCheckmark: false,
tapEnabled: true,
isEnabled: true,
);
}),
],
),
),
),
);
}
final UniqueKey avatarKey = new UniqueKey();
await pushChip(
avatar: new Container(width: 40.0, height: 40.0, key: avatarKey),
selectable: true,
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(labelKey));
expect(selected, equals(true));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(getSelectProgress(tester), closeTo(0.002, 0.01));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 50));
expect(getSelectProgress(tester), closeTo(0.54, 0.01));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pump(const Duration(milliseconds: 100));
expect(getSelectProgress(tester), equals(1.0));
expect(getAvatarDrawerProgress(tester), equals(1.0));
expect(getDeleteDrawerProgress(tester), equals(0.0));
await tester.pumpAndSettle();
}); });
} }
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