blob: f12ecd2712af2b761fc64285cca454bef19394c8 [file] [log] [blame]
// 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:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
/// Applies a chip theme to descendant [RawChip]-based widgets, like [Chip],
/// [InputChip], [ChoiceChip], [FilterChip], and [ActionChip].
///
/// A chip theme describes the color, shape and text styles for the chips it is
/// applied to.
///
/// Descendant widgets obtain the current theme's [ChipThemeData] object using
/// [ChipTheme.of]. When a widget uses [ChipTheme.of], it is automatically
/// rebuilt if the theme later changes.
///
/// The [ThemeData] object given by the [Theme.of] call also contains a default
/// [ThemeData.chipTheme] that can be customized by copying it (using
/// [ChipThemeData.copyWith]).
///
/// 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.
/// * [ChipThemeData], which describes the actual configuration of a chip
/// theme.
/// * [ThemeData], which describes the overall theme information for the
/// application.
class ChipTheme extends InheritedTheme {
/// Applies the given theme [data] to [child].
///
/// The [data] and [child] arguments must not be null.
const ChipTheme({
Key? key,
required this.data,
required Widget child,
}) : assert(child != null),
assert(data != null),
super(key: key, child: child);
/// Specifies the color, shape, and text style values for descendant chip
/// widgets.
final ChipThemeData data;
/// Returns the data from the closest [ChipTheme] instance that encloses
/// the given context.
///
/// Defaults to the ambient [ThemeData.chipTheme] if there is no
/// [ChipTheme] in the given build context.
///
/// {@tool snippet}
///
/// ```dart
/// class Spaceship extends StatelessWidget {
/// const Spaceship({Key? key}) : super(key: key);
///
/// @override
/// Widget build(BuildContext context) {
/// return ChipTheme(
/// data: ChipTheme.of(context).copyWith(backgroundColor: Colors.red),
/// child: ActionChip(
/// label: const Text('Launch'),
/// onPressed: () { print('We have liftoff!'); },
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [ChipThemeData], which describes the actual configuration of a chip
/// theme.
static ChipThemeData of(BuildContext context) {
final ChipTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<ChipTheme>();
return inheritedTheme?.data ?? Theme.of(context).chipTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return ChipTheme(data: data, child: child);
}
@override
bool updateShouldNotify(ChipTheme oldWidget) => data != oldWidget.data;
}
/// Holds the color, shape, and text styles for a material design chip theme.
///
/// Use this class to configure a [ChipTheme] widget, or to set the
/// [ThemeData.chipTheme] for a [Theme] widget.
///
/// To obtain the current ambient chip theme, use [ChipTheme.of].
///
/// The parts of a chip are:
///
/// * The "avatar", which is a widget that appears at the beginning of the
/// chip. This is typically a [CircleAvatar] widget.
/// * The "label", which is the widget displayed in the center of the chip.
/// Typically this is a [Text] widget.
/// * The "delete icon", which is a widget that appears at the end of the chip.
/// * The chip is disabled when it is not accepting user input. Only some chips
/// have a disabled state: [InputChip], [ChoiceChip], and [FilterChip].
///
/// The simplest way to create a ChipThemeData is to use [copyWith] on the one
/// you get from [ChipTheme.of], or create an entirely new one with
/// [ChipThemeData.fromDefaults].
///
/// {@tool snippet}
///
/// ```dart
/// class CarColor extends StatefulWidget {
/// const CarColor({Key? key}) : super(key: key);
///
/// @override
/// State createState() => _CarColorState();
/// }
///
/// class _CarColorState extends State<CarColor> {
/// Color _color = Colors.red;
///
/// @override
/// Widget build(BuildContext context) {
/// return ChipTheme(
/// data: ChipTheme.of(context).copyWith(backgroundColor: Colors.lightBlue),
/// child: ChoiceChip(
/// label: const Text('Light Blue'),
/// onSelected: (bool value) {
/// setState(() {
/// _color = value ? Colors.lightBlue : Colors.red;
/// });
/// },
/// selected: _color == Colors.lightBlue,
/// ),
/// );
/// }
/// }
/// ```
/// {@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.
/// * [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.
/// * [ChipTheme] widget, which can override the chip theme of its
/// children.
/// * [Theme] widget, which performs a similar function to [ChipTheme],
/// but for overall themes.
/// * [ThemeData], which has a default [ChipThemeData].
@immutable
class ChipThemeData with Diagnosticable {
/// Create a [ChipThemeData] given a set of exact values. All the values
/// must be specified except for [shadowColor], [selectedShadowColor],
/// [elevation], and [pressElevation], which may be null.
///
/// This will rarely be used directly. It is used by [lerp] to
/// create intermediate themes based on two themes.
const ChipThemeData({
this.backgroundColor,
this.deleteIconColor,
this.disabledColor,
this.selectedColor,
this.secondarySelectedColor,
this.shadowColor,
this.selectedShadowColor,
this.showCheckmark,
this.checkmarkColor,
this.labelPadding,
this.padding,
this.side,
this.shape,
this.labelStyle,
this.secondaryLabelStyle,
this.brightness,
this.elevation,
this.pressElevation,
});
/// Generates a ChipThemeData from a brightness, a primary color, and a text
/// style.
///
/// The [brightness] is used to select a primary color from the default
/// values.
///
/// The optional [primaryColor] is used as the base color for the other
/// colors. The opacity of the [primaryColor] is ignored. If a [primaryColor]
/// is specified, then the [brightness] is ignored, and the theme brightness
/// is determined from the [primaryColor].
///
/// Only one of [primaryColor] or [brightness] may be specified.
///
/// The [secondaryColor] is used for the selection colors needed by
/// [ChoiceChip].
///
/// This is used to generate the default chip theme for a [ThemeData].
factory ChipThemeData.fromDefaults({
Brightness? brightness,
Color? primaryColor,
required Color secondaryColor,
required TextStyle labelStyle,
}) {
assert(primaryColor != null || brightness != null, 'One of primaryColor or brightness must be specified');
assert(primaryColor == null || brightness == null, 'Only one of primaryColor or brightness may be specified');
assert(secondaryColor != null);
assert(labelStyle != null);
if (primaryColor != null) {
brightness = ThemeData.estimateBrightnessForColor(primaryColor);
}
// These are Material Design defaults, and are used to derive
// component Colors (with opacity) from base colors.
const int backgroundAlpha = 0x1f; // 12%
const int deleteIconAlpha = 0xde; // 87%
const int disabledAlpha = 0x0c; // 38% * 12% = 5%
const int selectAlpha = 0x3d; // 12% + 12% = 24%
const int textLabelAlpha = 0xde; // 87%
const EdgeInsetsGeometry padding = EdgeInsets.all(4.0);
primaryColor = primaryColor ?? (brightness == Brightness.light ? Colors.black : Colors.white);
final Color backgroundColor = primaryColor.withAlpha(backgroundAlpha);
final Color deleteIconColor = primaryColor.withAlpha(deleteIconAlpha);
final Color disabledColor = primaryColor.withAlpha(disabledAlpha);
final Color selectedColor = primaryColor.withAlpha(selectAlpha);
final Color secondarySelectedColor = secondaryColor.withAlpha(selectAlpha);
final TextStyle secondaryLabelStyle = labelStyle.copyWith(
color: secondaryColor.withAlpha(textLabelAlpha),
);
labelStyle = labelStyle.copyWith(color: primaryColor.withAlpha(textLabelAlpha));
return ChipThemeData(
backgroundColor: backgroundColor,
deleteIconColor: deleteIconColor,
disabledColor: disabledColor,
selectedColor: selectedColor,
secondarySelectedColor: secondarySelectedColor,
padding: padding,
labelStyle: labelStyle,
secondaryLabelStyle: secondaryLabelStyle,
brightness: brightness,
);
}
/// Overrides the default for [ChipAttributes.backgroundColor]
/// which is used for unselected, enabled chip backgrounds.
///
/// This property applies to [ActionChip], [Chip], [ChoiceChip],
/// [FilterChip], [InputChip], [RawChip].
final Color? backgroundColor;
/// Overrides the default for [DeletableChipAttributes.deleteIconColor].
///
/// This property applies to [Chip], [InputChip], [RawChip].
final Color? deleteIconColor;
/// Overrides the default for
/// [DisabledChipAttributes.disabledColor], the background color
/// which indicates that the chip is not enabled.
///
/// This property applies to [ChoiceChip], [FilterChip],
/// [InputChip], [RawChip].
final Color? disabledColor;
/// Overrides the default for
/// [SelectableChipAttributes.selectedColor], the background color
/// that indicates that the chip is selected.
///
/// This property applies to [ChoiceChip], [FilterChip],
/// [InputChip], [RawChip].
final Color? selectedColor;
/// Overrides the default for [ChoiceChip.selectedColor], the
/// background color that indicates that the chip is selected.
final Color? secondarySelectedColor;
/// Overrides the default for [ChipAttributes.shadowColor], the
/// Color of the chip's shadow when its elevation is greater than 0.
///
/// This property applies to [ActionChip], [Chip], [ChoiceChip],
/// [FilterChip], [InputChip], [RawChip].
final Color? shadowColor;
/// Overrides the default for
/// [SelectableChipAttributes.selectedShadowColor], the Color of the
/// chip's shadow when its elevation is greater than 0 and the chip
/// is selected.
///
/// This property applies to [ChoiceChip], [FilterChip],
/// [InputChip], [RawChip].
final Color? selectedShadowColor;
/// Overrides the default for
/// [CheckmarkableChipAttributes.showCheckmark], which indicates if
/// a check mark should be shown.
///
/// This property applies to [FilterChip], [InputChip], [RawChip].
final bool? showCheckmark;
/// Overrides the default for
/// [CheckmarkableChipAttributes.checkmarkColor].
///
/// This property applies to [FilterChip], [InputChip], [RawChip].
final Color? checkmarkColor;
/// Overrides the default for [ChipAttributes.labelPadding],
/// the padding around the chip's label widget.
///
/// This property applies to [ActionChip], [Chip], [ChoiceChip],
/// [FilterChip], [InputChip], [RawChip].
final EdgeInsetsGeometry? labelPadding;
/// Overrides the default for [ChipAttributes.padding],
/// the padding between the contents of the chip and the outside [shape].
///
/// This property applies to [ActionChip], [Chip], [ChoiceChip],
/// [FilterChip], [InputChip], [RawChip].
final EdgeInsetsGeometry? padding;
/// Overrides the default for [ChipAttributes.side],
/// the color and weight of the chip's outline.
///
/// 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].
///
/// This property applies to [ActionChip], [Chip], [ChoiceChip],
/// [FilterChip], [InputChip], [RawChip].
final BorderSide? side;
/// Overrides the default for [ChipAttributes.shape],
/// the shape of border to draw around the chip.
///
/// 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].
///
/// This property applies to [ActionChip], [Chip], [ChoiceChip],
/// [FilterChip], [InputChip], [RawChip].
final OutlinedBorder? shape;
/// Overrides the default for [ChipAttributes.labelStyle],
/// the style of the [DefaultTextStyle] that contains the
/// chip's label.
///
/// This only has an effect on label widgets that respect the
/// [DefaultTextStyle], such as [Text].
///
/// This property applies to [ActionChip], [Chip],
/// [FilterChip], [InputChip], [RawChip].
final TextStyle? labelStyle;
/// Overrides the default for [ChoiceChip.labelStyle],
/// the style of the [DefaultTextStyle] that contains the
/// chip's label.
///
/// This only has an effect on label widgets that respect the
/// [DefaultTextStyle], such as [Text].
final TextStyle? secondaryLabelStyle;
/// Overrides the default value for all chips which affects various base
/// material color choices in the chip rendering.
final Brightness? brightness;
/// Overrides the default for [ChipAttributes.elevation],
/// the elevation of the chip's [Material].
///
/// This property applies to [ActionChip], [Chip], [ChoiceChip],
/// [FilterChip], [InputChip], [RawChip].
final double? elevation;
/// Overrides the default for [TappableChipAttributes.pressElevation],
/// the elevation of the chip's [Material] during a "press" or tap down.
///
/// This property applies to [ActionChip], [InputChip], [RawChip].
final double? pressElevation;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
ChipThemeData copyWith({
Color? backgroundColor,
Color? deleteIconColor,
Color? disabledColor,
Color? selectedColor,
Color? secondarySelectedColor,
Color? shadowColor,
Color? selectedShadowColor,
Color? checkmarkColor,
EdgeInsetsGeometry? labelPadding,
EdgeInsetsGeometry? padding,
BorderSide? side,
OutlinedBorder? shape,
TextStyle? labelStyle,
TextStyle? secondaryLabelStyle,
Brightness? brightness,
double? elevation,
double? pressElevation,
}) {
return ChipThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
deleteIconColor: deleteIconColor ?? this.deleteIconColor,
disabledColor: disabledColor ?? this.disabledColor,
selectedColor: selectedColor ?? this.selectedColor,
secondarySelectedColor: secondarySelectedColor ?? this.secondarySelectedColor,
shadowColor: shadowColor ?? this.shadowColor,
selectedShadowColor: selectedShadowColor ?? this.selectedShadowColor,
checkmarkColor: checkmarkColor ?? this.checkmarkColor,
labelPadding: labelPadding ?? this.labelPadding,
padding: padding ?? this.padding,
side: side ?? this.side,
shape: shape ?? this.shape,
labelStyle: labelStyle ?? this.labelStyle,
secondaryLabelStyle: secondaryLabelStyle ?? this.secondaryLabelStyle,
brightness: brightness ?? this.brightness,
elevation: elevation ?? this.elevation,
pressElevation: pressElevation ?? this.pressElevation,
);
}
/// Linearly interpolate between two chip themes.
///
/// The arguments must not be null.
///
/// {@macro dart.ui.shadow.lerp}
static ChipThemeData? lerp(ChipThemeData? a, ChipThemeData? b, double t) {
assert(t != null);
if (a == null && b == null)
return null;
return ChipThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
deleteIconColor: Color.lerp(a?.deleteIconColor, b?.deleteIconColor, t),
disabledColor: Color.lerp(a?.disabledColor, b?.disabledColor, t),
selectedColor: Color.lerp(a?.selectedColor, b?.selectedColor, t),
secondarySelectedColor: Color.lerp(a?.secondarySelectedColor, b?.secondarySelectedColor, t),
shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
selectedShadowColor: Color.lerp(a?.selectedShadowColor, b?.selectedShadowColor, t),
checkmarkColor: Color.lerp(a?.checkmarkColor, b?.checkmarkColor, t),
labelPadding: EdgeInsetsGeometry.lerp(a?.labelPadding, b?.labelPadding, t),
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
side: _lerpSides(a?.side, b?.side, t),
shape: _lerpShapes(a?.shape, b?.shape, t),
labelStyle: TextStyle.lerp(a?.labelStyle, b?.labelStyle, t),
secondaryLabelStyle: TextStyle.lerp(a?.secondaryLabelStyle, b?.secondaryLabelStyle, t),
brightness: t < 0.5 ? a?.brightness ?? Brightness.light : b?.brightness ?? Brightness.light,
elevation: lerpDouble(a?.elevation, b?.elevation, t),
pressElevation: lerpDouble(a?.pressElevation, b?.pressElevation, t),
);
}
// Special case because BorderSide.lerp() doesn't support null arguments.
static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) {
if (a == null && b == null)
return null;
if (a == null)
return BorderSide.lerp(BorderSide(width: 0, color: b!.color.withAlpha(0)), b, t);
if (b == null)
return BorderSide.lerp(BorderSide(width: 0, color: a.color.withAlpha(0)), a, t);
return BorderSide.lerp(a, b, t);
}
// TODO(perclasson): OutlinedBorder needs a lerp method - https://github.com/flutter/flutter/issues/60555.
static OutlinedBorder? _lerpShapes(OutlinedBorder? a, OutlinedBorder? b, double t) {
if (a == null && b == null)
return null;
return ShapeBorder.lerp(a, b, t) as OutlinedBorder?;
}
@override
int get hashCode {
return hashValues(
backgroundColor,
deleteIconColor,
disabledColor,
selectedColor,
secondarySelectedColor,
shadowColor,
selectedShadowColor,
checkmarkColor,
labelPadding,
padding,
side,
shape,
labelStyle,
secondaryLabelStyle,
brightness,
elevation,
pressElevation,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is ChipThemeData
&& other.backgroundColor == backgroundColor
&& other.deleteIconColor == deleteIconColor
&& other.disabledColor == disabledColor
&& other.selectedColor == selectedColor
&& other.secondarySelectedColor == secondarySelectedColor
&& other.shadowColor == shadowColor
&& other.selectedShadowColor == selectedShadowColor
&& other.checkmarkColor == checkmarkColor
&& other.labelPadding == labelPadding
&& other.padding == padding
&& other.side == side
&& other.shape == shape
&& other.labelStyle == labelStyle
&& other.secondaryLabelStyle == secondaryLabelStyle
&& other.brightness == brightness
&& other.elevation == elevation
&& other.pressElevation == pressElevation;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
properties.add(ColorProperty('deleteIconColor', deleteIconColor, defaultValue: null));
properties.add(ColorProperty('disabledColor', disabledColor, defaultValue: null));
properties.add(ColorProperty('selectedColor', selectedColor, defaultValue: null));
properties.add(ColorProperty('secondarySelectedColor', secondarySelectedColor, defaultValue: null));
properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null));
properties.add(ColorProperty('selectedShadowColor', selectedShadowColor, defaultValue: null));
properties.add(ColorProperty('checkMarkColor', checkmarkColor, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('labelPadding', labelPadding, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide>('side', side, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('labelStyle', labelStyle, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('secondaryLabelStyle', secondaryLabelStyle, defaultValue: null));
properties.add(EnumProperty<Brightness>('brightness', brightness, defaultValue: null));
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
properties.add(DoubleProperty('pressElevation', pressElevation, defaultValue: null));
}
}