Skip to content

Commit

Permalink
[Material] Add support for hovered, pressed, focused, and selected te…
Browse files Browse the repository at this point in the history
…xt color on Chips. (#37259)

* Chip keeps track of state, resolves text color
  • Loading branch information
johnsonmh committed Aug 3, 2019
1 parent 3928f30 commit da8c7a9
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 60 deletions.
155 changes: 99 additions & 56 deletions packages/flutter/lib/src/material/chip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand Down Expand Up @@ -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);
});
}

Expand All @@ -1501,6 +1520,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
}
setState(() {
_isTapping = false;
_updateState(MaterialState.pressed, false);
});
}

Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -1621,65 +1656,73 @@ 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(
elevation: isTapping ? pressElevation : elevation,
shadowColor: selected ? selectedShadowColor : shadowColor,
animationDuration: pressedAnimationDuration,
shape: shape,
clipBehavior: widget.clipBehavior,
child: InkWell(
onTap: canTap ? _handleTap : null,
onTapDown: canTap ? _handleTapDown : null,
onTapCancel: canTap ? _handleTapCancel : null,
customBorder: shape,
child: AnimatedBuilder(
animation: Listenable.merge(<Listenable>[selectController, enableController]),
builder: (BuildContext context, Widget child) {
return Container(
decoration: ShapeDecoration(
shape: shape,
color: getBackgroundColor(chipTheme),
),
child: child,
);
},
child: _wrapWithTooltip(
widget.tooltip,
widget.onPressed,
_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,
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,
shape: shape,
clipBehavior: widget.clipBehavior,
child: InkWell(
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]),
builder: (BuildContext context, Widget child) {
return Container(
decoration: ShapeDecoration(
shape: shape,
color: getBackgroundColor(chipTheme),
),
deleteIcon: AnimatedSwitcher(
child: _buildDeleteIcon(context, theme, chipTheme),
duration: _kDrawerDuration,
switchInCurve: Curves.fastOutSlowIn,
child: child,
);
},
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,
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
labelPadding: (widget.labelPadding ?? chipTheme.labelPadding).resolve(textDirection),
showAvatar: hasAvatar,
showCheckmark: widget.showCheckmark,
canTapBody: canTap,
value: widget.selected,
checkmarkAnimation: checkmarkAnimation,
enableAnimation: enableAnimation,
avatarDrawerAnimation: avatarDrawerAnimation,
deleteDrawerAnimation: deleteDrawerAnimation,
isEnabled: widget.isEnabled,
avatarBorder: widget.avatarBorder,
),
value: widget.selected,
checkmarkAnimation: checkmarkAnimation,
enableAnimation: enableAnimation,
avatarDrawerAnimation: avatarDrawerAnimation,
deleteDrawerAnimation: deleteDrawerAnimation,
isEnabled: widget.isEnabled,
avatarBorder: widget.avatarBorder,
),
),
),
Expand Down
94 changes: 90 additions & 4 deletions packages/flutter/test/material/chip_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
);
}

Expand Down Expand Up @@ -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();
});
}
Loading

0 comments on commit da8c7a9

Please sign in to comment.