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';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'tooltip.dart';
......@@ -77,6 +78,15 @@ abstract class ChipAttributes {
///
/// This only has an effect on widgets that respect the [DefaultTextStyle],
/// 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;
/// The [ShapeBorder] to draw around the chip.
......@@ -1400,6 +1410,8 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
Animation<double> enableAnimation;
Animation<double> selectionFade;
final Set<MaterialState> _states = <MaterialState>{};
bool get hasDeleteButton => widget.onDeleted != null;
bool get hasAvatar => widget.avatar != null;
......@@ -1416,6 +1428,8 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
void initState() {
assert(widget.onSelected == null || widget.onPressed == null);
super.initState();
_updateState(MaterialState.disabled, !widget.isEnabled);
_updateState(MaterialState.selected, widget.selected ?? false);
selectController = AnimationController(
duration: _kSelectDuration,
value: widget.selected == true ? 1.0 : 0.0,
......@@ -1486,12 +1500,17 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
super.dispose();
}
void _updateState(MaterialState state, bool value) {
value ? _states.add(state) : _states.remove(state);
}
void _handleTapDown(TapDownDetails details) {
if (!canTap) {
return;
}
setState(() {
_isTapping = true;
_updateState(MaterialState.pressed, true);
});
}
......@@ -1501,6 +1520,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
}
setState(() {
_isTapping = false;
_updateState(MaterialState.pressed, false);
});
}
......@@ -1510,12 +1530,25 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
}
setState(() {
_isTapping = false;
_updateState(MaterialState.pressed, false);
});
// Only one of these can be set, so only one will be called.
widget.onSelected?.call(!widget.selected);
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
/// different animations.
Color getBackgroundColor(ChipThemeData theme) {
......@@ -1535,6 +1568,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
super.didUpdateWidget(oldWidget);
if (oldWidget.isEnabled != widget.isEnabled) {
setState(() {
_updateState(MaterialState.disabled, !widget.isEnabled);
if (widget.isEnabled) {
enableController.forward();
} else {
......@@ -1553,6 +1587,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
}
if (oldWidget.selected != widget.selected) {
setState(() {
_updateState(MaterialState.selected, widget.selected ?? false);
if (widget.selected == true) {
selectController.forward();
} else {
......@@ -1621,7 +1656,13 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
final Color selectedShadowColor = widget.selectedShadowColor ?? chipTheme.selectedShadowColor ?? _defaultShadowColor;
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,
shadowColor: selected ? selectedShadowColor : shadowColor,
animationDuration: pressedAnimationDuration,
......@@ -1631,6 +1672,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
onTap: canTap ? _handleTap : null,
onTapDown: canTap ? _handleTapDown : null,
onTapCancel: canTap ? _handleTapCancel : null,
onHover: canTap ? _handleHover : null,
customBorder: shape,
child: AnimatedBuilder(
animation: Listenable.merge(<Listenable>[selectController, enableController]),
......@@ -1653,7 +1695,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
textAlign: TextAlign.start,
maxLines: 1,
softWrap: false,
style: widget.labelStyle ?? chipTheme.labelStyle,
style: resolvedLabelStyle,
child: widget.label,
),
avatar: AnimatedSwitcher(
......@@ -1684,6 +1726,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
),
),
),
),
);
BoxConstraints constraints;
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
......
......@@ -4,6 +4,8 @@
import 'dart:ui' show window;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
......@@ -47,12 +49,10 @@ IconThemeData getIconData(WidgetTester tester) {
DefaultTextStyle getLabelStyle(WidgetTester tester) {
return tester.widget(
find
.descendant(
find.descendant(
of: find.byType(RawChip),
matching: find.byType(DefaultTextStyle),
)
.last,
).last,
);
}
......@@ -1737,4 +1737,90 @@ void main() {
);
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 @@
import 'dart:ui' show window;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -338,4 +339,98 @@ void main() {
expect(lerpBNull75.elevation, 0.25);
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