Unverified Commit da8c7a97 authored by MH Johnson's avatar MH Johnson Committed by GitHub

[Material] Add support for hovered, pressed, focused, and selected text color on Chips. (#37259)

* Chip keeps track of state, resolves text color
parent 3928f30c
...@@ -16,6 +16,7 @@ import 'icons.dart'; ...@@ -16,6 +16,7 @@ import 'icons.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
import 'tooltip.dart'; import 'tooltip.dart';
...@@ -77,6 +78,15 @@ abstract class ChipAttributes { ...@@ -77,6 +78,15 @@ abstract class ChipAttributes {
/// ///
/// This only has an effect on widgets that respect the [DefaultTextStyle], /// This only has an effect on widgets that respect the [DefaultTextStyle],
/// such as [Text]. /// such as [Text].
///
/// If [labelStyle.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; TextStyle get labelStyle;
/// The [ShapeBorder] to draw around the chip. /// The [ShapeBorder] to draw around the chip.
...@@ -1400,6 +1410,8 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1400,6 +1410,8 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
Animation<double> enableAnimation; Animation<double> enableAnimation;
Animation<double> selectionFade; Animation<double> selectionFade;
final Set<MaterialState> _states = <MaterialState>{};
bool get hasDeleteButton => widget.onDeleted != null; bool get hasDeleteButton => widget.onDeleted != null;
bool get hasAvatar => widget.avatar != null; bool get hasAvatar => widget.avatar != null;
...@@ -1416,6 +1428,8 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1416,6 +1428,8 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
void initState() { void initState() {
assert(widget.onSelected == null || widget.onPressed == null); assert(widget.onSelected == null || widget.onPressed == null);
super.initState(); super.initState();
_updateState(MaterialState.disabled, !widget.isEnabled);
_updateState(MaterialState.selected, widget.selected ?? false);
selectController = AnimationController( selectController = AnimationController(
duration: _kSelectDuration, duration: _kSelectDuration,
value: widget.selected == true ? 1.0 : 0.0, value: widget.selected == true ? 1.0 : 0.0,
...@@ -1486,12 +1500,17 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1486,12 +1500,17 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
super.dispose(); super.dispose();
} }
void _updateState(MaterialState state, bool value) {
value ? _states.add(state) : _states.remove(state);
}
void _handleTapDown(TapDownDetails details) { void _handleTapDown(TapDownDetails details) {
if (!canTap) { if (!canTap) {
return; return;
} }
setState(() { setState(() {
_isTapping = true; _isTapping = true;
_updateState(MaterialState.pressed, true);
}); });
} }
...@@ -1501,6 +1520,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1501,6 +1520,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
} }
setState(() { setState(() {
_isTapping = false; _isTapping = false;
_updateState(MaterialState.pressed, false);
}); });
} }
...@@ -1510,12 +1530,25 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1510,12 +1530,25 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
} }
setState(() { setState(() {
_isTapping = false; _isTapping = false;
_updateState(MaterialState.pressed, false);
}); });
// Only one of these can be set, so only one will be called. // Only one of these can be set, so only one will be called.
widget.onSelected?.call(!widget.selected); widget.onSelected?.call(!widget.selected);
widget.onPressed?.call(); widget.onPressed?.call();
} }
void _handleFocus(bool isFocused) {
setState(() {
_updateState(MaterialState.focused, isFocused);
});
}
void _handleHover(bool isHovered) {
setState(() {
_updateState(MaterialState.hovered, isHovered);
});
}
/// Picks between three different colors, depending upon the state of two /// Picks between three different colors, depending upon the state of two
/// different animations. /// different animations.
Color getBackgroundColor(ChipThemeData theme) { Color getBackgroundColor(ChipThemeData theme) {
...@@ -1535,6 +1568,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1535,6 +1568,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.isEnabled != widget.isEnabled) { if (oldWidget.isEnabled != widget.isEnabled) {
setState(() { setState(() {
_updateState(MaterialState.disabled, !widget.isEnabled);
if (widget.isEnabled) { if (widget.isEnabled) {
enableController.forward(); enableController.forward();
} else { } else {
...@@ -1553,6 +1587,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1553,6 +1587,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
} }
if (oldWidget.selected != widget.selected) { if (oldWidget.selected != widget.selected) {
setState(() { setState(() {
_updateState(MaterialState.selected, widget.selected ?? false);
if (widget.selected == true) { if (widget.selected == true) {
selectController.forward(); selectController.forward();
} else { } else {
...@@ -1621,65 +1656,73 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1621,65 +1656,73 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
final Color selectedShadowColor = widget.selectedShadowColor ?? chipTheme.selectedShadowColor ?? _defaultShadowColor; final Color selectedShadowColor = widget.selectedShadowColor ?? chipTheme.selectedShadowColor ?? _defaultShadowColor;
final bool selected = widget.selected ?? false; final bool selected = widget.selected ?? false;
Widget result = Material( final TextStyle effectiveLabelStyle = widget.labelStyle ?? chipTheme.labelStyle;
elevation: isTapping ? pressElevation : elevation, final Color resolvedLabelColor = MaterialStateProperty.resolveAs<Color>(effectiveLabelStyle?.color, _states);
shadowColor: selected ? selectedShadowColor : shadowColor, final TextStyle resolvedLabelStyle = effectiveLabelStyle?.copyWith(color: resolvedLabelColor);
animationDuration: pressedAnimationDuration,
shape: shape, Widget result = Focus(
clipBehavior: widget.clipBehavior, onFocusChange: _handleFocus,
child: InkWell( child: Material(
onTap: canTap ? _handleTap : null, elevation: isTapping ? pressElevation : elevation,
onTapDown: canTap ? _handleTapDown : null, shadowColor: selected ? selectedShadowColor : shadowColor,
onTapCancel: canTap ? _handleTapCancel : null, animationDuration: pressedAnimationDuration,
customBorder: shape, shape: shape,
child: AnimatedBuilder( clipBehavior: widget.clipBehavior,
animation: Listenable.merge(<Listenable>[selectController, enableController]), child: InkWell(
builder: (BuildContext context, Widget child) { onTap: canTap ? _handleTap : null,
return Container( onTapDown: canTap ? _handleTapDown : null,
decoration: ShapeDecoration( onTapCancel: canTap ? _handleTapCancel : null,
shape: shape, onHover: canTap ? _handleHover : null,
color: getBackgroundColor(chipTheme), customBorder: shape,
), child: AnimatedBuilder(
child: child, animation: Listenable.merge(<Listenable>[selectController, enableController]),
); builder: (BuildContext context, Widget child) {
}, return Container(
child: _wrapWithTooltip( decoration: ShapeDecoration(
widget.tooltip, shape: shape,
widget.onPressed, color: getBackgroundColor(chipTheme),
_ChipRenderWidget(
theme: _ChipRenderTheme(
label: DefaultTextStyle(
overflow: TextOverflow.fade,
textAlign: TextAlign.start,
maxLines: 1,
softWrap: false,
style: widget.labelStyle ?? chipTheme.labelStyle,
child: widget.label,
),
avatar: AnimatedSwitcher(
child: widget.avatar,
duration: _kDrawerDuration,
switchInCurve: Curves.fastOutSlowIn,
), ),
deleteIcon: AnimatedSwitcher( child: child,
child: _buildDeleteIcon(context, theme, chipTheme), );
duration: _kDrawerDuration, },
switchInCurve: Curves.fastOutSlowIn, child: _wrapWithTooltip(
widget.tooltip,
widget.onPressed,
_ChipRenderWidget(
theme: _ChipRenderTheme(
label: DefaultTextStyle(
overflow: TextOverflow.fade,
textAlign: TextAlign.start,
maxLines: 1,
softWrap: false,
style: resolvedLabelStyle,
child: widget.label,
),
avatar: AnimatedSwitcher(
child: widget.avatar,
duration: _kDrawerDuration,
switchInCurve: Curves.fastOutSlowIn,
),
deleteIcon: AnimatedSwitcher(
child: _buildDeleteIcon(context, theme, chipTheme),
duration: _kDrawerDuration,
switchInCurve: Curves.fastOutSlowIn,
),
brightness: chipTheme.brightness,
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
labelPadding: (widget.labelPadding ?? chipTheme.labelPadding).resolve(textDirection),
showAvatar: hasAvatar,
showCheckmark: widget.showCheckmark,
canTapBody: canTap,
), ),
brightness: chipTheme.brightness, value: widget.selected,
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection), checkmarkAnimation: checkmarkAnimation,
labelPadding: (widget.labelPadding ?? chipTheme.labelPadding).resolve(textDirection), enableAnimation: enableAnimation,
showAvatar: hasAvatar, avatarDrawerAnimation: avatarDrawerAnimation,
showCheckmark: widget.showCheckmark, deleteDrawerAnimation: deleteDrawerAnimation,
canTapBody: canTap, isEnabled: widget.isEnabled,
avatarBorder: widget.avatarBorder,
), ),
value: widget.selected,
checkmarkAnimation: checkmarkAnimation,
enableAnimation: enableAnimation,
avatarDrawerAnimation: avatarDrawerAnimation,
deleteDrawerAnimation: deleteDrawerAnimation,
isEnabled: widget.isEnabled,
avatarBorder: widget.avatarBorder,
), ),
), ),
), ),
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:ui' show window; import 'dart:ui' show window;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -47,12 +49,10 @@ IconThemeData getIconData(WidgetTester tester) { ...@@ -47,12 +49,10 @@ IconThemeData getIconData(WidgetTester tester) {
DefaultTextStyle getLabelStyle(WidgetTester tester) { DefaultTextStyle getLabelStyle(WidgetTester tester) {
return tester.widget( return tester.widget(
find find.descendant(
.descendant(
of: find.byType(RawChip), of: find.byType(RawChip),
matching: find.byType(DefaultTextStyle), matching: find.byType(DefaultTextStyle),
) ).last,
.last,
); );
} }
...@@ -1737,4 +1737,90 @@ void main() { ...@@ -1737,4 +1737,90 @@ void main() {
); );
expect(find.byType(InkWell), findsOneWidget); expect(find.byType(InkWell), findsOneWidget);
}); });
testWidgets('Chip uses stateful color for text 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);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return disabledColor;
if (states.contains(MaterialState.pressed))
return pressedColor;
if (states.contains(MaterialState.hovered))
return hoverColor;
if (states.contains(MaterialState.focused))
return focusedColor;
if (states.contains(MaterialState.selected))
return selectedColor;
return defaultColor;
}
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,
labelStyle: TextStyle(color: MaterialStateColor.resolveWith(getTextColor)),
),
),
),
);
}
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('Chip')).text.style.color;
}
// Default, not disabled.
await tester.pumpWidget(chipWidget());
expect(textColor(), equals(defaultColor));
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(textColor(), selectedColor);
// Focused.
final FocusNode chipFocusNode = focusNode.children.first;
chipFocusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), 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(textColor(), hoverColor);
// Pressed.
await gesture.down(center);
await tester.pumpAndSettle();
expect(textColor(), pressedColor);
// Disabled.
await tester.pumpWidget(chipWidget(enabled: false));
await tester.pumpAndSettle();
expect(textColor(), disabledColor);
// Teardown.
await gesture.removePointer();
});
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui' show window; import 'dart:ui' show window;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -338,4 +339,98 @@ void main() { ...@@ -338,4 +339,98 @@ void main() {
expect(lerpBNull75.elevation, 0.25); expect(lerpBNull75.elevation, 0.25);
expect(lerpBNull75.pressElevation, 1.0); expect(lerpBNull75.pressElevation, 1.0);
}); });
testWidgets('Chip uses stateful color from chip theme', (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);
Color getTextColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return disabledColor;
if (states.contains(MaterialState.pressed))
return pressedColor;
if (states.contains(MaterialState.hovered))
return hoverColor;
if (states.contains(MaterialState.focused))
return focusedColor;
if (states.contains(MaterialState.selected))
return selectedColor;
return defaultColor;
}
final TextStyle labelStyle = TextStyle(
color: MaterialStateColor.resolveWith(getTextColor),
);
Widget chipWidget({ bool enabled = true, bool selected = false }) {
return MaterialApp(
theme: ThemeData(
chipTheme: ThemeData.light().chipTheme.copyWith(
labelStyle: labelStyle,
secondaryLabelStyle: labelStyle,
),
),
home: Scaffold(
body: Focus(
focusNode: focusNode,
child: ChoiceChip(
label: const Text('Chip'),
selected: selected,
onSelected: enabled ? (_) {} : null,
),
),
),
);
}
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('Chip')).text.style.color;
}
// Default, not disabled.
await tester.pumpWidget(chipWidget());
expect(textColor(), equals(defaultColor));
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(textColor(), selectedColor);
// Focused.
final FocusNode chipFocusNode = focusNode.children.first;
chipFocusNode.requestFocus();
await tester.pumpAndSettle();
expect(textColor(), 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(textColor(), hoverColor);
// Pressed.
await gesture.down(center);
await tester.pumpAndSettle();
expect(textColor(), pressedColor);
// Disabled.
await tester.pumpWidget(chipWidget(enabled: false));
await tester.pumpAndSettle();
expect(textColor(), disabledColor);
// Teardown.
await gesture.removePointer();
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment