mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
3081 lines
96 KiB
Dart
3081 lines
96 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style license that can be
|
||
// found in the LICENSE file.
|
||
|
||
import 'dart:math' as math;
|
||
|
||
import 'package:flutter/rendering.dart';
|
||
import 'package:flutter/widgets.dart';
|
||
|
||
import 'chip_theme.dart';
|
||
import 'colors.dart';
|
||
import 'constants.dart';
|
||
import 'debug.dart';
|
||
import 'feedback.dart';
|
||
import 'icons.dart';
|
||
import 'ink_well.dart';
|
||
import 'material.dart';
|
||
import 'material_localizations.dart';
|
||
import 'material_state.dart';
|
||
import 'theme.dart';
|
||
import 'theme_data.dart';
|
||
import 'tooltip.dart';
|
||
|
||
// Some design constants
|
||
const double _kChipHeight = 32.0;
|
||
const double _kDeleteIconSize = 18.0;
|
||
|
||
const int _kCheckmarkAlpha = 0xde; // 87%
|
||
const int _kDisabledAlpha = 0x61; // 38%
|
||
const double _kCheckmarkStrokeWidth = 2.0;
|
||
|
||
const Duration _kSelectDuration = Duration(milliseconds: 195);
|
||
const Duration _kCheckmarkDuration = Duration(milliseconds: 150);
|
||
const Duration _kCheckmarkReverseDuration = Duration(milliseconds: 50);
|
||
const Duration _kDrawerDuration = Duration(milliseconds: 150);
|
||
const Duration _kReverseDrawerDuration = Duration(milliseconds: 100);
|
||
const Duration _kDisableDuration = Duration(milliseconds: 75);
|
||
|
||
const Color _kSelectScrimColor = Color(0x60191919);
|
||
const Icon _kDefaultDeleteIcon = 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.io/design/components/chips.html>
|
||
abstract class ChipAttributes {
|
||
// This class is intended to be used as an interface, and should not be
|
||
// extended directly; this constructor prevents instantiation and extension.
|
||
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.
|
||
///
|
||
/// If null, the value of the [ChipTheme]'s [ChipThemeData.labelStyle] is used.
|
||
//
|
||
/// This only has an effect on widgets that respect the [DefaultTextStyle],
|
||
/// such as [Text].
|
||
///
|
||
/// If [TextStyle.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve]
|
||
/// is used for the following [MaterialState]s:
|
||
///
|
||
/// * [MaterialState.disabled].
|
||
/// * [MaterialState.selected].
|
||
/// * [MaterialState.hovered].
|
||
/// * [MaterialState.focused].
|
||
/// * [MaterialState.pressed].
|
||
TextStyle? get labelStyle;
|
||
|
||
/// The color and weight of the chip's outline.
|
||
///
|
||
/// Defaults to the border side in the ambient [ChipThemeData]. If the theme
|
||
/// border side resolves to null, the default is the border side of [shape].
|
||
///
|
||
/// This value is combined with [shape] to create a shape decorated with an
|
||
/// outline. If it is a [MaterialStateBorderSide],
|
||
/// [MaterialStateProperty.resolve] is used for the following
|
||
/// [MaterialState]s:
|
||
///
|
||
/// * [MaterialState.disabled].
|
||
/// * [MaterialState.selected].
|
||
/// * [MaterialState.hovered].
|
||
/// * [MaterialState.focused].
|
||
/// * [MaterialState.pressed].
|
||
BorderSide? get side;
|
||
|
||
/// The [OutlinedBorder] to draw around the chip.
|
||
///
|
||
/// Defaults to the shape in the ambient [ChipThemeData]. If the theme
|
||
/// shape resolves to null, the default is [StadiumBorder].
|
||
///
|
||
/// This shape is combined with [side] to create a shape decorated with an
|
||
/// outline. If it is a [MaterialStateOutlinedBorder],
|
||
/// [MaterialStateProperty.resolve] is used for the following
|
||
/// [MaterialState]s:
|
||
///
|
||
/// * [MaterialState.disabled].
|
||
/// * [MaterialState.selected].
|
||
/// * [MaterialState.hovered].
|
||
/// * [MaterialState.focused].
|
||
/// * [MaterialState.pressed].
|
||
OutlinedBorder? get shape;
|
||
|
||
/// {@macro flutter.material.Material.clipBehavior}
|
||
///
|
||
/// Defaults to [Clip.none], and must not be null.
|
||
Clip get clipBehavior;
|
||
|
||
/// {@macro flutter.widgets.Focus.focusNode}
|
||
FocusNode? get focusNode;
|
||
|
||
/// {@macro flutter.widgets.Focus.autofocus}
|
||
bool get autofocus;
|
||
|
||
/// 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 [shape].
|
||
///
|
||
/// Defaults to 4 logical pixels on all sides.
|
||
EdgeInsetsGeometry? get padding;
|
||
|
||
/// Defines how compact the chip's layout will be.
|
||
///
|
||
/// Chips are unaffected by horizontal density changes.
|
||
///
|
||
/// {@macro flutter.material.themedata.visualDensity}
|
||
///
|
||
/// See also:
|
||
///
|
||
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
|
||
/// widgets within a [Theme].
|
||
VisualDensity? get visualDensity;
|
||
|
||
/// 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;
|
||
|
||
/// Configures the minimum size of the tap target.
|
||
///
|
||
/// Defaults to [ThemeData.materialTapTargetSize].
|
||
///
|
||
/// See also:
|
||
///
|
||
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
||
MaterialTapTargetSize? get materialTapTargetSize;
|
||
|
||
/// Elevation to be applied on the chip relative to its parent.
|
||
///
|
||
/// This controls the size of the shadow below the chip.
|
||
///
|
||
/// Defaults to 0. The value is always non-negative.
|
||
double? get elevation;
|
||
|
||
/// Color of the chip's shadow when the elevation is greater than 0.
|
||
///
|
||
/// The default is [Colors.black].
|
||
Color? get shadowColor;
|
||
}
|
||
|
||
/// 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.io/design/components/chips.html>
|
||
abstract class DeletableChipAttributes {
|
||
// This class is intended to be used as an interface, and should not be
|
||
// extended directly; this constructor prevents instantiation and extension.
|
||
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 similar to the following sample:
|
||
///
|
||
/// {@tool dartpad --template=stateful_widget_scaffold_center}
|
||
///
|
||
/// This sample shows how to use [onDeleted] to remove an entry when the
|
||
/// delete button is tapped.
|
||
///
|
||
/// ```dart preamble
|
||
/// class Actor {
|
||
/// const Actor(this.name, this.initials);
|
||
/// final String name;
|
||
/// final String initials;
|
||
/// }
|
||
///
|
||
/// class CastList extends StatefulWidget {
|
||
/// const CastList({Key? key}) : super(key: key);
|
||
///
|
||
/// @override
|
||
/// State createState() => 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 (final Actor actor in _cast) {
|
||
/// yield Padding(
|
||
/// padding: const EdgeInsets.all(4.0),
|
||
/// child: Chip(
|
||
/// avatar: CircleAvatar(child: Text(actor.initials)),
|
||
/// label: Text(actor.name),
|
||
/// onDeleted: () {
|
||
/// setState(() {
|
||
/// _cast.removeWhere((Actor entry) {
|
||
/// return entry.name == actor.name;
|
||
/// });
|
||
/// });
|
||
/// },
|
||
/// ),
|
||
/// );
|
||
/// }
|
||
/// }
|
||
///
|
||
/// @override
|
||
/// Widget build(BuildContext context) {
|
||
/// return Wrap(
|
||
/// children: actorWidgets.toList(),
|
||
/// );
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
///
|
||
/// ```dart
|
||
/// @override
|
||
/// Widget build(BuildContext context) {
|
||
/// return const CastList();
|
||
/// }
|
||
/// ```
|
||
/// {@end-tool}
|
||
VoidCallback? get onDeleted;
|
||
|
||
/// The [Color] for the delete icon. The default is based on the ambient
|
||
/// [IconThemeData.color].
|
||
Color? get deleteIconColor;
|
||
|
||
/// Whether to use a tooltip on the chip's delete button showing the
|
||
/// [deleteButtonTooltipMessage].
|
||
///
|
||
/// Must not be null. Defaults to true.
|
||
bool get useDeleteButtonTooltip;
|
||
|
||
/// The message to be used for the chip's delete button tooltip.
|
||
///
|
||
/// This will be shown only if [useDeleteButtonTooltip] is true.
|
||
String? get deleteButtonTooltipMessage;
|
||
}
|
||
|
||
/// An interface for material design chips that can have check marks.
|
||
///
|
||
/// 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.
|
||
/// * [FilterChip], uses tags or descriptive words as a way to filter content.
|
||
/// * <https://material.io/design/components/chips.html>
|
||
abstract class CheckmarkableChipAttributes {
|
||
// This class is intended to be used as an interface, and should not be
|
||
// extended directly; this constructor prevents instantiation and extension.
|
||
CheckmarkableChipAttributes._();
|
||
|
||
/// Whether or not to show a check mark when
|
||
/// [SelectableChipAttributes.selected] is true.
|
||
///
|
||
/// Defaults to true.
|
||
bool? get showCheckmark;
|
||
|
||
/// [Color] of the chip's check mark when a check mark is visible.
|
||
///
|
||
/// This will override the color set by the platform's brightness setting.
|
||
///
|
||
/// If null, it will defer to a color selected by the platform's brightness
|
||
/// setting.
|
||
Color? get checkmarkColor;
|
||
}
|
||
|
||
/// 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.io/design/components/chips.html>
|
||
abstract class SelectableChipAttributes {
|
||
// This class is intended to be used as an interface, and should not be
|
||
// extended directly; this constructor prevents instantiation and extension.
|
||
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 de-selected
|
||
/// 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.
|
||
///
|
||
/// {@tool snippet}
|
||
///
|
||
/// A [StatefulWidget] that illustrates use of onSelected in an [InputChip].
|
||
///
|
||
/// ```dart
|
||
/// class Wood extends StatefulWidget {
|
||
/// const Wood({Key? key}) : super(key: key);
|
||
///
|
||
/// @override
|
||
/// State<StatefulWidget> createState() => WoodState();
|
||
/// }
|
||
///
|
||
/// class WoodState extends State<Wood> {
|
||
/// bool _useChisel = false;
|
||
///
|
||
/// @override
|
||
/// Widget build(BuildContext context) {
|
||
/// return InputChip(
|
||
/// label: const Text('Use Chisel'),
|
||
/// selected: _useChisel,
|
||
/// onSelected: (bool newValue) {
|
||
/// setState(() {
|
||
/// _useChisel = newValue;
|
||
/// });
|
||
/// },
|
||
/// );
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
/// {@end-tool}
|
||
ValueChanged<bool>? get onSelected;
|
||
|
||
/// Elevation to be applied on the chip relative to its parent during the
|
||
/// press motion.
|
||
///
|
||
/// This controls the size of the shadow below the chip.
|
||
///
|
||
/// Defaults to 8. The value is always non-negative.
|
||
double? get pressElevation;
|
||
|
||
/// 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;
|
||
|
||
/// Color of the chip's shadow when the elevation is greater than 0 and the
|
||
/// chip is selected.
|
||
///
|
||
/// The default is [Colors.black].
|
||
Color? get selectedShadowColor;
|
||
|
||
/// Tooltip string to be used for the body area (where the label and avatar
|
||
/// are) of the chip.
|
||
String? get tooltip;
|
||
|
||
/// The shape of the translucent highlight painted over the avatar when the
|
||
/// [selected] property is true.
|
||
///
|
||
/// Only the outer path of the shape is used.
|
||
///
|
||
/// Defaults to [CircleBorder].
|
||
ShapeBorder get avatarBorder;
|
||
}
|
||
|
||
/// 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.io/design/components/chips.html>
|
||
abstract class DisabledChipAttributes {
|
||
// This class is intended to be used as an interface, and should not be
|
||
// extended directly; this constructor prevents instantiation and extension.
|
||
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.onDeleted]), 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.onDeleted] 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.io/design/components/chips.html>
|
||
abstract class TappableChipAttributes {
|
||
// This class is intended to be used as an interface, and should not be
|
||
// extended directly; this constructor prevents instantiation and extension.
|
||
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.
|
||
///
|
||
/// {@tool snippet}
|
||
///
|
||
/// ```dart
|
||
/// class Blacksmith extends StatelessWidget {
|
||
/// const Blacksmith({Key? key}) : super(key: key);
|
||
///
|
||
/// void startHammering() {
|
||
/// print('bang bang bang');
|
||
/// }
|
||
///
|
||
/// @override
|
||
/// Widget build(BuildContext context) {
|
||
/// return InputChip(
|
||
/// label: const Text('Apply Hammer'),
|
||
/// onPressed: startHammering,
|
||
/// );
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
/// {@end-tool}
|
||
VoidCallback? get onPressed;
|
||
|
||
/// Elevation to be applied on the chip relative to its parent during the
|
||
/// press motion.
|
||
///
|
||
/// This controls the size of the shadow below the chip.
|
||
///
|
||
/// Defaults to 8. The value is always non-negative.
|
||
double? get pressElevation;
|
||
|
||
/// 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.
|
||
///
|
||
/// Its ancestors must include [Material], [MediaQuery], [Directionality], and
|
||
/// [MaterialLocalizations]. Typically all of these widgets are provided by
|
||
/// [MaterialApp] and [Scaffold]. The [label] and [clipBehavior] arguments must
|
||
/// not be null.
|
||
///
|
||
/// {@tool snippet}
|
||
///
|
||
/// ```dart
|
||
/// Chip(
|
||
/// avatar: CircleAvatar(
|
||
/// backgroundColor: Colors.grey.shade800,
|
||
/// child: const Text('AB'),
|
||
/// ),
|
||
/// label: const Text('Aaron Burr'),
|
||
/// )
|
||
/// ```
|
||
/// {@end-tool}
|
||
///
|
||
/// 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.io/design/components/chips.html>
|
||
class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttributes {
|
||
/// Creates a material design chip.
|
||
///
|
||
/// The [label], [autofocus], and [clipBehavior] arguments must not be null.
|
||
/// The [elevation] must be null or non-negative.
|
||
const Chip({
|
||
Key? key,
|
||
this.avatar,
|
||
required this.label,
|
||
this.labelStyle,
|
||
this.labelPadding,
|
||
this.deleteIcon,
|
||
this.onDeleted,
|
||
this.deleteIconColor,
|
||
this.useDeleteButtonTooltip = true,
|
||
this.deleteButtonTooltipMessage,
|
||
this.side,
|
||
this.shape,
|
||
this.clipBehavior = Clip.none,
|
||
this.focusNode,
|
||
this.autofocus = false,
|
||
this.backgroundColor,
|
||
this.padding,
|
||
this.visualDensity,
|
||
this.materialTapTargetSize,
|
||
this.elevation,
|
||
this.shadowColor,
|
||
}) : assert(label != null),
|
||
assert(autofocus != null),
|
||
assert(clipBehavior != null),
|
||
assert(elevation == null || elevation >= 0.0),
|
||
assert(useDeleteButtonTooltip != null),
|
||
super(key: key);
|
||
|
||
@override
|
||
final Widget? avatar;
|
||
@override
|
||
final Widget label;
|
||
@override
|
||
final TextStyle? labelStyle;
|
||
@override
|
||
final EdgeInsetsGeometry? labelPadding;
|
||
@override
|
||
final BorderSide? side;
|
||
@override
|
||
final OutlinedBorder? shape;
|
||
@override
|
||
final Clip clipBehavior;
|
||
@override
|
||
final FocusNode? focusNode;
|
||
@override
|
||
final bool autofocus;
|
||
@override
|
||
final Color? backgroundColor;
|
||
@override
|
||
final EdgeInsetsGeometry? padding;
|
||
@override
|
||
final VisualDensity? visualDensity;
|
||
@override
|
||
final Widget? deleteIcon;
|
||
@override
|
||
final VoidCallback? onDeleted;
|
||
@override
|
||
final Color? deleteIconColor;
|
||
@override
|
||
final bool useDeleteButtonTooltip;
|
||
@override
|
||
final String? deleteButtonTooltipMessage;
|
||
@override
|
||
final MaterialTapTargetSize? materialTapTargetSize;
|
||
@override
|
||
final double? elevation;
|
||
@override
|
||
final Color? shadowColor;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
assert(debugCheckHasMaterial(context));
|
||
return RawChip(
|
||
avatar: avatar,
|
||
label: label,
|
||
labelStyle: labelStyle,
|
||
labelPadding: labelPadding,
|
||
deleteIcon: deleteIcon,
|
||
onDeleted: onDeleted,
|
||
deleteIconColor: deleteIconColor,
|
||
useDeleteButtonTooltip: useDeleteButtonTooltip,
|
||
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
|
||
tapEnabled: false,
|
||
side: side,
|
||
shape: shape,
|
||
clipBehavior: clipBehavior,
|
||
focusNode: focusNode,
|
||
autofocus: autofocus,
|
||
backgroundColor: backgroundColor,
|
||
padding: padding,
|
||
visualDensity: visualDensity,
|
||
materialTapTargetSize: materialTapTargetSize,
|
||
elevation: elevation,
|
||
shadowColor: shadowColor,
|
||
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].
|
||
///
|
||
/// {@tool snippet}
|
||
///
|
||
/// ```dart
|
||
/// InputChip(
|
||
/// avatar: CircleAvatar(
|
||
/// backgroundColor: Colors.grey.shade800,
|
||
/// child: const Text('AB'),
|
||
/// ),
|
||
/// label: const Text('Aaron Burr'),
|
||
/// onPressed: () {
|
||
/// print('I am the one thing in life.');
|
||
/// }
|
||
/// )
|
||
/// ```
|
||
/// {@end-tool}
|
||
///
|
||
/// 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.io/design/components/chips.html>
|
||
class InputChip extends StatelessWidget
|
||
implements
|
||
ChipAttributes,
|
||
DeletableChipAttributes,
|
||
SelectableChipAttributes,
|
||
CheckmarkableChipAttributes,
|
||
DisabledChipAttributes,
|
||
TappableChipAttributes {
|
||
/// Creates an [InputChip].
|
||
///
|
||
/// The [onPressed] and [onSelected] callbacks must not both be specified at
|
||
/// the same time.
|
||
///
|
||
/// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior]
|
||
/// arguments must not be null. The [pressElevation] and [elevation] must be
|
||
/// null or non-negative. Typically, [pressElevation] is greater than
|
||
/// [elevation].
|
||
const InputChip({
|
||
Key? key,
|
||
this.avatar,
|
||
required this.label,
|
||
this.labelStyle,
|
||
this.labelPadding,
|
||
this.selected = false,
|
||
this.isEnabled = true,
|
||
this.onSelected,
|
||
this.deleteIcon,
|
||
this.onDeleted,
|
||
this.deleteIconColor,
|
||
this.useDeleteButtonTooltip = true,
|
||
this.deleteButtonTooltipMessage,
|
||
this.onPressed,
|
||
this.pressElevation,
|
||
this.disabledColor,
|
||
this.selectedColor,
|
||
this.tooltip,
|
||
this.side,
|
||
this.shape,
|
||
this.clipBehavior = Clip.none,
|
||
this.focusNode,
|
||
this.autofocus = false,
|
||
this.backgroundColor,
|
||
this.padding,
|
||
this.visualDensity,
|
||
this.materialTapTargetSize,
|
||
this.elevation,
|
||
this.shadowColor,
|
||
this.selectedShadowColor,
|
||
this.showCheckmark,
|
||
this.checkmarkColor,
|
||
this.avatarBorder = const CircleBorder(),
|
||
}) : assert(selected != null),
|
||
assert(isEnabled != null),
|
||
assert(label != null),
|
||
assert(clipBehavior != null),
|
||
assert(autofocus != null),
|
||
assert(pressElevation == null || pressElevation >= 0.0),
|
||
assert(elevation == null || elevation >= 0.0),
|
||
assert(useDeleteButtonTooltip != null),
|
||
super(key: key);
|
||
|
||
@override
|
||
final Widget? avatar;
|
||
@override
|
||
final Widget label;
|
||
@override
|
||
final TextStyle? labelStyle;
|
||
@override
|
||
final EdgeInsetsGeometry? labelPadding;
|
||
@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 bool useDeleteButtonTooltip;
|
||
@override
|
||
final String? deleteButtonTooltipMessage;
|
||
@override
|
||
final VoidCallback? onPressed;
|
||
@override
|
||
final double? pressElevation;
|
||
@override
|
||
final Color? disabledColor;
|
||
@override
|
||
final Color? selectedColor;
|
||
@override
|
||
final String? tooltip;
|
||
@override
|
||
final BorderSide? side;
|
||
@override
|
||
final OutlinedBorder? shape;
|
||
@override
|
||
final Clip clipBehavior;
|
||
@override
|
||
final FocusNode? focusNode;
|
||
@override
|
||
final bool autofocus;
|
||
@override
|
||
final Color? backgroundColor;
|
||
@override
|
||
final EdgeInsetsGeometry? padding;
|
||
@override
|
||
final VisualDensity? visualDensity;
|
||
@override
|
||
final MaterialTapTargetSize? materialTapTargetSize;
|
||
@override
|
||
final double? elevation;
|
||
@override
|
||
final Color? shadowColor;
|
||
@override
|
||
final Color? selectedShadowColor;
|
||
@override
|
||
final bool? showCheckmark;
|
||
@override
|
||
final Color? checkmarkColor;
|
||
@override
|
||
final ShapeBorder avatarBorder;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
assert(debugCheckHasMaterial(context));
|
||
return RawChip(
|
||
avatar: avatar,
|
||
label: label,
|
||
labelStyle: labelStyle,
|
||
labelPadding: labelPadding,
|
||
deleteIcon: deleteIcon,
|
||
onDeleted: onDeleted,
|
||
deleteIconColor: deleteIconColor,
|
||
useDeleteButtonTooltip: useDeleteButtonTooltip,
|
||
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
|
||
onSelected: onSelected,
|
||
onPressed: onPressed,
|
||
pressElevation: pressElevation,
|
||
selected: selected,
|
||
tapEnabled: true,
|
||
disabledColor: disabledColor,
|
||
selectedColor: selectedColor,
|
||
tooltip: tooltip,
|
||
side: side,
|
||
shape: shape,
|
||
clipBehavior: clipBehavior,
|
||
focusNode: focusNode,
|
||
autofocus: autofocus,
|
||
backgroundColor: backgroundColor,
|
||
padding: padding,
|
||
visualDensity: visualDensity,
|
||
materialTapTargetSize: materialTapTargetSize,
|
||
elevation: elevation,
|
||
shadowColor: shadowColor,
|
||
selectedShadowColor: selectedShadowColor,
|
||
showCheckmark: showCheckmark,
|
||
checkmarkColor: checkmarkColor,
|
||
isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null),
|
||
avatarBorder: avatarBorder,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 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] and
|
||
/// [label] arguments must not be null.
|
||
///
|
||
/// {@tool snippet}
|
||
///
|
||
/// ```dart
|
||
/// class MyThreeOptions extends StatefulWidget {
|
||
/// const MyThreeOptions({Key? key}) : super(key: key);
|
||
///
|
||
/// @override
|
||
/// _MyThreeOptionsState createState() => _MyThreeOptionsState();
|
||
/// }
|
||
///
|
||
/// class _MyThreeOptionsState extends State<MyThreeOptions> {
|
||
/// int? _value = 1;
|
||
///
|
||
/// @override
|
||
/// Widget build(BuildContext context) {
|
||
/// return Wrap(
|
||
/// children: List<Widget>.generate(
|
||
/// 3,
|
||
/// (int index) {
|
||
/// return ChoiceChip(
|
||
/// label: Text('Item $index'),
|
||
/// selected: _value == index,
|
||
/// onSelected: (bool selected) {
|
||
/// setState(() {
|
||
/// _value = selected ? index : null;
|
||
/// });
|
||
/// },
|
||
/// );
|
||
/// },
|
||
/// ).toList(),
|
||
/// );
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
/// {@end-tool}
|
||
///
|
||
/// 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.io/design/components/chips.html>
|
||
class ChoiceChip extends StatelessWidget
|
||
implements
|
||
ChipAttributes,
|
||
SelectableChipAttributes,
|
||
DisabledChipAttributes {
|
||
/// Create a chip that acts like a radio button.
|
||
///
|
||
/// The [label], [selected], [autofocus], and [clipBehavior] arguments must
|
||
/// not be null. The [pressElevation] and [elevation] must be null or
|
||
/// non-negative. Typically, [pressElevation] is greater than [elevation].
|
||
const ChoiceChip({
|
||
Key? key,
|
||
this.avatar,
|
||
required this.label,
|
||
this.labelStyle,
|
||
this.labelPadding,
|
||
this.onSelected,
|
||
this.pressElevation,
|
||
required this.selected,
|
||
this.selectedColor,
|
||
this.disabledColor,
|
||
this.tooltip,
|
||
this.side,
|
||
this.shape,
|
||
this.clipBehavior = Clip.none,
|
||
this.focusNode,
|
||
this.autofocus = false,
|
||
this.backgroundColor,
|
||
this.padding,
|
||
this.visualDensity,
|
||
this.materialTapTargetSize,
|
||
this.elevation,
|
||
this.shadowColor,
|
||
this.selectedShadowColor,
|
||
this.avatarBorder = const CircleBorder(),
|
||
}) : assert(selected != null),
|
||
assert(label != null),
|
||
assert(clipBehavior != null),
|
||
assert(autofocus != null),
|
||
assert(pressElevation == null || pressElevation >= 0.0),
|
||
assert(elevation == null || elevation >= 0.0),
|
||
super(key: key);
|
||
|
||
@override
|
||
final Widget? avatar;
|
||
@override
|
||
final Widget label;
|
||
@override
|
||
final TextStyle? labelStyle;
|
||
@override
|
||
final EdgeInsetsGeometry? labelPadding;
|
||
@override
|
||
final ValueChanged<bool>? onSelected;
|
||
@override
|
||
final double? pressElevation;
|
||
@override
|
||
final bool selected;
|
||
@override
|
||
final Color? disabledColor;
|
||
@override
|
||
final Color? selectedColor;
|
||
@override
|
||
final String? tooltip;
|
||
@override
|
||
final BorderSide? side;
|
||
@override
|
||
final OutlinedBorder? shape;
|
||
@override
|
||
final Clip clipBehavior;
|
||
@override
|
||
final FocusNode? focusNode;
|
||
@override
|
||
final bool autofocus;
|
||
@override
|
||
final Color? backgroundColor;
|
||
@override
|
||
final EdgeInsetsGeometry? padding;
|
||
@override
|
||
final VisualDensity? visualDensity;
|
||
@override
|
||
final MaterialTapTargetSize? materialTapTargetSize;
|
||
@override
|
||
final double? elevation;
|
||
@override
|
||
final Color? shadowColor;
|
||
@override
|
||
final Color? selectedShadowColor;
|
||
@override
|
||
final ShapeBorder avatarBorder;
|
||
|
||
@override
|
||
bool get isEnabled => onSelected != null;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
assert(debugCheckHasMaterial(context));
|
||
final ChipThemeData chipTheme = ChipTheme.of(context);
|
||
return RawChip(
|
||
avatar: avatar,
|
||
label: label,
|
||
labelStyle: labelStyle ?? (selected ? chipTheme.secondaryLabelStyle : null),
|
||
labelPadding: labelPadding,
|
||
onSelected: onSelected,
|
||
pressElevation: pressElevation,
|
||
selected: selected,
|
||
showCheckmark: false,
|
||
onDeleted: null,
|
||
tooltip: tooltip,
|
||
side: side,
|
||
shape: shape,
|
||
clipBehavior: clipBehavior,
|
||
focusNode: focusNode,
|
||
autofocus: autofocus,
|
||
disabledColor: disabledColor,
|
||
selectedColor: selectedColor ?? chipTheme.secondarySelectedColor,
|
||
backgroundColor: backgroundColor,
|
||
padding: padding,
|
||
visualDensity: visualDensity,
|
||
isEnabled: isEnabled,
|
||
materialTapTargetSize: materialTapTargetSize,
|
||
elevation: elevation,
|
||
shadowColor: shadowColor,
|
||
selectedShadowColor: selectedShadowColor,
|
||
avatarBorder: avatarBorder,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 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.
|
||
///
|
||
/// {@tool snippet}
|
||
///
|
||
/// ```dart
|
||
/// class ActorFilterEntry {
|
||
/// const ActorFilterEntry(this.name, this.initials);
|
||
/// final String name;
|
||
/// final String initials;
|
||
/// }
|
||
///
|
||
/// class CastFilter extends StatefulWidget {
|
||
/// const CastFilter({Key? key}) : super(key: key);
|
||
///
|
||
/// @override
|
||
/// State createState() => 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'),
|
||
/// ];
|
||
/// final List<String> _filters = <String>[];
|
||
///
|
||
/// Iterable<Widget> get actorWidgets sync* {
|
||
/// for (final ActorFilterEntry actor in _cast) {
|
||
/// yield Padding(
|
||
/// padding: const EdgeInsets.all(4.0),
|
||
/// child: FilterChip(
|
||
/// avatar: CircleAvatar(child: Text(actor.initials)),
|
||
/// label: 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>[
|
||
/// Wrap(
|
||
/// children: actorWidgets.toList(),
|
||
/// ),
|
||
/// Text('Look for: ${_filters.join(', ')}'),
|
||
/// ],
|
||
/// );
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
/// {@end-tool}
|
||
///
|
||
/// 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.io/design/components/chips.html>
|
||
class FilterChip extends StatelessWidget
|
||
implements
|
||
ChipAttributes,
|
||
SelectableChipAttributes,
|
||
CheckmarkableChipAttributes,
|
||
DisabledChipAttributes {
|
||
/// Create a chip that acts like a checkbox.
|
||
///
|
||
/// The [selected], [label], [autofocus], and [clipBehavior] arguments must
|
||
/// not be null. The [pressElevation] and [elevation] must be null or
|
||
/// non-negative. Typically, [pressElevation] is greater than [elevation].
|
||
const FilterChip({
|
||
Key? key,
|
||
this.avatar,
|
||
required this.label,
|
||
this.labelStyle,
|
||
this.labelPadding,
|
||
this.selected = false,
|
||
required this.onSelected,
|
||
this.pressElevation,
|
||
this.disabledColor,
|
||
this.selectedColor,
|
||
this.tooltip,
|
||
this.side,
|
||
this.shape,
|
||
this.clipBehavior = Clip.none,
|
||
this.focusNode,
|
||
this.autofocus = false,
|
||
this.backgroundColor,
|
||
this.padding,
|
||
this.visualDensity,
|
||
this.materialTapTargetSize,
|
||
this.elevation,
|
||
this.shadowColor,
|
||
this.selectedShadowColor,
|
||
this.showCheckmark,
|
||
this.checkmarkColor,
|
||
this.avatarBorder = const CircleBorder(),
|
||
}) : assert(selected != null),
|
||
assert(label != null),
|
||
assert(clipBehavior != null),
|
||
assert(autofocus != null),
|
||
assert(pressElevation == null || pressElevation >= 0.0),
|
||
assert(elevation == null || elevation >= 0.0),
|
||
super(key: key);
|
||
|
||
@override
|
||
final Widget? avatar;
|
||
@override
|
||
final Widget label;
|
||
@override
|
||
final TextStyle? labelStyle;
|
||
@override
|
||
final EdgeInsetsGeometry? labelPadding;
|
||
@override
|
||
final bool selected;
|
||
@override
|
||
final ValueChanged<bool>? onSelected;
|
||
@override
|
||
final double? pressElevation;
|
||
@override
|
||
final Color? disabledColor;
|
||
@override
|
||
final Color? selectedColor;
|
||
@override
|
||
final String? tooltip;
|
||
@override
|
||
final BorderSide? side;
|
||
@override
|
||
final OutlinedBorder? shape;
|
||
@override
|
||
final Clip clipBehavior;
|
||
@override
|
||
final FocusNode? focusNode;
|
||
@override
|
||
final bool autofocus;
|
||
@override
|
||
final Color? backgroundColor;
|
||
@override
|
||
final EdgeInsetsGeometry? padding;
|
||
@override
|
||
final VisualDensity? visualDensity;
|
||
@override
|
||
final MaterialTapTargetSize? materialTapTargetSize;
|
||
@override
|
||
final double? elevation;
|
||
@override
|
||
final Color? shadowColor;
|
||
@override
|
||
final Color? selectedShadowColor;
|
||
@override
|
||
final bool? showCheckmark;
|
||
@override
|
||
final Color? checkmarkColor;
|
||
@override
|
||
final ShapeBorder avatarBorder;
|
||
|
||
@override
|
||
bool get isEnabled => onSelected != null;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
assert(debugCheckHasMaterial(context));
|
||
return RawChip(
|
||
avatar: avatar,
|
||
label: label,
|
||
labelStyle: labelStyle,
|
||
labelPadding: labelPadding,
|
||
onSelected: onSelected,
|
||
pressElevation: pressElevation,
|
||
selected: selected,
|
||
tooltip: tooltip,
|
||
side: side,
|
||
shape: shape,
|
||
clipBehavior: clipBehavior,
|
||
focusNode: focusNode,
|
||
autofocus: autofocus,
|
||
backgroundColor: backgroundColor,
|
||
disabledColor: disabledColor,
|
||
selectedColor: selectedColor,
|
||
padding: padding,
|
||
visualDensity: visualDensity,
|
||
isEnabled: isEnabled,
|
||
materialTapTargetSize: materialTapTargetSize,
|
||
elevation: elevation,
|
||
shadowColor: shadowColor,
|
||
selectedShadowColor: selectedShadowColor,
|
||
showCheckmark: showCheckmark,
|
||
checkmarkColor: checkmarkColor,
|
||
avatarBorder: avatarBorder,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 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. They cannot be disabled; if the action is not applicable, the
|
||
/// chip should not be included in the interface. (This contrasts with buttons,
|
||
/// where unavailable choices are usually represented as disabled controls.)
|
||
///
|
||
/// Action chips are displayed after primary content, such as below a card or
|
||
/// persistently at the bottom of a screen.
|
||
///
|
||
/// The material button widgets, [ElevatedButton], [TextButton], and
|
||
/// [OutlinedButton], 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.
|
||
///
|
||
/// {@tool snippet}
|
||
///
|
||
/// ```dart
|
||
/// ActionChip(
|
||
/// avatar: CircleAvatar(
|
||
/// backgroundColor: Colors.grey.shade800,
|
||
/// child: const Text('AB'),
|
||
/// ),
|
||
/// label: const Text('Aaron Burr'),
|
||
/// onPressed: () {
|
||
/// print('If you stand for nothing, Burr, what’ll you fall for?');
|
||
/// }
|
||
/// )
|
||
/// ```
|
||
/// {@end-tool}
|
||
///
|
||
/// 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.io/design/components/chips.html>
|
||
class ActionChip extends StatelessWidget implements ChipAttributes, TappableChipAttributes {
|
||
/// Create a chip that acts like a button.
|
||
///
|
||
/// The [label], [onPressed], [autofocus], and [clipBehavior] arguments must
|
||
/// not be null. The [pressElevation] and [elevation] must be null or
|
||
/// non-negative. Typically, [pressElevation] is greater than [elevation].
|
||
const ActionChip({
|
||
Key? key,
|
||
this.avatar,
|
||
required this.label,
|
||
this.labelStyle,
|
||
this.labelPadding,
|
||
required this.onPressed,
|
||
this.pressElevation,
|
||
this.tooltip,
|
||
this.side,
|
||
this.shape,
|
||
this.clipBehavior = Clip.none,
|
||
this.focusNode,
|
||
this.autofocus = false,
|
||
this.backgroundColor,
|
||
this.padding,
|
||
this.visualDensity,
|
||
this.materialTapTargetSize,
|
||
this.elevation,
|
||
this.shadowColor,
|
||
}) : assert(label != null),
|
||
assert(clipBehavior != null),
|
||
assert(autofocus != null),
|
||
assert(
|
||
onPressed != null,
|
||
'Rather than disabling an ActionChip by setting onPressed to null, '
|
||
'remove it from the interface entirely.',
|
||
),
|
||
assert(pressElevation == null || pressElevation >= 0.0),
|
||
assert(elevation == null || elevation >= 0.0),
|
||
super(key: key);
|
||
|
||
@override
|
||
final Widget? avatar;
|
||
@override
|
||
final Widget label;
|
||
@override
|
||
final TextStyle? labelStyle;
|
||
@override
|
||
final EdgeInsetsGeometry? labelPadding;
|
||
@override
|
||
final VoidCallback onPressed;
|
||
@override
|
||
final double? pressElevation;
|
||
@override
|
||
final String? tooltip;
|
||
@override
|
||
final BorderSide? side;
|
||
@override
|
||
final OutlinedBorder? shape;
|
||
@override
|
||
final Clip clipBehavior;
|
||
@override
|
||
final FocusNode? focusNode;
|
||
@override
|
||
final bool autofocus;
|
||
@override
|
||
final Color? backgroundColor;
|
||
@override
|
||
final EdgeInsetsGeometry? padding;
|
||
@override
|
||
final VisualDensity? visualDensity;
|
||
@override
|
||
final MaterialTapTargetSize? materialTapTargetSize;
|
||
@override
|
||
final double? elevation;
|
||
@override
|
||
final Color? shadowColor;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
assert(debugCheckHasMaterial(context));
|
||
return RawChip(
|
||
avatar: avatar,
|
||
label: label,
|
||
onPressed: onPressed,
|
||
pressElevation: pressElevation,
|
||
tooltip: tooltip,
|
||
labelStyle: labelStyle,
|
||
backgroundColor: backgroundColor,
|
||
side: side,
|
||
shape: shape,
|
||
clipBehavior: clipBehavior,
|
||
focusNode: focusNode,
|
||
autofocus: autofocus,
|
||
padding: padding,
|
||
visualDensity: visualDensity,
|
||
labelPadding: labelPadding,
|
||
isEnabled: true,
|
||
materialTapTargetSize: materialTapTargetSize,
|
||
elevation: elevation,
|
||
shadowColor: shadowColor,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// A raw material design chip.
|
||
///
|
||
/// This serves as the basis for all of the chip widget types to aggregate.
|
||
/// It is typically not created directly, one of the other chip types
|
||
/// that are appropriate for the use case are used instead:
|
||
///
|
||
/// * [Chip] a simple chip that can only display information and be deleted.
|
||
/// * [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.
|
||
///
|
||
/// Raw chips are typically only used if you want to create your own custom chip
|
||
/// type.
|
||
///
|
||
/// 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.
|
||
///
|
||
/// Requires one of its ancestors to be a [Material] widget.
|
||
///
|
||
/// See also:
|
||
///
|
||
/// * [CircleAvatar], which shows images or initials of people.
|
||
/// * [Wrap], A widget that displays its children in multiple horizontal or
|
||
/// vertical runs.
|
||
/// * <https://material.io/design/components/chips.html>
|
||
class RawChip extends StatefulWidget
|
||
implements
|
||
ChipAttributes,
|
||
DeletableChipAttributes,
|
||
SelectableChipAttributes,
|
||
CheckmarkableChipAttributes,
|
||
DisabledChipAttributes,
|
||
TappableChipAttributes {
|
||
/// Creates a RawChip.
|
||
///
|
||
/// The [onPressed] and [onSelected] callbacks must not both be specified at
|
||
/// the same time.
|
||
///
|
||
/// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior]
|
||
/// arguments must not be null. The [pressElevation] and [elevation] must be
|
||
/// null or non-negative. Typically, [pressElevation] is greater than
|
||
/// [elevation].
|
||
const RawChip({
|
||
Key? key,
|
||
this.avatar,
|
||
required this.label,
|
||
this.labelStyle,
|
||
this.padding,
|
||
this.visualDensity,
|
||
this.labelPadding,
|
||
Widget? deleteIcon,
|
||
this.onDeleted,
|
||
this.deleteIconColor,
|
||
this.useDeleteButtonTooltip = true,
|
||
this.deleteButtonTooltipMessage,
|
||
this.onPressed,
|
||
this.onSelected,
|
||
this.pressElevation,
|
||
this.tapEnabled = true,
|
||
this.selected = false,
|
||
this.isEnabled = true,
|
||
this.disabledColor,
|
||
this.selectedColor,
|
||
this.tooltip,
|
||
this.side,
|
||
this.shape,
|
||
this.clipBehavior = Clip.none,
|
||
this.focusNode,
|
||
this.autofocus = false,
|
||
this.backgroundColor,
|
||
this.materialTapTargetSize,
|
||
this.elevation,
|
||
this.shadowColor,
|
||
this.selectedShadowColor,
|
||
this.showCheckmark = true,
|
||
this.checkmarkColor,
|
||
this.avatarBorder = const CircleBorder(),
|
||
}) : assert(label != null),
|
||
assert(isEnabled != null),
|
||
assert(selected != null),
|
||
assert(clipBehavior != null),
|
||
assert(autofocus != null),
|
||
assert(pressElevation == null || pressElevation >= 0.0),
|
||
assert(elevation == null || elevation >= 0.0),
|
||
assert(useDeleteButtonTooltip != null),
|
||
deleteIcon = deleteIcon ?? _kDefaultDeleteIcon,
|
||
super(key: key);
|
||
|
||
@override
|
||
final Widget? avatar;
|
||
@override
|
||
final Widget label;
|
||
@override
|
||
final TextStyle? labelStyle;
|
||
@override
|
||
final EdgeInsetsGeometry? labelPadding;
|
||
@override
|
||
final Widget deleteIcon;
|
||
@override
|
||
final VoidCallback? onDeleted;
|
||
@override
|
||
final Color? deleteIconColor;
|
||
@override
|
||
final bool useDeleteButtonTooltip;
|
||
@override
|
||
final String? deleteButtonTooltipMessage;
|
||
@override
|
||
final ValueChanged<bool>? onSelected;
|
||
@override
|
||
final VoidCallback? onPressed;
|
||
@override
|
||
final double? pressElevation;
|
||
@override
|
||
final bool selected;
|
||
@override
|
||
final bool isEnabled;
|
||
@override
|
||
final Color? disabledColor;
|
||
@override
|
||
final Color? selectedColor;
|
||
@override
|
||
final String? tooltip;
|
||
@override
|
||
final BorderSide? side;
|
||
@override
|
||
final OutlinedBorder? shape;
|
||
@override
|
||
final Clip clipBehavior;
|
||
@override
|
||
final FocusNode? focusNode;
|
||
@override
|
||
final bool autofocus;
|
||
@override
|
||
final Color? backgroundColor;
|
||
@override
|
||
final EdgeInsetsGeometry? padding;
|
||
@override
|
||
final VisualDensity? visualDensity;
|
||
@override
|
||
final MaterialTapTargetSize? materialTapTargetSize;
|
||
@override
|
||
final double? elevation;
|
||
@override
|
||
final Color? shadowColor;
|
||
@override
|
||
final Color? selectedShadowColor;
|
||
@override
|
||
final bool? showCheckmark;
|
||
@override
|
||
final Color? checkmarkColor;
|
||
@override
|
||
final ShapeBorder avatarBorder;
|
||
|
||
/// If set, this indicates that the chip should be disabled if all of the
|
||
/// tap callbacks ([onSelected], [onPressed]) are null.
|
||
///
|
||
/// For example, the [Chip] class sets this to false because it can't be
|
||
/// disabled, even if no callbacks are set on it, since it is used for
|
||
/// displaying information only.
|
||
///
|
||
/// Defaults to true.
|
||
final bool tapEnabled;
|
||
|
||
@override
|
||
_RawChipState createState() => _RawChipState();
|
||
}
|
||
|
||
class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip> {
|
||
static const Duration pressedAnimationDuration = Duration(milliseconds: 75);
|
||
|
||
late AnimationController selectController;
|
||
late AnimationController avatarDrawerController;
|
||
late AnimationController deleteDrawerController;
|
||
late AnimationController enableController;
|
||
late Animation<double> checkmarkAnimation;
|
||
late Animation<double> avatarDrawerAnimation;
|
||
late Animation<double> deleteDrawerAnimation;
|
||
late Animation<double> enableAnimation;
|
||
late Animation<double> selectionFade;
|
||
|
||
final Set<MaterialState> _states = <MaterialState>{};
|
||
|
||
final GlobalKey deleteIconKey = GlobalKey();
|
||
|
||
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);
|
||
}
|
||
|
||
bool _isTapping = false;
|
||
bool get isTapping => canTap && _isTapping;
|
||
|
||
@override
|
||
void initState() {
|
||
assert(widget.onSelected == null || widget.onPressed == null);
|
||
super.initState();
|
||
_updateState(MaterialState.disabled, !widget.isEnabled);
|
||
_updateState(MaterialState.selected, widget.selected);
|
||
selectController = AnimationController(
|
||
duration: _kSelectDuration,
|
||
value: widget.selected == true ? 1.0 : 0.0,
|
||
vsync: this,
|
||
);
|
||
selectionFade = CurvedAnimation(
|
||
parent: selectController,
|
||
curve: Curves.fastOutSlowIn,
|
||
);
|
||
avatarDrawerController = AnimationController(
|
||
duration: _kDrawerDuration,
|
||
value: hasAvatar || widget.selected == true ? 1.0 : 0.0,
|
||
vsync: this,
|
||
);
|
||
deleteDrawerController = AnimationController(
|
||
duration: _kDrawerDuration,
|
||
value: hasDeleteButton ? 1.0 : 0.0,
|
||
vsync: this,
|
||
);
|
||
enableController = AnimationController(
|
||
duration: _kDisableDuration,
|
||
value: widget.isEnabled ? 1.0 : 0.0,
|
||
vsync: this,
|
||
);
|
||
|
||
// These will delay the start of some animations, and/or reduce their
|
||
// 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 = CurvedAnimation(
|
||
parent: selectController,
|
||
curve: Interval(1.0 - checkmarkPercentage, 1.0, curve: Curves.fastOutSlowIn),
|
||
reverseCurve: Interval(
|
||
1.0 - checkmarkReversePercentage,
|
||
1.0,
|
||
curve: Curves.fastOutSlowIn,
|
||
),
|
||
);
|
||
deleteDrawerAnimation = CurvedAnimation(
|
||
parent: deleteDrawerController,
|
||
curve: Curves.fastOutSlowIn,
|
||
);
|
||
avatarDrawerAnimation = CurvedAnimation(
|
||
parent: avatarDrawerController,
|
||
curve: Curves.fastOutSlowIn,
|
||
reverseCurve: Interval(
|
||
1.0 - avatarDrawerReversePercentage,
|
||
1.0,
|
||
curve: Curves.fastOutSlowIn,
|
||
),
|
||
);
|
||
enableAnimation = CurvedAnimation(
|
||
parent: enableController,
|
||
curve: Curves.fastOutSlowIn,
|
||
);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
selectController.dispose();
|
||
avatarDrawerController.dispose();
|
||
deleteDrawerController.dispose();
|
||
enableController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _updateState(MaterialState state, bool value) {
|
||
value ? _states.add(state) : _states.remove(state);
|
||
}
|
||
|
||
void _handleTapDown(TapDownDetails details) {
|
||
if (!canTap) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
_isTapping = true;
|
||
_updateState(MaterialState.pressed, true);
|
||
});
|
||
}
|
||
|
||
void _handleTapCancel() {
|
||
if (!canTap) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
_isTapping = false;
|
||
_updateState(MaterialState.pressed, false);
|
||
});
|
||
}
|
||
|
||
void _handleTap() {
|
||
if (!canTap) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
_isTapping = false;
|
||
_updateState(MaterialState.pressed, false);
|
||
});
|
||
// Only one of these can be set, so only one will be called.
|
||
widget.onSelected?.call(!widget.selected);
|
||
widget.onPressed?.call();
|
||
}
|
||
|
||
void _handleFocus(bool isFocused) {
|
||
setState(() {
|
||
_updateState(MaterialState.focused, isFocused);
|
||
});
|
||
}
|
||
|
||
void _handleHover(bool isHovered) {
|
||
setState(() {
|
||
_updateState(MaterialState.hovered, isHovered);
|
||
});
|
||
}
|
||
|
||
OutlinedBorder _getShape(ChipThemeData theme) {
|
||
final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, _states)
|
||
?? MaterialStateProperty.resolveAs<BorderSide?>(theme.side, _states);
|
||
final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, _states)
|
||
?? MaterialStateProperty.resolveAs<OutlinedBorder?>(theme.shape, _states)
|
||
?? const StadiumBorder();
|
||
return resolvedShape.copyWith(side: resolvedSide);
|
||
}
|
||
|
||
/// Picks between three different colors, depending upon the state of two
|
||
/// different animations.
|
||
Color? getBackgroundColor(ChipThemeData theme) {
|
||
final ColorTween backgroundTween = ColorTween(
|
||
begin: widget.disabledColor ?? theme.disabledColor,
|
||
end: widget.backgroundColor ?? theme.backgroundColor,
|
||
);
|
||
final ColorTween selectTween = ColorTween(
|
||
begin: backgroundTween.evaluate(enableController),
|
||
end: widget.selectedColor ?? theme.selectedColor,
|
||
);
|
||
return selectTween.evaluate(selectionFade);
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(RawChip oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
if (oldWidget.isEnabled != widget.isEnabled) {
|
||
setState(() {
|
||
_updateState(MaterialState.disabled, !widget.isEnabled);
|
||
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(() {
|
||
_updateState(MaterialState.selected, widget.selected);
|
||
if (widget.selected == true) {
|
||
selectController.forward();
|
||
} else {
|
||
selectController.reverse();
|
||
}
|
||
});
|
||
}
|
||
if (oldWidget.onDeleted != widget.onDeleted) {
|
||
setState(() {
|
||
if (hasDeleteButton) {
|
||
deleteDrawerController.forward();
|
||
} else {
|
||
deleteDrawerController.reverse();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
Widget? _wrapWithTooltip(String? tooltip, VoidCallback? callback, Widget? child) {
|
||
if(!widget.useDeleteButtonTooltip){
|
||
return child;
|
||
}
|
||
if (child == null || callback == null || tooltip == null) {
|
||
return child;
|
||
}
|
||
return Tooltip(
|
||
message: tooltip,
|
||
child: child,
|
||
);
|
||
}
|
||
|
||
Widget? _buildDeleteIcon(
|
||
BuildContext context,
|
||
ThemeData theme,
|
||
ChipThemeData chipTheme,
|
||
GlobalKey deleteIconKey,
|
||
) {
|
||
if (!hasDeleteButton) {
|
||
return null;
|
||
}
|
||
return Semantics(
|
||
container: true,
|
||
button: true,
|
||
child: _wrapWithTooltip(
|
||
widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip,
|
||
widget.onDeleted,
|
||
GestureDetector(
|
||
key: deleteIconKey,
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: widget.isEnabled
|
||
? () {
|
||
Feedback.forTap(context);
|
||
widget.onDeleted!();
|
||
}
|
||
: null,
|
||
child: IconTheme(
|
||
data: theme.iconTheme.copyWith(
|
||
color: widget.deleteIconColor ?? chipTheme.deleteIconColor,
|
||
),
|
||
child: widget.deleteIcon,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
static const double _defaultElevation = 0.0;
|
||
static const double _defaultPressElevation = 8.0;
|
||
static const Color _defaultShadowColor = Colors.black;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
assert(debugCheckHasMaterial(context));
|
||
assert(debugCheckHasMediaQuery(context));
|
||
assert(debugCheckHasDirectionality(context));
|
||
assert(debugCheckHasMaterialLocalizations(context));
|
||
|
||
/// The chip at text scale 1 starts with 8px on each side and as text scaling
|
||
/// gets closer to 2 the label padding is linearly interpolated from 8px to 4px.
|
||
/// Once the widget has a text scaling of 2 or higher than the label padding
|
||
/// remains 4px.
|
||
final EdgeInsetsGeometry _defaultLabelPadding = EdgeInsets.lerp(
|
||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||
const EdgeInsets.symmetric(horizontal: 4.0),
|
||
(MediaQuery.of(context).textScaleFactor - 1.0).clamp(0.0, 1.0),
|
||
)!;
|
||
|
||
final ThemeData theme = Theme.of(context);
|
||
final ChipThemeData chipTheme = ChipTheme.of(context);
|
||
final TextDirection? textDirection = Directionality.maybeOf(context);
|
||
final OutlinedBorder resolvedShape = _getShape(chipTheme);
|
||
final double elevation = widget.elevation ?? chipTheme.elevation ?? _defaultElevation;
|
||
final double pressElevation = widget.pressElevation ?? chipTheme.pressElevation ?? _defaultPressElevation;
|
||
final Color shadowColor = widget.shadowColor ?? chipTheme.shadowColor ?? _defaultShadowColor;
|
||
final Color selectedShadowColor = widget.selectedShadowColor ?? chipTheme.selectedShadowColor ?? _defaultShadowColor;
|
||
final Color? checkmarkColor = widget.checkmarkColor ?? chipTheme.checkmarkColor;
|
||
final bool showCheckmark = widget.showCheckmark ?? chipTheme.showCheckmark ?? true;
|
||
|
||
final TextStyle effectiveLabelStyle = chipTheme.labelStyle.merge(widget.labelStyle);
|
||
final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, _states);
|
||
final TextStyle resolvedLabelStyle = effectiveLabelStyle.copyWith(color: resolvedLabelColor);
|
||
final EdgeInsetsGeometry labelPadding = widget.labelPadding ?? chipTheme.labelPadding ?? _defaultLabelPadding;
|
||
|
||
Widget result = Material(
|
||
elevation: isTapping ? pressElevation : elevation,
|
||
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
|
||
animationDuration: pressedAnimationDuration,
|
||
shape: resolvedShape,
|
||
clipBehavior: widget.clipBehavior,
|
||
child: InkWell(
|
||
onFocusChange: _handleFocus,
|
||
focusNode: widget.focusNode,
|
||
autofocus: widget.autofocus,
|
||
canRequestFocus: widget.isEnabled,
|
||
onTap: canTap ? _handleTap : null,
|
||
onTapDown: canTap ? _handleTapDown : null,
|
||
onTapCancel: canTap ? _handleTapCancel : null,
|
||
onHover: canTap ? _handleHover : null,
|
||
splashFactory: _LocationAwareInkRippleFactory(
|
||
hasDeleteButton,
|
||
context,
|
||
deleteIconKey,
|
||
),
|
||
customBorder: resolvedShape,
|
||
child: AnimatedBuilder(
|
||
animation: Listenable.merge(<Listenable>[selectController, enableController]),
|
||
builder: (BuildContext context, Widget? child) {
|
||
return Container(
|
||
decoration: ShapeDecoration(
|
||
shape: resolvedShape,
|
||
color: getBackgroundColor(chipTheme),
|
||
),
|
||
child: child,
|
||
);
|
||
},
|
||
child: _wrapWithTooltip(
|
||
widget.tooltip,
|
||
widget.onPressed,
|
||
_ChipRenderWidget(
|
||
theme: _ChipRenderTheme(
|
||
label: DefaultTextStyle(
|
||
overflow: TextOverflow.fade,
|
||
textAlign: TextAlign.start,
|
||
maxLines: 1,
|
||
softWrap: false,
|
||
style: resolvedLabelStyle,
|
||
child: widget.label,
|
||
),
|
||
avatar: AnimatedSwitcher(
|
||
child: widget.avatar,
|
||
duration: _kDrawerDuration,
|
||
switchInCurve: Curves.fastOutSlowIn,
|
||
),
|
||
deleteIcon: AnimatedSwitcher(
|
||
child: _buildDeleteIcon(context, theme, chipTheme, deleteIconKey),
|
||
duration: _kDrawerDuration,
|
||
switchInCurve: Curves.fastOutSlowIn,
|
||
),
|
||
brightness: chipTheme.brightness,
|
||
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
|
||
visualDensity: widget.visualDensity ?? theme.visualDensity,
|
||
labelPadding: labelPadding.resolve(textDirection),
|
||
showAvatar: hasAvatar,
|
||
showCheckmark: showCheckmark,
|
||
checkmarkColor: checkmarkColor,
|
||
canTapBody: canTap,
|
||
),
|
||
value: widget.selected,
|
||
checkmarkAnimation: checkmarkAnimation,
|
||
enableAnimation: enableAnimation,
|
||
avatarDrawerAnimation: avatarDrawerAnimation,
|
||
deleteDrawerAnimation: deleteDrawerAnimation,
|
||
isEnabled: widget.isEnabled,
|
||
avatarBorder: widget.avatarBorder,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
final BoxConstraints constraints;
|
||
final Offset densityAdjustment = (widget.visualDensity ?? theme.visualDensity).baseSizeAdjustment;
|
||
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
|
||
case MaterialTapTargetSize.padded:
|
||
constraints = BoxConstraints(
|
||
minWidth: kMinInteractiveDimension + densityAdjustment.dx,
|
||
minHeight: kMinInteractiveDimension + densityAdjustment.dy,
|
||
);
|
||
break;
|
||
case MaterialTapTargetSize.shrinkWrap:
|
||
constraints = const BoxConstraints();
|
||
break;
|
||
}
|
||
result = _ChipRedirectingHitDetectionWidget(
|
||
constraints: constraints,
|
||
child: Center(
|
||
child: result,
|
||
widthFactor: 1.0,
|
||
heightFactor: 1.0,
|
||
),
|
||
);
|
||
return Semantics(
|
||
button: widget.tapEnabled,
|
||
container: true,
|
||
selected: widget.selected,
|
||
enabled: widget.tapEnabled ? canTap : null,
|
||
child: result,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Redirects the [position.dy] passed to [RenderBox.hitTest] to the vertical
|
||
/// center of the widget.
|
||
///
|
||
/// The primary purpose of this widget is to allow padding around the [RawChip]
|
||
/// to trigger the child ink feature without increasing the size of the material.
|
||
class _ChipRedirectingHitDetectionWidget extends SingleChildRenderObjectWidget {
|
||
const _ChipRedirectingHitDetectionWidget({
|
||
Key? key,
|
||
Widget? child,
|
||
required this.constraints,
|
||
}) : super(key: key, child: child);
|
||
|
||
final BoxConstraints constraints;
|
||
|
||
@override
|
||
RenderObject createRenderObject(BuildContext context) {
|
||
return _RenderChipRedirectingHitDetection(constraints);
|
||
}
|
||
|
||
@override
|
||
void updateRenderObject(BuildContext context, covariant _RenderChipRedirectingHitDetection renderObject) {
|
||
renderObject.additionalConstraints = constraints;
|
||
}
|
||
}
|
||
|
||
class _RenderChipRedirectingHitDetection extends RenderConstrainedBox {
|
||
_RenderChipRedirectingHitDetection(BoxConstraints additionalConstraints) : super(additionalConstraints: additionalConstraints);
|
||
|
||
@override
|
||
bool hitTest(BoxHitTestResult result, { required Offset position }) {
|
||
if (!size.contains(position))
|
||
return false;
|
||
// Only redirects hit detection which occurs above and below the render object.
|
||
// In order to make this assumption true, I have removed the minimum width
|
||
// constraints, since any reasonable chip would be at least that wide.
|
||
final Offset offset = Offset(position.dx, size.height / 2);
|
||
return result.addWithRawTransform(
|
||
transform: MatrixUtils.forceToPoint(offset),
|
||
position: position,
|
||
hitTest: (BoxHitTestResult result, Offset? position) {
|
||
assert(position == offset);
|
||
return child!.hitTest(result, position: offset);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ChipRenderWidget extends RenderObjectWidget {
|
||
const _ChipRenderWidget({
|
||
Key? key,
|
||
required this.theme,
|
||
this.value,
|
||
this.isEnabled,
|
||
required this.checkmarkAnimation,
|
||
required this.avatarDrawerAnimation,
|
||
required this.deleteDrawerAnimation,
|
||
required this.enableAnimation,
|
||
this.avatarBorder,
|
||
}) : assert(theme != null),
|
||
super(key: key);
|
||
|
||
final _ChipRenderTheme theme;
|
||
final bool? value;
|
||
final bool? isEnabled;
|
||
final Animation<double> checkmarkAnimation;
|
||
final Animation<double> avatarDrawerAnimation;
|
||
final Animation<double> deleteDrawerAnimation;
|
||
final Animation<double> enableAnimation;
|
||
final ShapeBorder? avatarBorder;
|
||
|
||
@override
|
||
_RenderChipElement createElement() => _RenderChipElement(this);
|
||
|
||
@override
|
||
void updateRenderObject(BuildContext context, _RenderChip renderObject) {
|
||
renderObject
|
||
..theme = theme
|
||
..textDirection = Directionality.of(context)
|
||
..value = value
|
||
..isEnabled = isEnabled
|
||
..checkmarkAnimation = checkmarkAnimation
|
||
..avatarDrawerAnimation = avatarDrawerAnimation
|
||
..deleteDrawerAnimation = deleteDrawerAnimation
|
||
..enableAnimation = enableAnimation
|
||
..avatarBorder = avatarBorder;
|
||
}
|
||
|
||
@override
|
||
RenderObject createRenderObject(BuildContext context) {
|
||
return _RenderChip(
|
||
theme: theme,
|
||
textDirection: Directionality.of(context),
|
||
value: value,
|
||
isEnabled: isEnabled,
|
||
checkmarkAnimation: checkmarkAnimation,
|
||
avatarDrawerAnimation: avatarDrawerAnimation,
|
||
deleteDrawerAnimation: deleteDrawerAnimation,
|
||
enableAnimation: enableAnimation,
|
||
avatarBorder: avatarBorder,
|
||
);
|
||
}
|
||
}
|
||
|
||
enum _ChipSlot {
|
||
label,
|
||
avatar,
|
||
deleteIcon,
|
||
}
|
||
|
||
class _RenderChipElement extends RenderObjectElement {
|
||
_RenderChipElement(_ChipRenderWidget chip) : super(chip);
|
||
|
||
final Map<_ChipSlot, Element> slotToChild = <_ChipSlot, Element>{};
|
||
|
||
@override
|
||
_ChipRenderWidget get widget => super.widget as _ChipRenderWidget;
|
||
|
||
@override
|
||
_RenderChip get renderObject => super.renderObject as _RenderChip;
|
||
|
||
@override
|
||
void visitChildren(ElementVisitor visitor) {
|
||
slotToChild.values.forEach(visitor);
|
||
}
|
||
|
||
@override
|
||
void forgetChild(Element child) {
|
||
assert(slotToChild.containsValue(child));
|
||
assert(child.slot is _ChipSlot);
|
||
assert(slotToChild.containsKey(child.slot));
|
||
slotToChild.remove(child.slot);
|
||
super.forgetChild(child);
|
||
}
|
||
|
||
void _mountChild(Widget widget, _ChipSlot slot) {
|
||
final Element? oldChild = slotToChild[slot];
|
||
final Element? newChild = updateChild(oldChild, widget, slot);
|
||
if (oldChild != null) {
|
||
slotToChild.remove(slot);
|
||
}
|
||
if (newChild != null) {
|
||
slotToChild[slot] = newChild;
|
||
}
|
||
}
|
||
|
||
@override
|
||
void mount(Element? parent, dynamic newSlot) {
|
||
super.mount(parent, newSlot);
|
||
_mountChild(widget.theme.avatar, _ChipSlot.avatar);
|
||
_mountChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon);
|
||
_mountChild(widget.theme.label, _ChipSlot.label);
|
||
}
|
||
|
||
void _updateChild(Widget widget, _ChipSlot slot) {
|
||
final Element? oldChild = slotToChild[slot];
|
||
final Element? newChild = updateChild(oldChild, widget, slot);
|
||
if (oldChild != null) {
|
||
slotToChild.remove(slot);
|
||
}
|
||
if (newChild != null) {
|
||
slotToChild[slot] = newChild;
|
||
}
|
||
}
|
||
|
||
@override
|
||
void update(_ChipRenderWidget newWidget) {
|
||
super.update(newWidget);
|
||
assert(widget == newWidget);
|
||
_updateChild(widget.theme.label, _ChipSlot.label);
|
||
_updateChild(widget.theme.avatar, _ChipSlot.avatar);
|
||
_updateChild(widget.theme.deleteIcon, _ChipSlot.deleteIcon);
|
||
}
|
||
|
||
void _updateRenderObject(RenderObject? child, _ChipSlot slot) {
|
||
switch (slot) {
|
||
case _ChipSlot.avatar:
|
||
renderObject.avatar = child as RenderBox?;
|
||
break;
|
||
case _ChipSlot.label:
|
||
renderObject.label = child as RenderBox?;
|
||
break;
|
||
case _ChipSlot.deleteIcon:
|
||
renderObject.deleteIcon = child as RenderBox?;
|
||
break;
|
||
}
|
||
}
|
||
|
||
@override
|
||
void insertRenderObjectChild(RenderObject child, _ChipSlot slot) {
|
||
assert(child is RenderBox);
|
||
_updateRenderObject(child, slot);
|
||
assert(renderObject.children.keys.contains(slot));
|
||
}
|
||
|
||
@override
|
||
void removeRenderObjectChild(RenderObject child, _ChipSlot slot) {
|
||
assert(child is RenderBox);
|
||
assert(renderObject.children[slot] == child);
|
||
_updateRenderObject(null, slot);
|
||
assert(!renderObject.children.keys.contains(slot));
|
||
}
|
||
|
||
@override
|
||
void moveRenderObjectChild(RenderObject child, dynamic oldSlot, dynamic newSlot) {
|
||
assert(false, 'not reachable');
|
||
}
|
||
}
|
||
|
||
@immutable
|
||
class _ChipRenderTheme {
|
||
const _ChipRenderTheme({
|
||
required this.avatar,
|
||
required this.label,
|
||
required this.deleteIcon,
|
||
required this.brightness,
|
||
required this.padding,
|
||
required this.visualDensity,
|
||
required this.labelPadding,
|
||
required this.showAvatar,
|
||
required this.showCheckmark,
|
||
required this.checkmarkColor,
|
||
required this.canTapBody,
|
||
});
|
||
|
||
final Widget avatar;
|
||
final Widget label;
|
||
final Widget deleteIcon;
|
||
final Brightness brightness;
|
||
final EdgeInsets padding;
|
||
final VisualDensity visualDensity;
|
||
final EdgeInsets labelPadding;
|
||
final bool showAvatar;
|
||
final bool showCheckmark;
|
||
final Color? checkmarkColor;
|
||
final bool canTapBody;
|
||
|
||
@override
|
||
bool operator ==(Object other) {
|
||
if (identical(this, other)) {
|
||
return true;
|
||
}
|
||
if (other.runtimeType != runtimeType) {
|
||
return false;
|
||
}
|
||
return other is _ChipRenderTheme
|
||
&& other.avatar == avatar
|
||
&& other.label == label
|
||
&& other.deleteIcon == deleteIcon
|
||
&& other.brightness == brightness
|
||
&& other.padding == padding
|
||
&& other.labelPadding == labelPadding
|
||
&& other.showAvatar == showAvatar
|
||
&& other.showCheckmark == showCheckmark
|
||
&& other.checkmarkColor == checkmarkColor
|
||
&& other.canTapBody == canTapBody;
|
||
}
|
||
|
||
@override
|
||
int get hashCode {
|
||
return hashValues(
|
||
avatar,
|
||
label,
|
||
deleteIcon,
|
||
brightness,
|
||
padding,
|
||
labelPadding,
|
||
showAvatar,
|
||
showCheckmark,
|
||
checkmarkColor,
|
||
canTapBody,
|
||
);
|
||
}
|
||
}
|
||
|
||
class _RenderChip extends RenderBox {
|
||
_RenderChip({
|
||
required _ChipRenderTheme theme,
|
||
required TextDirection textDirection,
|
||
this.value,
|
||
this.isEnabled,
|
||
required this.checkmarkAnimation,
|
||
required this.avatarDrawerAnimation,
|
||
required this.deleteDrawerAnimation,
|
||
required this.enableAnimation,
|
||
this.avatarBorder,
|
||
}) : assert(theme != null),
|
||
assert(textDirection != null),
|
||
_theme = theme,
|
||
_textDirection = textDirection {
|
||
checkmarkAnimation.addListener(markNeedsPaint);
|
||
avatarDrawerAnimation.addListener(markNeedsLayout);
|
||
deleteDrawerAnimation.addListener(markNeedsLayout);
|
||
enableAnimation.addListener(markNeedsPaint);
|
||
}
|
||
|
||
final Map<_ChipSlot, RenderBox> children = <_ChipSlot, RenderBox>{};
|
||
|
||
bool? value;
|
||
bool? isEnabled;
|
||
late Rect _deleteButtonRect;
|
||
late Rect _pressRect;
|
||
Animation<double> checkmarkAnimation;
|
||
Animation<double> avatarDrawerAnimation;
|
||
Animation<double> deleteDrawerAnimation;
|
||
Animation<double> enableAnimation;
|
||
ShapeBorder? avatarBorder;
|
||
|
||
RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _ChipSlot slot) {
|
||
if (oldChild != null) {
|
||
dropChild(oldChild);
|
||
children.remove(slot);
|
||
}
|
||
if (newChild != null) {
|
||
children[slot] = newChild;
|
||
adoptChild(newChild);
|
||
}
|
||
return newChild;
|
||
}
|
||
|
||
RenderBox? _avatar;
|
||
RenderBox? get avatar => _avatar;
|
||
set avatar(RenderBox? value) {
|
||
_avatar = _updateChild(_avatar, value, _ChipSlot.avatar);
|
||
}
|
||
|
||
RenderBox? _deleteIcon;
|
||
RenderBox? get deleteIcon => _deleteIcon;
|
||
set deleteIcon(RenderBox? value) {
|
||
_deleteIcon = _updateChild(_deleteIcon, value, _ChipSlot.deleteIcon);
|
||
}
|
||
|
||
RenderBox? _label;
|
||
RenderBox? get label => _label;
|
||
set label(RenderBox? value) {
|
||
_label = _updateChild(_label, value, _ChipSlot.label);
|
||
}
|
||
|
||
_ChipRenderTheme get theme => _theme;
|
||
_ChipRenderTheme _theme;
|
||
set theme(_ChipRenderTheme value) {
|
||
if (_theme == value) {
|
||
return;
|
||
}
|
||
_theme = value;
|
||
markNeedsLayout();
|
||
}
|
||
|
||
TextDirection? get textDirection => _textDirection;
|
||
TextDirection? _textDirection;
|
||
set textDirection(TextDirection? value) {
|
||
if (_textDirection == value) {
|
||
return;
|
||
}
|
||
_textDirection = value;
|
||
markNeedsLayout();
|
||
}
|
||
|
||
// The returned list is ordered for hit testing.
|
||
Iterable<RenderBox> get _children sync* {
|
||
if (avatar != null) {
|
||
yield avatar!;
|
||
}
|
||
if (label != null) {
|
||
yield label!;
|
||
}
|
||
if (deleteIcon != null) {
|
||
yield deleteIcon!;
|
||
}
|
||
}
|
||
|
||
bool get isDrawingCheckmark => theme.showCheckmark && !checkmarkAnimation.isDismissed;
|
||
bool get deleteIconShowing => !deleteDrawerAnimation.isDismissed;
|
||
|
||
@override
|
||
void attach(PipelineOwner owner) {
|
||
super.attach(owner);
|
||
for (final RenderBox child in _children) {
|
||
child.attach(owner);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void detach() {
|
||
super.detach();
|
||
for (final RenderBox child in _children) {
|
||
child.detach();
|
||
}
|
||
}
|
||
|
||
@override
|
||
void redepthChildren() {
|
||
_children.forEach(redepthChild);
|
||
}
|
||
|
||
@override
|
||
void visitChildren(RenderObjectVisitor visitor) {
|
||
_children.forEach(visitor);
|
||
}
|
||
|
||
@override
|
||
List<DiagnosticsNode> debugDescribeChildren() {
|
||
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
|
||
void add(RenderBox? child, String name) {
|
||
if (child != null) {
|
||
value.add(child.toDiagnosticsNode(name: name));
|
||
}
|
||
}
|
||
|
||
add(avatar, 'avatar');
|
||
add(label, 'label');
|
||
add(deleteIcon, 'deleteIcon');
|
||
return value;
|
||
}
|
||
|
||
@override
|
||
bool get sizedByParent => false;
|
||
|
||
static double _minWidth(RenderBox? box, double height) {
|
||
return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
|
||
}
|
||
|
||
static double _maxWidth(RenderBox? box, double height) {
|
||
return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
|
||
}
|
||
|
||
static double _minHeight(RenderBox? box, double width) {
|
||
return box == null ? 0.0 : box.getMinIntrinsicHeight(width);
|
||
}
|
||
|
||
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! as BoxParentData;
|
||
|
||
@override
|
||
double computeMinIntrinsicWidth(double height) {
|
||
// The overall padding isn't affected by missing avatar or delete icon
|
||
// because we add the padding regardless to give extra padding for the label
|
||
// when they're missing.
|
||
final double overallPadding = theme.padding.horizontal +
|
||
theme.labelPadding.horizontal;
|
||
return overallPadding +
|
||
_minWidth(avatar, height) +
|
||
_minWidth(label, height) +
|
||
_minWidth(deleteIcon, height);
|
||
}
|
||
|
||
@override
|
||
double computeMaxIntrinsicWidth(double height) {
|
||
final double overallPadding = theme.padding.horizontal +
|
||
theme.labelPadding.horizontal;
|
||
return overallPadding +
|
||
_maxWidth(avatar, height) +
|
||
_maxWidth(label, height) +
|
||
_maxWidth(deleteIcon, height);
|
||
}
|
||
|
||
@override
|
||
double computeMinIntrinsicHeight(double width) {
|
||
return math.max(
|
||
_kChipHeight,
|
||
theme.padding.vertical + theme.labelPadding.vertical + _minHeight(label, width),
|
||
);
|
||
}
|
||
|
||
@override
|
||
double computeMaxIntrinsicHeight(double width) => computeMinIntrinsicHeight(width);
|
||
|
||
@override
|
||
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||
// The baseline of this widget is the baseline of the label.
|
||
return label!.getDistanceToActualBaseline(baseline);
|
||
}
|
||
|
||
Size _layoutLabel(BoxConstraints contentConstraints, double iconSizes, Size size, Size rawSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
|
||
// Now that we know the label height and the width of the icons, we can
|
||
// determine how much to shrink the width constraints for the "real" layout.
|
||
if (contentConstraints.maxWidth.isFinite) {
|
||
final double maxWidth = math.max(
|
||
0.0,
|
||
contentConstraints.maxWidth
|
||
- iconSizes
|
||
- theme.labelPadding.horizontal
|
||
- theme.padding.horizontal,
|
||
);
|
||
final Size updatedSize = layoutChild(
|
||
label!,
|
||
BoxConstraints(
|
||
minWidth: 0.0,
|
||
maxWidth: maxWidth,
|
||
minHeight: rawSize.height,
|
||
maxHeight: size.height,
|
||
),
|
||
);
|
||
|
||
return Size(
|
||
updatedSize.width + theme.labelPadding.horizontal,
|
||
updatedSize.height + theme.labelPadding.vertical,
|
||
);
|
||
}
|
||
|
||
final Size updatedSize = layoutChild(
|
||
label!,
|
||
BoxConstraints(
|
||
minHeight: rawSize.height,
|
||
maxHeight: size.height,
|
||
minWidth: 0.0,
|
||
maxWidth: size.width,
|
||
),
|
||
);
|
||
|
||
return Size(
|
||
updatedSize.width + theme.labelPadding.horizontal,
|
||
updatedSize.height + theme.labelPadding.vertical,
|
||
);
|
||
}
|
||
|
||
Size _layoutAvatar(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
|
||
final double requestedSize = math.max(0.0, contentSize);
|
||
final BoxConstraints avatarConstraints = BoxConstraints.tightFor(
|
||
width: requestedSize,
|
||
height: requestedSize,
|
||
);
|
||
final Size avatarBoxSize = layoutChild(avatar!, avatarConstraints);
|
||
if (!theme.showCheckmark && !theme.showAvatar) {
|
||
return Size(0.0, contentSize);
|
||
}
|
||
double avatarWidth = 0.0;
|
||
double avatarHeight = 0.0;
|
||
if (theme.showAvatar) {
|
||
avatarWidth += avatarDrawerAnimation.value * avatarBoxSize.width;
|
||
} else {
|
||
avatarWidth += avatarDrawerAnimation.value * contentSize;
|
||
}
|
||
avatarHeight += avatarBoxSize.height;
|
||
return Size(avatarWidth, avatarHeight);
|
||
}
|
||
|
||
Size _layoutDeleteIcon(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
|
||
final double requestedSize = math.max(0.0, contentSize);
|
||
final BoxConstraints deleteIconConstraints = BoxConstraints.tightFor(
|
||
width: requestedSize,
|
||
height: requestedSize,
|
||
);
|
||
final Size boxSize = layoutChild(deleteIcon!, deleteIconConstraints);
|
||
if (!deleteIconShowing) {
|
||
return Size(0.0, contentSize);
|
||
}
|
||
double deleteIconWidth = 0.0;
|
||
double deleteIconHeight = 0.0;
|
||
deleteIconWidth += deleteDrawerAnimation.value * boxSize.width;
|
||
deleteIconHeight += boxSize.height;
|
||
return Size(deleteIconWidth, deleteIconHeight);
|
||
}
|
||
|
||
@override
|
||
bool hitTest(BoxHitTestResult result, { required Offset position }) {
|
||
if (!size.contains(position)) {
|
||
return false;
|
||
}
|
||
final bool tapIsOnDeleteIcon = _tapIsOnDeleteIcon(
|
||
hasDeleteButton: deleteIcon != null,
|
||
tapPosition: position,
|
||
chipSize: size,
|
||
textDirection: textDirection!,
|
||
);
|
||
final RenderBox? hitTestChild = tapIsOnDeleteIcon
|
||
? (deleteIcon ?? label ?? avatar)
|
||
: (label ?? avatar);
|
||
|
||
if (hitTestChild != null) {
|
||
final Offset center = hitTestChild.size.center(Offset.zero);
|
||
return result.addWithRawTransform(
|
||
transform: MatrixUtils.forceToPoint(center),
|
||
position: position,
|
||
hitTest: (BoxHitTestResult result, Offset? position) {
|
||
assert(position == center);
|
||
return hitTestChild.hitTest(result, position: center);
|
||
},
|
||
);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
@override
|
||
Size computeDryLayout(BoxConstraints constraints) {
|
||
return _computeSizes(constraints, ChildLayoutHelper.dryLayoutChild).size;
|
||
}
|
||
|
||
_ChipSizes _computeSizes(BoxConstraints constraints, ChildLayouter layoutChild) {
|
||
final BoxConstraints contentConstraints = constraints.loosen();
|
||
// Find out the height of the label within the constraints.
|
||
final Offset densityAdjustment = Offset(0.0, theme.visualDensity.baseSizeAdjustment.dy / 2.0);
|
||
final Size rawLabelSize = layoutChild(label!, contentConstraints);
|
||
final double contentSize = math.max(
|
||
_kChipHeight - theme.padding.vertical + theme.labelPadding.vertical,
|
||
rawLabelSize.height + theme.labelPadding.vertical,
|
||
);
|
||
final Size avatarSize = _layoutAvatar(contentConstraints, contentSize, layoutChild);
|
||
final Size deleteIconSize = _layoutDeleteIcon(contentConstraints, contentSize, layoutChild);
|
||
final Size labelSize = _layoutLabel(
|
||
contentConstraints,
|
||
avatarSize.width + deleteIconSize.width,
|
||
Size(rawLabelSize.width, contentSize),
|
||
rawLabelSize,
|
||
layoutChild,
|
||
);
|
||
|
||
// This is the overall size of the content: it doesn't include
|
||
// theme.padding, that is added in at the end.
|
||
final Size overallSize = Size(
|
||
avatarSize.width + labelSize.width + deleteIconSize.width,
|
||
contentSize,
|
||
) + densityAdjustment;
|
||
final Size paddedSize = Size(
|
||
overallSize.width + theme.padding.horizontal,
|
||
overallSize.height + theme.padding.vertical,
|
||
);
|
||
return _ChipSizes(
|
||
size: constraints.constrain(paddedSize),
|
||
overall: overallSize,
|
||
content: contentSize,
|
||
densityAdjustment: densityAdjustment,
|
||
avatar: avatarSize,
|
||
label: labelSize,
|
||
deleteIcon: deleteIconSize,
|
||
);
|
||
}
|
||
|
||
@override
|
||
void performLayout() {
|
||
final _ChipSizes sizes = _computeSizes(constraints, ChildLayoutHelper.layoutChild);
|
||
|
||
// Now we have all of the dimensions. Place the children where they belong.
|
||
|
||
const double left = 0.0;
|
||
final double right = sizes.overall.width;
|
||
|
||
Offset centerLayout(Size boxSize, double x) {
|
||
assert(sizes.content >= boxSize.height);
|
||
switch (textDirection!) {
|
||
case TextDirection.rtl:
|
||
return Offset(x - boxSize.width, (sizes.content - boxSize.height + sizes.densityAdjustment.dy) / 2.0);
|
||
case TextDirection.ltr:
|
||
return Offset(x, (sizes.content - boxSize.height + sizes.densityAdjustment.dy) / 2.0);
|
||
}
|
||
}
|
||
|
||
// 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!) {
|
||
case TextDirection.rtl:
|
||
double start = right;
|
||
if (theme.showCheckmark || theme.showAvatar) {
|
||
avatarOffset = centerLayout(sizes.avatar, start);
|
||
start -= sizes.avatar.width;
|
||
}
|
||
labelOffset = centerLayout(sizes.label, start);
|
||
start -= sizes.label.width;
|
||
if (deleteIconShowing) {
|
||
_deleteButtonRect = Rect.fromLTWH(
|
||
0.0,
|
||
0.0,
|
||
sizes.deleteIcon.width + theme.padding.right,
|
||
sizes.overall.height + theme.padding.vertical,
|
||
);
|
||
deleteIconOffset = centerLayout(sizes.deleteIcon, start);
|
||
} else {
|
||
_deleteButtonRect = Rect.zero;
|
||
}
|
||
start -= sizes.deleteIcon.width;
|
||
if (theme.canTapBody) {
|
||
_pressRect = Rect.fromLTWH(
|
||
_deleteButtonRect.width,
|
||
0.0,
|
||
sizes.overall.width - _deleteButtonRect.width + theme.padding.horizontal,
|
||
sizes.overall.height + theme.padding.vertical,
|
||
);
|
||
} else {
|
||
_pressRect = Rect.zero;
|
||
}
|
||
break;
|
||
case TextDirection.ltr:
|
||
double start = left;
|
||
if (theme.showCheckmark || theme.showAvatar) {
|
||
avatarOffset = centerLayout(sizes.avatar, start - _boxSize(avatar).width + sizes.avatar.width);
|
||
start += sizes.avatar.width;
|
||
}
|
||
labelOffset = centerLayout(sizes.label, start);
|
||
start += sizes.label.width;
|
||
if (theme.canTapBody) {
|
||
_pressRect = Rect.fromLTWH(
|
||
0.0,
|
||
0.0,
|
||
deleteIconShowing
|
||
? start + theme.padding.left
|
||
: sizes.overall.width + theme.padding.horizontal,
|
||
sizes.overall.height + theme.padding.vertical,
|
||
);
|
||
} else {
|
||
_pressRect = Rect.zero;
|
||
}
|
||
start -= _boxSize(deleteIcon).width - sizes.deleteIcon.width;
|
||
if (deleteIconShowing) {
|
||
deleteIconOffset = centerLayout(sizes.deleteIcon, start);
|
||
_deleteButtonRect = Rect.fromLTWH(
|
||
start + theme.padding.left,
|
||
0.0,
|
||
sizes.deleteIcon.width + theme.padding.right,
|
||
sizes.overall.height + theme.padding.vertical,
|
||
);
|
||
} else {
|
||
_deleteButtonRect = Rect.zero;
|
||
}
|
||
break;
|
||
}
|
||
// Center the label vertically.
|
||
labelOffset = labelOffset +
|
||
Offset(
|
||
0.0,
|
||
((sizes.label.height - theme.labelPadding.vertical) - _boxSize(label).height) / 2.0,
|
||
);
|
||
_boxParentData(avatar!).offset = theme.padding.topLeft + avatarOffset;
|
||
_boxParentData(label!).offset = theme.padding.topLeft + labelOffset + theme.labelPadding.topLeft;
|
||
_boxParentData(deleteIcon!).offset = theme.padding.topLeft + deleteIconOffset;
|
||
final Size paddedSize = Size(
|
||
sizes.overall.width + theme.padding.horizontal,
|
||
sizes.overall.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)}');
|
||
}
|
||
|
||
static final ColorTween selectionScrimTween = ColorTween(
|
||
begin: Colors.transparent,
|
||
end: _kSelectScrimColor,
|
||
);
|
||
|
||
Color get _disabledColor {
|
||
if (enableAnimation == null || enableAnimation.isCompleted) {
|
||
return Colors.white;
|
||
}
|
||
final ColorTween enableTween;
|
||
switch (theme.brightness) {
|
||
case Brightness.light:
|
||
enableTween = ColorTween(
|
||
begin: Colors.white.withAlpha(_kDisabledAlpha),
|
||
end: Colors.white,
|
||
);
|
||
break;
|
||
case Brightness.dark:
|
||
enableTween = ColorTween(
|
||
begin: Colors.black.withAlpha(_kDisabledAlpha),
|
||
end: Colors.black,
|
||
);
|
||
break;
|
||
}
|
||
return enableTween.evaluate(enableAnimation)!;
|
||
}
|
||
|
||
void _paintCheck(Canvas canvas, Offset origin, double size) {
|
||
Color? paintColor;
|
||
if (theme.checkmarkColor != null) {
|
||
paintColor = theme.checkmarkColor;
|
||
} else {
|
||
switch (theme.brightness) {
|
||
case Brightness.light:
|
||
paintColor = theme.showAvatar ? Colors.white : Colors.black.withAlpha(_kCheckmarkAlpha);
|
||
break;
|
||
case Brightness.dark:
|
||
paintColor = theme.showAvatar ? Colors.black : Colors.white.withAlpha(_kCheckmarkAlpha);
|
||
break;
|
||
}
|
||
}
|
||
|
||
final ColorTween fadeTween = ColorTween(begin: Colors.transparent, end: paintColor);
|
||
|
||
paintColor = checkmarkAnimation.status == AnimationStatus.reverse
|
||
? fadeTween.evaluate(checkmarkAnimation)
|
||
: paintColor;
|
||
|
||
final Paint paint = 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 = Path();
|
||
final Offset start = Offset(size * 0.15, size * 0.45);
|
||
final Offset mid = Offset(size * 0.4, size * 0.7);
|
||
final Offset end = 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) {
|
||
if (theme.showAvatar) {
|
||
final Rect avatarRect = _boxRect(avatar).shift(offset);
|
||
final Paint darkenPaint = Paint()
|
||
..color = selectionScrimTween.evaluate(checkmarkAnimation)!
|
||
..blendMode = BlendMode.srcATop;
|
||
final Path path = avatarBorder!.getOuterPath(avatarRect);
|
||
context.canvas.drawPath(path, 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 +
|
||
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 Color disabledColor = _disabledColor;
|
||
final int disabledColorAlpha = disabledColor.alpha;
|
||
if (needsCompositing) {
|
||
context.pushLayer(OpacityLayer(alpha: disabledColorAlpha), paintWithOverlay, offset);
|
||
} else {
|
||
if (disabledColorAlpha != 0xff) {
|
||
context.canvas.saveLayer(
|
||
_boxRect(avatar).shift(offset).inflate(20.0),
|
||
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(
|
||
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), Paint()..color = _disabledColor);
|
||
context.paintChild(child, _boxParentData(child).offset + offset);
|
||
context.canvas.restore();
|
||
}
|
||
} else {
|
||
context.paintChild(child, _boxParentData(child).offset + offset);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void paint(PaintingContext context, Offset offset) {
|
||
_paintAvatar(context, offset);
|
||
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
|
||
void debugPaint(PaintingContext context, Offset offset) {
|
||
assert(!_debugShowTapTargetOutlines || () {
|
||
// Draws a rect around the tap targets to help with visualizing where
|
||
// they really are.
|
||
final Paint outlinePaint = Paint()
|
||
..color = const Color(0xff800000)
|
||
..strokeWidth = 1.0
|
||
..style = PaintingStyle.stroke;
|
||
if (deleteIconShowing) {
|
||
context.canvas.drawRect(_deleteButtonRect.shift(offset), outlinePaint);
|
||
}
|
||
context.canvas.drawRect(
|
||
_pressRect.shift(offset),
|
||
outlinePaint..color = const Color(0xff008000),
|
||
);
|
||
return true;
|
||
}());
|
||
}
|
||
|
||
@override
|
||
bool hitTestSelf(Offset position) => _deleteButtonRect.contains(position) || _pressRect.contains(position);
|
||
}
|
||
|
||
class _ChipSizes {
|
||
_ChipSizes({
|
||
required this.size,
|
||
required this.overall,
|
||
required this.content,
|
||
required this.avatar,
|
||
required this.label,
|
||
required this.deleteIcon,
|
||
required this.densityAdjustment,
|
||
});
|
||
final Size size;
|
||
final Size overall;
|
||
final double content;
|
||
final Size avatar;
|
||
final Size label;
|
||
final Size deleteIcon;
|
||
final Offset densityAdjustment;
|
||
}
|
||
|
||
class _LocationAwareInkRippleFactory extends InteractiveInkFeatureFactory {
|
||
const _LocationAwareInkRippleFactory(
|
||
this.hasDeleteButton,
|
||
this.chipContext,
|
||
this.deleteIconKey,
|
||
);
|
||
|
||
final bool hasDeleteButton;
|
||
final BuildContext chipContext;
|
||
final GlobalKey deleteIconKey;
|
||
|
||
@override
|
||
InteractiveInkFeature create({
|
||
required MaterialInkController controller,
|
||
required RenderBox referenceBox,
|
||
required Offset position,
|
||
required Color color,
|
||
required TextDirection textDirection,
|
||
bool containedInkWell = false,
|
||
RectCallback? rectCallback,
|
||
BorderRadius? borderRadius,
|
||
ShapeBorder? customBorder,
|
||
double? radius,
|
||
VoidCallback? onRemoved,
|
||
}) {
|
||
|
||
final bool tapIsOnDeleteIcon = _tapIsOnDeleteIcon(
|
||
hasDeleteButton: hasDeleteButton,
|
||
tapPosition: position,
|
||
chipSize: chipContext.size!,
|
||
textDirection: textDirection,
|
||
);
|
||
|
||
final BuildContext splashContext = tapIsOnDeleteIcon
|
||
? deleteIconKey.currentContext!
|
||
: chipContext;
|
||
|
||
final InteractiveInkFeatureFactory splashFactory = Theme.of(splashContext).splashFactory;
|
||
|
||
if (tapIsOnDeleteIcon) {
|
||
final RenderBox currentBox = referenceBox;
|
||
referenceBox = deleteIconKey.currentContext!.findRenderObject()! as RenderBox;
|
||
position = referenceBox.globalToLocal(currentBox.localToGlobal(position));
|
||
containedInkWell = false;
|
||
}
|
||
|
||
return splashFactory.create(
|
||
controller: controller,
|
||
referenceBox: referenceBox,
|
||
position: position,
|
||
color: color,
|
||
textDirection: textDirection,
|
||
containedInkWell: containedInkWell,
|
||
rectCallback: rectCallback,
|
||
borderRadius: borderRadius,
|
||
customBorder: customBorder,
|
||
radius: radius,
|
||
onRemoved: onRemoved,
|
||
);
|
||
}
|
||
}
|
||
|
||
bool _tapIsOnDeleteIcon({
|
||
required bool hasDeleteButton,
|
||
required Offset tapPosition,
|
||
required Size chipSize,
|
||
required TextDirection textDirection,
|
||
}) {
|
||
bool tapIsOnDeleteIcon;
|
||
if (!hasDeleteButton) {
|
||
tapIsOnDeleteIcon = false;
|
||
} else {
|
||
switch (textDirection) {
|
||
case TextDirection.ltr:
|
||
tapIsOnDeleteIcon = tapPosition.dx / chipSize.width > 0.66;
|
||
break;
|
||
case TextDirection.rtl:
|
||
tapIsOnDeleteIcon = tapPosition.dx / chipSize.width < 0.33;
|
||
break;
|
||
}
|
||
}
|
||
return tapIsOnDeleteIcon;
|
||
}
|