Add side property to Chips, and resolve it and the state of Chips to be MaterialState aware (#68596)

This commit is contained in:
Per Classon 2020-10-28 19:12:09 +01:00 committed by GitHub
parent f03caeafa7
commit 92d9630eaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 595 additions and 31 deletions

View file

@ -94,10 +94,39 @@ abstract class ChipAttributes {
/// * [MaterialState.pressed].
TextStyle? get labelStyle;
/// The [ShapeBorder] to draw around the chip.
/// The color and weight of the chip's outline.
///
/// Defaults to the shape in the ambient [ChipThemeData].
ShapeBorder? get shape;
/// 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.widgets.Clip}
///
@ -576,6 +605,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
this.deleteIconColor,
this.useDeleteButtonTooltip = true,
this.deleteButtonTooltipMessage,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
@ -602,7 +632,9 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
@override
final EdgeInsetsGeometry? labelPadding;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
@ -646,6 +678,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
useDeleteButtonTooltip: useDeleteButtonTooltip,
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
tapEnabled: false,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
@ -742,6 +775,7 @@ class InputChip extends StatelessWidget
this.disabledColor,
this.selectedColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
@ -801,7 +835,9 @@ class InputChip extends StatelessWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
@ -850,6 +886,7 @@ class InputChip extends StatelessWidget
disabledColor: disabledColor,
selectedColor: selectedColor,
tooltip: tooltip,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
@ -945,6 +982,7 @@ class ChoiceChip extends StatelessWidget
this.selectedColor,
this.disabledColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
@ -986,7 +1024,9 @@ class ChoiceChip extends StatelessWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
@ -1028,6 +1068,7 @@ class ChoiceChip extends StatelessWidget
showCheckmark: false,
onDeleted: null,
tooltip: tooltip,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
@ -1156,6 +1197,7 @@ class FilterChip extends StatelessWidget
this.disabledColor,
this.selectedColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
@ -1199,7 +1241,9 @@ class FilterChip extends StatelessWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
@ -1242,6 +1286,7 @@ class FilterChip extends StatelessWidget
pressElevation: pressElevation,
selected: selected,
tooltip: tooltip,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
@ -1325,6 +1370,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
required this.onPressed,
this.pressElevation,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
@ -1362,7 +1408,9 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
@ -1393,6 +1441,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
tooltip: tooltip,
labelStyle: labelStyle,
backgroundColor: backgroundColor,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
@ -1477,6 +1526,7 @@ class RawChip extends StatefulWidget
this.disabledColor,
this.selectedColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
@ -1535,7 +1585,9 @@ class RawChip extends StatefulWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
@ -1731,6 +1783,15 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
});
}
OutlinedBorder _getShape(ChipThemeData theme) {
final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, _states)
?? MaterialStateProperty.resolveAs<BorderSide?>(theme.side, _states);
final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, _states)
?? MaterialStateProperty.resolveAs<OutlinedBorder?>(theme.shape, _states)
?? const StadiumBorder();
return resolvedShape.copyWith(side: resolvedSide);
}
/// Picks between three different colors, depending upon the state of two
/// different animations.
Color? getBackgroundColor(ChipThemeData theme) {
@ -1860,7 +1921,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
final ThemeData theme = Theme.of(context)!;
final ChipThemeData chipTheme = ChipTheme.of(context);
final TextDirection? textDirection = Directionality.maybeOf(context);
final ShapeBorder shape = widget.shape ?? chipTheme.shape;
final OutlinedBorder resolvedShape = _getShape(chipTheme);
final double elevation = widget.elevation ?? chipTheme.elevation ?? _defaultElevation;
final double pressElevation = widget.pressElevation ?? chipTheme.pressElevation ?? _defaultPressElevation;
final Color shadowColor = widget.shadowColor ?? chipTheme.shadowColor ?? _defaultShadowColor;
@ -1869,7 +1930,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
final bool showCheckmark = widget.showCheckmark ?? chipTheme.showCheckmark ?? true;
final TextStyle effectiveLabelStyle = widget.labelStyle ?? chipTheme.labelStyle;
final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, _states);
final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, _states);
final TextStyle resolvedLabelStyle = effectiveLabelStyle.copyWith(color: resolvedLabelColor);
final EdgeInsetsGeometry labelPadding = widget.labelPadding ?? chipTheme.labelPadding ?? _defaultLabelPadding;
@ -1877,7 +1938,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
elevation: isTapping ? pressElevation : elevation,
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
animationDuration: pressedAnimationDuration,
shape: shape,
shape: resolvedShape,
clipBehavior: widget.clipBehavior,
child: InkWell(
onFocusChange: _handleFocus,
@ -1893,13 +1954,13 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
context,
deleteIconKey,
),
customBorder: shape,
customBorder: resolvedShape,
child: AnimatedBuilder(
animation: Listenable.merge(<Listenable>[selectController, enableController]),
builder: (BuildContext context, Widget? child) {
return Container(
decoration: ShapeDecoration(
shape: shape,
shape: resolvedShape,
color: getBackgroundColor(chipTheme),
),
child: child,

View file

@ -187,7 +187,8 @@ class ChipThemeData with Diagnosticable {
this.checkmarkColor,
this.labelPadding,
required this.padding,
required this.shape,
this.side,
this.shape,
required this.labelStyle,
required this.secondaryLabelStyle,
required this.brightness,
@ -198,7 +199,6 @@ class ChipThemeData with Diagnosticable {
assert(selectedColor != null),
assert(secondarySelectedColor != null),
assert(padding != null),
assert(shape != null),
assert(labelStyle != null),
assert(secondaryLabelStyle != null),
assert(brightness != null);
@ -244,7 +244,6 @@ class ChipThemeData with Diagnosticable {
const int disabledAlpha = 0x0c; // 38% * 12% = 5%
const int selectAlpha = 0x3d; // 12% + 12% = 24%
const int textLabelAlpha = 0xde; // 87%
const ShapeBorder shape = StadiumBorder();
const EdgeInsetsGeometry padding = EdgeInsets.all(4.0);
primaryColor = primaryColor ?? (brightness == Brightness.light ? Colors.black : Colors.white);
@ -265,7 +264,6 @@ class ChipThemeData with Diagnosticable {
selectedColor: selectedColor,
secondarySelectedColor: secondarySelectedColor,
padding: padding,
shape: shape,
labelStyle: labelStyle,
secondaryLabelStyle: secondaryLabelStyle,
brightness: brightness!,
@ -350,10 +348,37 @@ class ChipThemeData with Diagnosticable {
/// Defaults to 4 logical pixels on all sides.
final EdgeInsetsGeometry padding;
/// The color and weight of the chip's outline.
///
/// If null, the chip defaults to 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].
final BorderSide? side;
/// The border to draw around the chip.
///
/// Defaults to a [StadiumBorder]. Must not be null.
final ShapeBorder shape;
/// If null, the chip defaults to a [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].
final OutlinedBorder? shape;
/// The style to be applied to the chip's label.
///
@ -396,7 +421,8 @@ class ChipThemeData with Diagnosticable {
Color? checkmarkColor,
EdgeInsetsGeometry? labelPadding,
EdgeInsetsGeometry? padding,
ShapeBorder? shape,
BorderSide? side,
OutlinedBorder? shape,
TextStyle? labelStyle,
TextStyle? secondaryLabelStyle,
Brightness? brightness,
@ -414,6 +440,7 @@ class ChipThemeData with Diagnosticable {
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,
@ -443,7 +470,8 @@ class ChipThemeData with Diagnosticable {
checkmarkColor: Color.lerp(a?.checkmarkColor, b?.checkmarkColor, t),
labelPadding: EdgeInsetsGeometry.lerp(a?.labelPadding, b?.labelPadding, t),
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t)!,
shape: ShapeBorder.lerp(a?.shape, b?.shape, 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,
@ -452,6 +480,24 @@ class ChipThemeData with Diagnosticable {
);
}
// 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(
@ -465,6 +511,7 @@ class ChipThemeData with Diagnosticable {
checkmarkColor,
labelPadding,
padding,
side,
shape,
labelStyle,
secondaryLabelStyle,
@ -493,6 +540,7 @@ class ChipThemeData with Diagnosticable {
&& other.checkmarkColor == checkmarkColor
&& other.labelPadding == labelPadding
&& other.padding == padding
&& other.side == side
&& other.shape == shape
&& other.labelStyle == labelStyle
&& other.secondaryLabelStyle == secondaryLabelStyle
@ -520,6 +568,7 @@ class ChipThemeData with Diagnosticable {
properties.add(ColorProperty('checkMarkColor', checkmarkColor, defaultValue: defaultData.checkmarkColor));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('labelPadding', labelPadding, defaultValue: defaultData.labelPadding));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: defaultData.padding));
properties.add(DiagnosticsProperty<BorderSide>('side', side, defaultValue: defaultData.side));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultData.shape));
properties.add(DiagnosticsProperty<TextStyle>('labelStyle', labelStyle, defaultValue: defaultData.labelStyle));
properties.add(DiagnosticsProperty<TextStyle>('secondaryLabelStyle', secondaryLabelStyle, defaultValue: defaultData.secondaryLabelStyle));

View file

@ -21,9 +21,15 @@ import 'package:flutter/rendering.dart';
/// * [MaterialStateColor], a [Color] that implements `MaterialStateProperty`
/// which is used in APIs that need to accept either a [Color] or a
/// `MaterialStateProperty<Color>`.
/// * [MaterialStateMouseCursor], a [MouseCursor] that implements `MaterialStateProperty`
/// which is used in APIs that need to accept either a [MouseCursor] or a
/// [MaterialStateProperty<MouseCursor>].
/// * [MaterialStateMouseCursor], a [MouseCursor] that implements
/// `MaterialStateProperty` which is used in APIs that need to accept either
/// a [MouseCursor] or a [MaterialStateProperty<MouseCursor>].
/// * [MaterialStateOutlinedBorder], an [OutlinedBorder] that implements
/// `MaterialStateProperty` which is used in APIs that need to accept either
/// an [OutlinedBorder] or a [MaterialStateProperty<OutlinedBorder>].
/// * [MaterialStateBorderSide], a [BorderSide] that implements
/// `MaterialStateProperty` which is used in APIs that need to accept either
/// a [BorderSide] or a [MaterialStateProperty<BorderSide>].
enum MaterialState {
/// The state when the user drags their mouse cursor over the given widget.
@ -282,6 +288,123 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
String get debugDescription => 'MaterialStateMouseCursor($name)';
}
/// Defines a [BorderSide] whose value depends on a set of [MaterialState]s
/// which represent the interactive state of a component.
///
/// To use a [MaterialStateBorderSide], you should create a subclass of a
/// [MaterialStateBorderSide] and override the abstract `resolve` method.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This example defines a subclass of [MaterialStateBorderSide], that resolves
/// to a red border side when its widget is selected.
///
/// ```dart preamble
/// class RedSelectedBorderSide extends MaterialStateBorderSide {
/// @override
/// BorderSide resolve(Set<MaterialState> states) {
/// if (states.contains(MaterialState.selected)) {
/// return BorderSide(
/// width: 1,
/// color: Colors.red,
/// );
/// }
/// return null; // Defer to default value on the theme or widget.
/// }
/// }
/// ```
///
/// ```dart
/// bool isSelected = true;
///
/// Widget build(BuildContext context) {
/// return FilterChip(
/// label: Text('Select chip'),
/// selected: isSelected,
/// onSelected: (bool value) {
/// setState(() {
/// isSelected = value;
/// });
/// },
/// side: RedSelectedBorderSide(),
/// );
/// }
/// ```
/// {@end-tool}
///
/// This class should only be used for parameters which are documented to take
/// [MaterialStateBorderSide], otherwise only the default state will be used.
abstract class MaterialStateBorderSide extends BorderSide implements MaterialStateProperty<BorderSide?> {
/// Creates a [MaterialStateBorderSide].
const MaterialStateBorderSide();
/// Returns a [BorderSide] that's to be used when a Material component is
/// in the specified state. Return null to defer to the default value of the
/// widget or theme.
@override
BorderSide? resolve(Set<MaterialState> states);
}
/// Defines an [OutlinedBorder] whose value depends on a set of [MaterialState]s
/// which represent the interactive state of a component.
///
/// To use a [MaterialStateOutlinedBorder], you should create a subclass of an
/// [OutlinedBorder] and implement [MaterialStateOutlinedBorder]'s abstract
/// `resolve` method.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This example defines a subclass of [RoundedRectangleBorder] and an
/// implementation of [MaterialStateOutlinedBorder], that resolves to
/// [RoundedRectangleBorder] when its widget is selected.
///
/// ```dart preamble
/// class SelectedBorder extends RoundedRectangleBorder implements MaterialStateOutlinedBorder {
/// @override
/// OutlinedBorder resolve(Set<MaterialState> states) {
/// if (states.contains(MaterialState.selected)) {
/// return RoundedRectangleBorder();
/// }
/// return null; // Defer to default value on the theme or widget.
/// }
/// }
/// ```
///
/// ```dart
/// bool isSelected = true;
///
/// Widget build(BuildContext context) {
/// return FilterChip(
/// label: Text('Select chip'),
/// selected: isSelected,
/// onSelected: (bool value) {
/// setState(() {
/// isSelected = value;
/// });
/// },
/// shape: SelectedBorder(),
/// );
/// }
/// ```
/// {@end-tool}
///
/// This class should only be used for parameters which are documented to take
/// [MaterialStateOutlinedBorder], otherwise only the default state will be used.
///
/// See also:
///
/// * [ShapeBorder] the base class for shape outlines.
abstract class MaterialStateOutlinedBorder extends OutlinedBorder implements MaterialStateProperty<OutlinedBorder?> {
/// Creates a [MaterialStateOutlinedBorder].
const MaterialStateOutlinedBorder();
/// Returns an [OutlinedBorder] that's to be used when a Material component is
/// in the specified state. Return null to defer to the default value of the
/// widget or theme.
@override
OutlinedBorder? resolve(Set<MaterialState> states);
}
/// Interface for classes that [resolve] to a value of type `T` based
/// on a widget's interactive "state", which is defined as a set
/// of [MaterialState]s.

View file

@ -2382,6 +2382,216 @@ void main() {
await gesture.removePointer();
});
testWidgets('Chip uses stateful border side color in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
const Color selectedColor = Color(0x00000005);
const Color disabledColor = Color(0x00000006);
BorderSide getBorderSide(Set<MaterialState> states) {
Color sideColor = defaultColor;
if (states.contains(MaterialState.disabled))
sideColor = disabledColor;
else if (states.contains(MaterialState.pressed))
sideColor = pressedColor;
else if (states.contains(MaterialState.hovered))
sideColor = hoverColor;
else if (states.contains(MaterialState.focused))
sideColor = focusedColor;
else if (states.contains(MaterialState.selected))
sideColor = selectedColor;
return BorderSide(color: sideColor, width: 1);
}
Widget chipWidget({ bool enabled = true, bool selected = false }) {
return MaterialApp(
home: Scaffold(
body: Focus(
focusNode: focusNode,
child: ChoiceChip(
label: const Text('Chip'),
selected: selected,
onSelected: enabled ? (_) {} : null,
side: _MaterialStateBorderSide(getBorderSide),
),
),
),
);
}
// Default, not disabled.
await tester.pumpWidget(chipWidget());
expect(find.byType(RawChip), paints..rrect(color: defaultColor));
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(find.byType(RawChip), paints..rrect(color: selectedColor));
// Focused.
final FocusNode chipFocusNode = focusNode.children.first;
chipFocusNode.requestFocus();
await tester.pumpAndSettle();
expect(find.byType(RawChip), paints..rrect(color: focusedColor));
// Hovered.
final Offset center = tester.getCenter(find.byType(ChoiceChip));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(find.byType(RawChip), paints..rrect(color: hoverColor));
// Pressed.
await gesture.down(center);
await tester.pumpAndSettle();
expect(find.byType(RawChip), paints..rrect(color: pressedColor));
// Disabled.
await tester.pumpWidget(chipWidget(enabled: false));
await tester.pumpAndSettle();
expect(find.byType(RawChip), paints..rrect(color: disabledColor));
// Teardown.
await gesture.removePointer();
});
testWidgets('Chip uses stateful shape in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
OutlinedBorder? getShape(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return const BeveledRectangleBorder();
else if (states.contains(MaterialState.pressed))
return const CircleBorder();
else if (states.contains(MaterialState.hovered))
return const ContinuousRectangleBorder();
else if (states.contains(MaterialState.focused))
return const RoundedRectangleBorder();
else if (states.contains(MaterialState.selected))
return const BeveledRectangleBorder();
return null;
}
Widget chipWidget({ bool enabled = true, bool selected = false }) {
return MaterialApp(
home: Scaffold(
body: Focus(
focusNode: focusNode,
child: ChoiceChip(
selected: selected,
label: const Text('Chip'),
shape: _MaterialStateOutlinedBorder(getShape),
onSelected: enabled ? (_) {} : null,
),
),
),
);
}
// Default, not disabled. Defers to default shape.
await tester.pumpWidget(chipWidget());
expect(getMaterial(tester).shape, isA<StadiumBorder>());
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(getMaterial(tester).shape, isA<BeveledRectangleBorder>());
// Focused.
final FocusNode chipFocusNode = focusNode.children.first;
chipFocusNode.requestFocus();
await tester.pumpAndSettle();
expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());
// Hovered.
final Offset center = tester.getCenter(find.byType(ChoiceChip));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(getMaterial(tester).shape, isA<ContinuousRectangleBorder>());
// Pressed.
await gesture.down(center);
await tester.pumpAndSettle();
expect(getMaterial(tester).shape, isA<CircleBorder>());
// Disabled.
await tester.pumpWidget(chipWidget(enabled: false));
await tester.pumpAndSettle();
expect(getMaterial(tester).shape, isA<BeveledRectangleBorder>());
// Teardown.
await gesture.removePointer();
});
testWidgets('Chip defers to theme, if shape and side resolves to null', (WidgetTester tester) async {
const OutlinedBorder themeShape = StadiumBorder();
const OutlinedBorder selectedShape = RoundedRectangleBorder();
const BorderSide themeBorderSide = BorderSide(color: Color(0x00000001), width: 1);
const BorderSide selectedBorderSide = BorderSide(color: Color(0x00000002), width: 1);
OutlinedBorder? getShape(Set<MaterialState> states) {
if (states.contains(MaterialState.selected))
return selectedShape;
return null;
}
BorderSide? getBorderSide(Set<MaterialState> states) {
if (states.contains(MaterialState.selected))
return selectedBorderSide;
return null;
}
Widget chipWidget({ bool enabled = true, bool selected = false }) {
return MaterialApp(
theme: ThemeData(
chipTheme: ThemeData.light().chipTheme.copyWith(
shape: themeShape,
side: themeBorderSide,
),
),
home: Scaffold(
body: ChoiceChip(
selected: selected,
label: const Text('Chip'),
shape: _MaterialStateOutlinedBorder(getShape),
side: _MaterialStateBorderSide(getBorderSide),
onSelected: enabled ? (_) {} : null,
),
),
);
}
// Default, not disabled. Defer to theme.
await tester.pumpWidget(chipWidget());
expect(getMaterial(tester).shape, isA<StadiumBorder>());
expect(find.byType(RawChip), paints..rrect(color: themeBorderSide.color));
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());
expect(find.byType(RawChip), paints..drrect(color: selectedBorderSide.color));
});
testWidgets('loses focus when disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'InputChip');
await tester.pumpWidget(
@ -2707,3 +2917,21 @@ void main() {
await tapGesture.up();
});
}
class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder {
const _MaterialStateOutlinedBorder(this.resolver);
final MaterialPropertyResolver<OutlinedBorder?> resolver;
@override
OutlinedBorder? resolve(Set<MaterialState> states) => resolver(states);
}
class _MaterialStateBorderSide extends MaterialStateBorderSide {
const _MaterialStateBorderSide(this.resolver);
final MaterialPropertyResolver<BorderSide?> resolver;
@override
BorderSide? resolve(Set<MaterialState> states) => resolver(states);
}

View file

@ -184,7 +184,8 @@ void main() {
expect(lightTheme.secondarySelectedColor, equals(customColor1.withAlpha(0x3d)));
expect(lightTheme.labelPadding, isNull);
expect(lightTheme.padding, equals(const EdgeInsets.all(4.0)));
expect(lightTheme.shape, isA<StadiumBorder>());
expect(lightTheme.side, isNull);
expect(lightTheme.shape, isNull);
expect(lightTheme.labelStyle.color, equals(Colors.black.withAlpha(0xde)));
expect(lightTheme.secondaryLabelStyle.color, equals(customColor1.withAlpha(0xde)));
expect(lightTheme.brightness, equals(Brightness.light));
@ -202,7 +203,8 @@ void main() {
expect(darkTheme.secondarySelectedColor, equals(customColor1.withAlpha(0x3d)));
expect(darkTheme.labelPadding, isNull);
expect(darkTheme.padding, equals(const EdgeInsets.all(4.0)));
expect(darkTheme.shape, isA<StadiumBorder>());
expect(darkTheme.side, isNull);
expect(darkTheme.shape, isNull);
expect(darkTheme.labelStyle.color, equals(Colors.white.withAlpha(0xde)));
expect(darkTheme.secondaryLabelStyle.color, equals(customColor1.withAlpha(0xde)));
expect(darkTheme.brightness, equals(Brightness.dark));
@ -220,7 +222,8 @@ void main() {
expect(customTheme.secondarySelectedColor, equals(customColor2.withAlpha(0x3d)));
expect(customTheme.labelPadding, isNull);
expect(customTheme.padding, equals(const EdgeInsets.all(4.0)));
expect(customTheme.shape, isA<StadiumBorder>());
expect(customTheme.side, isNull);
expect(customTheme.shape, isNull);
expect(customTheme.labelStyle.color, equals(customColor1.withAlpha(0xde)));
expect(customTheme.secondaryLabelStyle.color, equals(customColor2.withAlpha(0xde)));
expect(customTheme.brightness, equals(Brightness.light));
@ -234,6 +237,8 @@ void main() {
).copyWith(
elevation: 1.0,
labelPadding: const EdgeInsets.symmetric(horizontal: 8.0),
shape: const StadiumBorder(),
side: const BorderSide(color: Colors.black),
pressElevation: 4.0,
shadowColor: Colors.black,
selectedShadowColor: Colors.black,
@ -246,6 +251,8 @@ void main() {
).copyWith(
padding: const EdgeInsets.all(2.0),
labelPadding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
shape: const BeveledRectangleBorder(),
side: const BorderSide(color: Colors.white),
elevation: 5.0,
pressElevation: 10.0,
shadowColor: Colors.white,
@ -264,7 +271,8 @@ void main() {
expect(lerp.selectedShadowColor, equals(middleGrey));
expect(lerp.labelPadding, equals(const EdgeInsets.all(4.0)));
expect(lerp.padding, equals(const EdgeInsets.all(3.0)));
expect(lerp.shape, isA<StadiumBorder>());
expect(lerp.side!.color, equals(middleGrey));
expect(lerp.shape, isA<BeveledRectangleBorder>());
expect(lerp.labelStyle.color, equals(middleGrey.withAlpha(0xde)));
expect(lerp.secondaryLabelStyle.color, equals(middleGrey.withAlpha(0xde)));
expect(lerp.brightness, equals(Brightness.light));
@ -284,7 +292,8 @@ void main() {
expect(lerpANull25.selectedShadowColor, equals(Colors.white.withAlpha(0x40)));
expect(lerpANull25.labelPadding, equals(const EdgeInsets.only(left: 0.0, top: 2.0, right: 0.0, bottom: 2.0)));
expect(lerpANull25.padding, equals(const EdgeInsets.all(0.5)));
expect(lerpANull25.shape, isA<StadiumBorder>());
expect(lerpANull25.side!.color, equals(Colors.white.withAlpha(0x3f)));
expect(lerpANull25.shape, isA<BeveledRectangleBorder>());
expect(lerpANull25.labelStyle.color, equals(Colors.black.withAlpha(0x38)));
expect(lerpANull25.secondaryLabelStyle.color, equals(Colors.white.withAlpha(0x38)));
expect(lerpANull25.brightness, equals(Brightness.light));
@ -302,7 +311,8 @@ void main() {
expect(lerpANull75.selectedShadowColor, equals(Colors.white.withAlpha(0xbf)));
expect(lerpANull75.labelPadding, equals(const EdgeInsets.only(left: 0.0, top: 6.0, right: 0.0, bottom: 6.0)));
expect(lerpANull75.padding, equals(const EdgeInsets.all(1.5)));
expect(lerpANull75.shape, isA<StadiumBorder>());
expect(lerpANull75.side!.color, equals(Colors.white.withAlpha(0xbf)));
expect(lerpANull75.shape, isA<BeveledRectangleBorder>());
expect(lerpANull75.labelStyle.color, equals(Colors.black.withAlpha(0xa7)));
expect(lerpANull75.secondaryLabelStyle.color, equals(Colors.white.withAlpha(0xa7)));
expect(lerpANull75.brightness, equals(Brightness.light));
@ -320,6 +330,7 @@ void main() {
expect(lerpBNull25.selectedShadowColor, equals(Colors.black.withAlpha(0xbf)));
expect(lerpBNull25.labelPadding, equals(const EdgeInsets.only(left: 6.0, top: 0.0, right: 6.0, bottom: 0.0)));
expect(lerpBNull25.padding, equals(const EdgeInsets.all(3.0)));
expect(lerpBNull25.side!.color, equals(Colors.black.withAlpha(0x3f)));
expect(lerpBNull25.shape, isA<StadiumBorder>());
expect(lerpBNull25.labelStyle.color, equals(Colors.white.withAlpha(0xa7)));
expect(lerpBNull25.secondaryLabelStyle.color, equals(Colors.black.withAlpha(0xa7)));
@ -338,6 +349,7 @@ void main() {
expect(lerpBNull75.selectedShadowColor, equals(Colors.black.withAlpha(0x40)));
expect(lerpBNull75.labelPadding, equals(const EdgeInsets.only(left: 2.0, top: 0.0, right: 2.0, bottom: 0.0)));
expect(lerpBNull75.padding, equals(const EdgeInsets.all(1.0)));
expect(lerpBNull75.side!.color, equals(Colors.black.withAlpha(0xbf)));
expect(lerpBNull75.shape, isA<StadiumBorder>());
expect(lerpBNull75.labelStyle.color, equals(Colors.white.withAlpha(0x38)));
expect(lerpBNull75.secondaryLabelStyle.color, equals(Colors.black.withAlpha(0x38)));
@ -440,4 +452,95 @@ void main() {
// Teardown.
await gesture.removePointer();
});
testWidgets('Chip uses stateful border side from chip theme', (WidgetTester tester) async {
const Color selectedColor = Color(0x00000001);
const Color defaultColor = Color(0x00000002);
BorderSide getBorderSide(Set<MaterialState> states) {
Color color = defaultColor;
if (states.contains(MaterialState.selected))
color = selectedColor;
return BorderSide(color: color, width: 1);
}
Widget chipWidget({ bool selected = false }) {
return MaterialApp(
theme: ThemeData(
chipTheme: ThemeData.light().chipTheme.copyWith(
side: _MaterialStateBorderSide(getBorderSide),
),
),
home: Scaffold(
body: ChoiceChip(
label: const Text('Chip'),
selected: selected,
onSelected: (_) {},
),
),
);
}
// Default.
await tester.pumpWidget(chipWidget());
expect(find.byType(RawChip), paints..rrect(color: defaultColor));
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(find.byType(RawChip), paints..rrect(color: selectedColor));
});
testWidgets('Chip uses stateful shape from chip theme', (WidgetTester tester) async {
OutlinedBorder? getShape(Set<MaterialState> states) {
if (states.contains(MaterialState.selected))
return const RoundedRectangleBorder();
return null;
}
Widget chipWidget({ bool selected = false }) {
return MaterialApp(
theme: ThemeData(
chipTheme: ThemeData.light().chipTheme.copyWith(
shape: _MaterialStateOutlinedBorder(getShape),
),
),
home: Scaffold(
body: ChoiceChip(
label: const Text('Chip'),
selected: selected,
onSelected: (_) {},
),
),
);
}
// Default.
await tester.pumpWidget(chipWidget());
expect(getMaterial(tester).shape, isA<StadiumBorder>());
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());
});
}
class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder {
const _MaterialStateOutlinedBorder(this.resolver);
final MaterialPropertyResolver<OutlinedBorder?> resolver;
@override
OutlinedBorder? resolve(Set<MaterialState> states) => resolver(states);
}
class _MaterialStateBorderSide extends MaterialStateBorderSide {
const _MaterialStateBorderSide(this.resolver);
final MaterialPropertyResolver<BorderSide?> resolver;
@override
BorderSide? resolve(Set<MaterialState> states) => resolver(states);
}