| // 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/foundation.dart' show clampDouble; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'chip_theme.dart'; |
| import 'colors.dart'; |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'icons.dart'; |
| import 'ink_well.dart'; |
| import 'material.dart'; |
| import 'material_localizations.dart'; |
| import 'material_state.dart'; |
| import 'material_state_mixin.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. |
| /// |
| /// The default label style is [TextTheme.bodyLarge] from the overall |
| /// theme's [ThemeData.textTheme]. |
| // |
| /// 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 null. |
| Color? get shadowColor; |
| |
| /// Color of the chip's surface tint overlay when its elevation is |
| /// greater than 0. |
| /// |
| /// The default is null. |
| Color? get surfaceTintColor; |
| |
| /// Theme used for all icons in the chip. |
| /// |
| /// The default is null. |
| IconThemeData? get iconTheme; |
| } |
| |
| /// 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} |
| /// This sample shows how to use [onDeleted] to remove an entry when the |
| /// delete button is tapped. |
| /// |
| /// ** See code in examples/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart ** |
| /// {@end-tool} |
| VoidCallback? get onDeleted; |
| |
| /// Used to define the delete icon's color with an [IconTheme] that |
| /// contains the icon. |
| /// |
| /// The default is `Color(0xde000000)` |
| /// (slightly transparent black) for light themes, and `Color(0xdeffffff)` |
| /// (slightly transparent white) for dark themes. |
| /// |
| /// The delete icon appears if [DeletableChipAttributes.onDeleted] is |
| /// non-null. |
| Color? get deleteIconColor; |
| |
| /// The message to be used for the chip's delete button tooltip. |
| /// |
| /// If provided with an empty string, the tooltip of the delete button will be |
| /// disabled. |
| /// |
| /// If null, the default [MaterialLocalizations.deleteButtonTooltip] will be |
| /// used. |
| String? get deleteButtonTooltipMessage; |
| |
| /// Whether to use a tooltip on the chip's delete button showing the |
| /// [deleteButtonTooltipMessage]. |
| /// |
| /// Defaults to true. |
| @Deprecated( |
| 'Migrate to deleteButtonTooltipMessage. ' |
| 'This feature was deprecated after v2.10.0-0.3.pre.' |
| ) |
| bool get useDeleteButtonTooltip; |
| } |
| |
| /// 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({super.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; |
| |
| /// The color used for the chip's background to indicate that it is not |
| /// enabled. |
| /// |
| /// 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({super.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({ |
| super.key, |
| this.avatar, |
| required this.label, |
| this.labelStyle, |
| this.labelPadding, |
| this.deleteIcon, |
| this.onDeleted, |
| this.deleteIconColor, |
| 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, |
| this.surfaceTintColor, |
| this.iconTheme, |
| @Deprecated( |
| 'Migrate to deleteButtonTooltipMessage. ' |
| 'This feature was deprecated after v2.10.0-0.3.pre.' |
| ) |
| this.useDeleteButtonTooltip = true, |
| }) : assert(elevation == null || elevation >= 0.0); |
| |
| @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 String? deleteButtonTooltipMessage; |
| @override |
| final MaterialTapTargetSize? materialTapTargetSize; |
| @override |
| final double? elevation; |
| @override |
| final Color? shadowColor; |
| @override |
| final Color? surfaceTintColor; |
| @override |
| final IconThemeData? iconTheme; |
| @override |
| @Deprecated( |
| 'Migrate to deleteButtonTooltipMessage. ' |
| 'This feature was deprecated after v2.10.0-0.3.pre.' |
| ) |
| final bool useDeleteButtonTooltip; |
| |
| @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, |
| surfaceTintColor: surfaceTintColor, |
| ); |
| } |
| } |
| |
| /// 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({ |
| super.key, |
| this.defaultProperties, |
| this.avatar, |
| required this.label, |
| this.labelStyle, |
| this.padding, |
| this.visualDensity, |
| this.labelPadding, |
| Widget? deleteIcon, |
| this.onDeleted, |
| this.deleteIconColor, |
| 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.surfaceTintColor, |
| this.iconTheme, |
| this.selectedShadowColor, |
| this.showCheckmark = true, |
| this.checkmarkColor, |
| this.avatarBorder = const CircleBorder(), |
| @Deprecated( |
| 'Migrate to deleteButtonTooltipMessage. ' |
| 'This feature was deprecated after v2.10.0-0.3.pre.' |
| ) |
| this.useDeleteButtonTooltip = true, |
| }) : assert(pressElevation == null || pressElevation >= 0.0), |
| assert(elevation == null || elevation >= 0.0), |
| deleteIcon = deleteIcon ?? _kDefaultDeleteIcon; |
| |
| /// Defines the defaults for the chip properties if |
| /// they are not specified elsewhere. |
| /// |
| /// If null then [ChipThemeData.fromDefaults] will be used |
| /// for the default properties. |
| final ChipThemeData? defaultProperties; |
| |
| @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 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? surfaceTintColor; |
| @override |
| final IconThemeData? iconTheme; |
| @override |
| final Color? selectedShadowColor; |
| @override |
| final bool? showCheckmark; |
| @override |
| final Color? checkmarkColor; |
| @override |
| final ShapeBorder avatarBorder; |
| @override |
| @Deprecated( |
| 'Migrate to deleteButtonTooltipMessage. ' |
| 'This feature was deprecated after v2.10.0-0.3.pre.' |
| ) |
| final bool useDeleteButtonTooltip; |
| |
| /// 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 |
| State<RawChip> createState() => _RawChipState(); |
| } |
| |
| class _RawChipState extends State<RawChip> with MaterialStateMixin, 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; |
| |
| 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(); |
| setMaterialState(MaterialState.disabled, !widget.isEnabled); |
| setMaterialState(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 _handleTapDown(TapDownDetails details) { |
| if (!canTap) { |
| return; |
| } |
| setMaterialState(MaterialState.pressed, true); |
| setState(() { |
| _isTapping = true; |
| }); |
| } |
| |
| void _handleTapCancel() { |
| if (!canTap) { |
| return; |
| } |
| setMaterialState(MaterialState.pressed, false); |
| setState(() { |
| _isTapping = false; |
| }); |
| } |
| |
| void _handleTap() { |
| if (!canTap) { |
| return; |
| } |
| setMaterialState(MaterialState.pressed, false); |
| setState(() { |
| _isTapping = false; |
| }); |
| // Only one of these can be set, so only one will be called. |
| widget.onSelected?.call(!widget.selected); |
| widget.onPressed?.call(); |
| } |
| |
| OutlinedBorder _getShape(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) { |
| final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, materialStates) |
| ?? MaterialStateProperty.resolveAs<BorderSide?>(chipTheme.side, materialStates) |
| ?? MaterialStateProperty.resolveAs<BorderSide?>(chipDefaults.side, materialStates); |
| final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, materialStates) |
| ?? MaterialStateProperty.resolveAs<OutlinedBorder?>(chipTheme.shape, materialStates) |
| ?? MaterialStateProperty.resolveAs<OutlinedBorder?>(chipDefaults.shape, materialStates) |
| ?? const StadiumBorder(); |
| return resolvedShape.copyWith(side: resolvedSide); |
| } |
| |
| /// Picks between three different colors, depending upon the state of two |
| /// different animations. |
| Color? _getBackgroundColor(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) { |
| if (theme.useMaterial3) { |
| final ColorTween backgroundTween = ColorTween( |
| begin: widget.disabledColor |
| ?? chipTheme.disabledColor |
| ?? chipDefaults.disabledColor, |
| end: widget.backgroundColor |
| ?? chipTheme.backgroundColor |
| ?? chipDefaults.backgroundColor, |
| ); |
| final ColorTween selectTween = ColorTween( |
| begin: backgroundTween.evaluate(enableController), |
| end: widget.selectedColor |
| ?? chipTheme.selectedColor |
| ?? chipDefaults.selectedColor, |
| ); |
| return selectTween.evaluate(selectionFade); |
| } else { |
| final ColorTween backgroundTween = ColorTween( |
| begin: widget.disabledColor |
| ?? chipTheme.disabledColor |
| ?? theme.disabledColor, |
| end: widget.backgroundColor |
| ?? chipTheme.backgroundColor |
| ?? theme.chipTheme.backgroundColor |
| ?? chipDefaults.backgroundColor, |
| ); |
| final ColorTween selectTween = ColorTween( |
| begin: backgroundTween.evaluate(enableController), |
| end: widget.selectedColor |
| ?? chipTheme.selectedColor |
| ?? theme.chipTheme.selectedColor |
| ?? chipDefaults.selectedColor, |
| ); |
| return selectTween.evaluate(selectionFade); |
| } |
| } |
| |
| @override |
| void didUpdateWidget(RawChip oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (oldWidget.isEnabled != widget.isEnabled) { |
| setState(() { |
| setMaterialState(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(() { |
| setMaterialState(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, bool enabled = true, Widget? child}) { |
| if (child == null || !enabled || tooltip == null) { |
| return child; |
| } |
| return Tooltip( |
| message: tooltip, |
| child: child, |
| ); |
| } |
| |
| Widget? _buildDeleteIcon( |
| BuildContext context, |
| ThemeData theme, |
| ChipThemeData chipTheme, |
| ChipThemeData chipDefaults, |
| ) { |
| if (!hasDeleteButton) { |
| return null; |
| } |
| return Semantics( |
| container: true, |
| button: true, |
| child: _wrapWithTooltip( |
| tooltip: widget.useDeleteButtonTooltip |
| ? widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip |
| : null, |
| enabled: widget.onDeleted != null, |
| child: InkWell( |
| // Radius should be slightly less than the full size of the chip. |
| radius: (_kChipHeight + (widget.padding?.vertical ?? 0.0)) * .45, |
| // Keeps the splash from being constrained to the icon alone. |
| splashFactory: _UnconstrainedInkSplashFactory(Theme.of(context).splashFactory), |
| onTap: widget.isEnabled ? widget.onDeleted : null, |
| child: IconTheme( |
| data: theme.iconTheme.copyWith( |
| color: widget.deleteIconColor |
| ?? chipTheme.deleteIconColor |
| ?? theme.chipTheme.deleteIconColor |
| ?? chipDefaults.deleteIconColor, |
| ), |
| child: widget.deleteIcon, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| @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), |
| clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), |
| )!; |
| |
| final ThemeData theme = Theme.of(context); |
| final ChipThemeData chipTheme = ChipTheme.of(context); |
| final Brightness brightness = chipTheme.brightness ?? theme.brightness; |
| final ChipThemeData chipDefaults = widget.defaultProperties ?? |
| (theme.useMaterial3 |
| ? _ChipDefaultsM3(context, widget.isEnabled) |
| : ChipThemeData.fromDefaults( |
| brightness: brightness, |
| secondaryColor: brightness == Brightness.dark ? Colors.tealAccent[200]! : theme.primaryColor, |
| labelStyle: theme.textTheme.bodyLarge!, |
| ) |
| ); |
| final TextDirection? textDirection = Directionality.maybeOf(context); |
| final OutlinedBorder resolvedShape = _getShape(theme, chipTheme, chipDefaults); |
| |
| final double elevation = widget.elevation |
| ?? chipTheme.elevation |
| ?? chipDefaults.elevation |
| ?? 0; |
| final double pressElevation = widget.pressElevation |
| ?? chipTheme.pressElevation |
| ?? chipDefaults.pressElevation |
| ?? 0; |
| final Color? shadowColor = widget.shadowColor |
| ?? chipTheme.shadowColor |
| ?? chipDefaults.shadowColor; |
| final Color? surfaceTintColor = widget.surfaceTintColor |
| ?? chipTheme.surfaceTintColor |
| ?? chipDefaults.surfaceTintColor; |
| final Color? selectedShadowColor = widget.selectedShadowColor |
| ?? chipTheme.selectedShadowColor |
| ?? chipDefaults.selectedShadowColor; |
| final Color? checkmarkColor = widget.checkmarkColor |
| ?? chipTheme.checkmarkColor |
| ?? chipDefaults.checkmarkColor; |
| final bool showCheckmark = widget.showCheckmark |
| ?? chipTheme.showCheckmark |
| ?? chipDefaults.showCheckmark!; |
| final EdgeInsetsGeometry padding = widget.padding |
| ?? chipTheme.padding |
| ?? chipDefaults.padding!; |
| // Widget's label style is merged with this below. |
| final TextStyle labelStyle = chipTheme.labelStyle |
| ?? chipDefaults.labelStyle!; |
| final EdgeInsetsGeometry labelPadding = widget.labelPadding |
| ?? chipTheme.labelPadding |
| ?? chipDefaults.labelPadding |
| ?? defaultLabelPadding; |
| final IconThemeData? iconTheme = widget.iconTheme |
| ?? chipTheme.iconTheme |
| ?? chipDefaults.iconTheme; |
| |
| final TextStyle effectiveLabelStyle = labelStyle.merge(widget.labelStyle); |
| final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, materialStates); |
| final TextStyle resolvedLabelStyle = effectiveLabelStyle.copyWith(color: resolvedLabelColor); |
| final Widget? avatar = iconTheme != null && hasAvatar |
| ? IconTheme(data: iconTheme, child: widget.avatar!) |
| : widget.avatar; |
| |
| Widget result = Material( |
| elevation: isTapping ? pressElevation : elevation, |
| shadowColor: widget.selected ? selectedShadowColor : shadowColor, |
| surfaceTintColor: surfaceTintColor, |
| animationDuration: pressedAnimationDuration, |
| shape: resolvedShape, |
| clipBehavior: widget.clipBehavior, |
| child: InkWell( |
| onFocusChange: updateMaterialState(MaterialState.focused), |
| focusNode: widget.focusNode, |
| autofocus: widget.autofocus, |
| canRequestFocus: widget.isEnabled, |
| onTap: canTap ? _handleTap : null, |
| onTapDown: canTap ? _handleTapDown : null, |
| onTapCancel: canTap ? _handleTapCancel : null, |
| onHover: canTap ? updateMaterialState(MaterialState.hovered) : null, |
| customBorder: resolvedShape, |
| child: AnimatedBuilder( |
| animation: Listenable.merge(<Listenable>[selectController, enableController]), |
| builder: (BuildContext context, Widget? child) { |
| return Container( |
| decoration: ShapeDecoration( |
| shape: resolvedShape, |
| color: _getBackgroundColor(theme, chipTheme, chipDefaults), |
| ), |
| child: child, |
| ); |
| }, |
| child: _wrapWithTooltip( |
| tooltip: widget.tooltip, |
| enabled: widget.onPressed != null || widget.onSelected != null, |
| child: _ChipRenderWidget( |
| theme: _ChipRenderTheme( |
| label: DefaultTextStyle( |
| overflow: TextOverflow.fade, |
| textAlign: TextAlign.start, |
| maxLines: 1, |
| softWrap: false, |
| style: resolvedLabelStyle, |
| child: widget.label, |
| ), |
| avatar: AnimatedSwitcher( |
| duration: _kDrawerDuration, |
| switchInCurve: Curves.fastOutSlowIn, |
| child: avatar, |
| ), |
| deleteIcon: AnimatedSwitcher( |
| duration: _kDrawerDuration, |
| switchInCurve: Curves.fastOutSlowIn, |
| child: _buildDeleteIcon(context, theme, chipTheme, chipDefaults), |
| ), |
| brightness: brightness, |
| padding: 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( |
| widthFactor: 1.0, |
| heightFactor: 1.0, |
| child: result, |
| ), |
| ); |
| return Semantics( |
| button: widget.tapEnabled, |
| container: true, |
| selected: widget.selected, |
| enabled: widget.tapEnabled ? canTap : null, |
| child: result, |
| ); |
| } |
| } |
| |
| /// Redirects the [buttonRect.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({ |
| super.child, |
| required this.constraints, |
| }); |
| |
| 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 with SlottedMultiChildRenderObjectWidgetMixin<_ChipSlot> { |
| const _ChipRenderWidget({ |
| required this.theme, |
| this.value, |
| this.isEnabled, |
| required this.checkmarkAnimation, |
| required this.avatarDrawerAnimation, |
| required this.deleteDrawerAnimation, |
| required this.enableAnimation, |
| this.avatarBorder, |
| }); |
| |
| 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 |
| Iterable<_ChipSlot> get slots => _ChipSlot.values; |
| |
| @override |
| Widget? childForSlot(_ChipSlot slot) { |
| switch (slot) { |
| case _ChipSlot.label: |
| return theme.label; |
| case _ChipSlot.avatar: |
| return theme.avatar; |
| case _ChipSlot.deleteIcon: |
| return theme.deleteIcon; |
| } |
| } |
| |
| @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 |
| SlottedContainerRenderObjectMixin<_ChipSlot> 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, |
| } |
| |
| @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 => Object.hash( |
| avatar, |
| label, |
| deleteIcon, |
| brightness, |
| padding, |
| labelPadding, |
| showAvatar, |
| showCheckmark, |
| checkmarkColor, |
| canTapBody, |
| ); |
| } |
| |
| class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_ChipSlot> { |
| _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, |
| }) : _theme = theme, |
| _textDirection = textDirection { |
| checkmarkAnimation.addListener(markNeedsPaint); |
| avatarDrawerAnimation.addListener(markNeedsLayout); |
| deleteDrawerAnimation.addListener(markNeedsLayout); |
| enableAnimation.addListener(markNeedsPaint); |
| } |
| |
| 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? get avatar => childForSlot(_ChipSlot.avatar); |
| RenderBox? get deleteIcon => childForSlot(_ChipSlot.deleteIcon); |
| RenderBox? get label => childForSlot(_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. |
| @override |
| Iterable<RenderBox> get children { |
| return <RenderBox>[ |
| if (avatar != null) |
| avatar!, |
| if (label != null) |
| label!, |
| if (deleteIcon != null) |
| deleteIcon!, |
| ]; |
| } |
| |
| bool get isDrawingCheckmark => theme.showCheckmark && !checkmarkAnimation.isDismissed; |
| bool get deleteIconShowing => !deleteDrawerAnimation.isDismissed; |
| |
| @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( |
| 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, |
| 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 hitIsOnDeleteIcon = deleteIcon != null && _hitIsOnDeleteIcon( |
| padding: theme.padding, |
| tapPosition: position, |
| chipSize: size, |
| deleteButtonSize: deleteIcon!.size, |
| textDirection: textDirection!, |
| ); |
| final RenderBox? hitTestChild = hitIsOnDeleteIcon |
| ? (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.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 _UnconstrainedInkSplashFactory extends InteractiveInkFeatureFactory { |
| const _UnconstrainedInkSplashFactory(this.parentFactory); |
| |
| final InteractiveInkFeatureFactory parentFactory; |
| |
| @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, |
| }) { |
| return parentFactory.create( |
| controller: controller, |
| referenceBox: referenceBox, |
| position: position, |
| color: color, |
| rectCallback: rectCallback, |
| borderRadius: borderRadius, |
| customBorder: customBorder, |
| radius: radius, |
| onRemoved: onRemoved, |
| textDirection: textDirection, |
| ); |
| } |
| } |
| |
| bool _hitIsOnDeleteIcon({ |
| required EdgeInsetsGeometry padding, |
| required Offset tapPosition, |
| required Size chipSize, |
| required Size deleteButtonSize, |
| required TextDirection textDirection, |
| }) { |
| // The chipSize includes the padding, so we need to deflate the size and adjust the |
| // tap position to account for the padding. |
| final EdgeInsets resolvedPadding = padding.resolve(textDirection); |
| final Size deflatedSize = resolvedPadding.deflateSize(chipSize); |
| final Offset adjustedPosition = tapPosition - Offset(resolvedPadding.left, resolvedPadding.top); |
| // The delete button hit area should be at least the width of the delete |
| // button, but, if there's room, up to 24 pixels from the center of the delete |
| // icon (corresponding to part of a 48x48 square that Material would prefer |
| // for touch targets), but no more than approximately half of the overall size |
| // of the chip when the chip is small. |
| // |
| // This isn't affected by materialTapTargetSize because it only applies to the |
| // width of the tappable region within the chip, not outside of the chip, |
| // which is handled elsewhere. Also because delete buttons aren't specified to |
| // be used on touch devices, only desktop devices. |
| |
| // Max out at not quite half, so that tests that tap on the center of a small |
| // chip will still hit the chip, not the delete button. |
| final double accessibleDeleteButtonWidth = math.min( |
| deflatedSize.width * 0.499, |
| math.max(deleteButtonSize.width, 24.0 + deleteButtonSize.width / 2.0), |
| ); |
| switch (textDirection) { |
| case TextDirection.ltr: |
| return adjustedPosition.dx >= deflatedSize.width - accessibleDeleteButtonWidth; |
| case TextDirection.rtl: |
| return adjustedPosition.dx <= accessibleDeleteButtonWidth; |
| } |
| } |
| |
| // BEGIN GENERATED TOKEN PROPERTIES - Chip |
| |
| // Do not edit by hand. The code between the "BEGIN GENERATED" and |
| // "END GENERATED" comments are generated from data in the Material |
| // Design token database by the script: |
| // dev/tools/gen_defaults/bin/gen_defaults.dart. |
| |
| // Token database version: v0_152 |
| |
| class _ChipDefaultsM3 extends ChipThemeData { |
| const _ChipDefaultsM3(this.context, this.isEnabled) |
| : super( |
| elevation: 0.0, |
| shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))), |
| showCheckmark: true, |
| ); |
| |
| final BuildContext context; |
| final bool isEnabled; |
| |
| @override |
| TextStyle? get labelStyle => Theme.of(context).textTheme.labelLarge; |
| |
| @override |
| Color? get backgroundColor => null; |
| |
| @override |
| Color? get shadowColor => Colors.transparent; |
| |
| @override |
| Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint; |
| |
| @override |
| Color? get selectedColor => null; |
| |
| @override |
| Color? get checkmarkColor => null; |
| |
| @override |
| Color? get disabledColor => null; |
| |
| @override |
| Color? get deleteIconColor => null; |
| |
| @override |
| BorderSide? get side => isEnabled |
| ? BorderSide(color: Theme.of(context).colorScheme.outline) |
| : BorderSide(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12)); |
| |
| @override |
| IconThemeData? get iconTheme => IconThemeData( |
| color: isEnabled |
| ? Theme.of(context).colorScheme.primary |
| : Theme.of(context).colorScheme.onSurface, |
| size: 18.0, |
| ); |
| |
| @override |
| EdgeInsetsGeometry? get padding => const EdgeInsets.all(8.0); |
| |
| /// 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. |
| @override |
| EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp( |
| const EdgeInsets.symmetric(horizontal: 8.0), |
| const EdgeInsets.symmetric(horizontal: 4.0), |
| clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0), |
| )!; |
| } |
| |
| // END GENERATED TOKEN PROPERTIES - Chip |