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,7 +1656,13 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1621,7 +1656,13 @@ 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;
final Color resolvedLabelColor = MaterialStateProperty.resolveAs<Color>(effectiveLabelStyle?.color, _states);
final TextStyle resolvedLabelStyle = effectiveLabelStyle?.copyWith(color: resolvedLabelColor);
Widget result = Focus(
onFocusChange: _handleFocus,
child: Material(
elevation: isTapping ? pressElevation : elevation, elevation: isTapping ? pressElevation : elevation,
shadowColor: selected ? selectedShadowColor : shadowColor, shadowColor: selected ? selectedShadowColor : shadowColor,
animationDuration: pressedAnimationDuration, animationDuration: pressedAnimationDuration,
...@@ -1631,6 +1672,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1631,6 +1672,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
onTap: canTap ? _handleTap : null, onTap: canTap ? _handleTap : null,
onTapDown: canTap ? _handleTapDown : null, onTapDown: canTap ? _handleTapDown : null,
onTapCancel: canTap ? _handleTapCancel : null, onTapCancel: canTap ? _handleTapCancel : null,
onHover: canTap ? _handleHover : null,
customBorder: shape, customBorder: shape,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: Listenable.merge(<Listenable>[selectController, enableController]), animation: Listenable.merge(<Listenable>[selectController, enableController]),
...@@ -1653,7 +1695,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1653,7 +1695,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
textAlign: TextAlign.start, textAlign: TextAlign.start,
maxLines: 1, maxLines: 1,
softWrap: false, softWrap: false,
style: widget.labelStyle ?? chipTheme.labelStyle, style: resolvedLabelStyle,
child: widget.label, child: widget.label,
), ),
avatar: AnimatedSwitcher( avatar: AnimatedSwitcher(
...@@ -1684,6 +1726,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1684,6 +1726,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
), ),
), ),
), ),
),
); );
BoxConstraints constraints; BoxConstraints constraints;
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) { switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
......
...@@ -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