Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Material] Add support for hovered, pressed, and focused text color on Buttons. #33090

Merged
merged 36 commits into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
07f0ac5
Support for stateful text colors in buttons
johnsonmh May 17, 2019
d070d3b
Add color and a11y tests for buttons
johnsonmh May 20, 2019
a96ff92
fix NPE
johnsonmh May 20, 2019
edf6e51
Add documentation
johnsonmh May 20, 2019
67b2b9a
update docs
johnsonmh May 21, 2019
8e26cda
remove unneeded test
johnsonmh May 21, 2019
935a074
Fix analyzer errors
johnsonmh May 21, 2019
2710243
Rami and Antrob comments
johnsonmh May 21, 2019
7387b1b
Merge branch 'master' of https://github.com/flutter/flutter into feat…
johnsonmh May 22, 2019
1a2693f
Add non-interactive states
johnsonmh May 24, 2019
ecce241
update doc
johnsonmh May 24, 2019
44097c4
Update docs to describe disabledTextColor behavior
johnsonmh May 24, 2019
a508ef8
Stable check point
johnsonmh May 24, 2019
48e78ff
experimental work on chips, dialogs, and tabs
johnsonmh May 29, 2019
f7f87aa
Revert "experimental work on chips, dialogs, and tabs"
johnsonmh May 29, 2019
765b939
add tests for text color in disabled state
johnsonmh May 29, 2019
3996123
fix analzyer error
johnsonmh May 29, 2019
10c25c7
MaterialStateColor as abstract class
johnsonmh May 29, 2019
cfa2ed0
variable name
johnsonmh May 29, 2019
3b68cbc
variable name
johnsonmh May 29, 2019
eb93a41
update disable logic
johnsonmh May 30, 2019
2da47cd
Revert "update disable logic"
johnsonmh May 30, 2019
7232a87
fix failing test
johnsonmh May 30, 2019
4779f99
Add constructor documentation
johnsonmh May 30, 2019
8f00eef
added semicolon
johnsonmh May 30, 2019
6c66bd0
added example to resolve method
johnsonmh May 30, 2019
83922b6
Merge branch 'master' of https://github.com/flutter/flutter into feat…
johnsonmh May 30, 2019
b5ba17f
Address Hans' comments
johnsonmh May 31, 2019
72e409b
Address Wills comments
johnsonmh Jun 3, 2019
aabce8c
Merge branch 'master' of https://github.com/flutter/flutter into feat…
johnsonmh Jun 3, 2019
781995b
Hans second round comments
johnsonmh Jun 3, 2019
23a775d
Doc typo
johnsonmh Jun 4, 2019
ec84021
Merge branch 'master' of https://github.com/flutter/flutter into feat…
johnsonmh Jun 4, 2019
22a0570
Merge branch 'master' of https://github.com/flutter/flutter into feat…
johnsonmh Jun 5, 2019
5c128e3
fix analyzer issue
johnsonmh Jun 5, 2019
b53b0e5
Merge branch 'master' of https://github.com/flutter/flutter into feat…
johnsonmh Jun 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/flutter/lib/material.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export 'src/material/list_tile.dart';
export 'src/material/material.dart';
export 'src/material/material_button.dart';
export 'src/material/material_localizations.dart';
export 'src/material/material_state.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/outline_button.dart';
export 'src/material/page.dart';
Expand Down
101 changes: 71 additions & 30 deletions packages/flutter/lib/src/material/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/src/material/material_state.dart';
import 'package:flutter/widgets.dart';

import 'button_theme.dart';
Expand Down Expand Up @@ -85,6 +86,14 @@ class RawMaterialButton extends StatefulWidget {

/// Defines the default text style, with [Material.textStyle], for the
/// button's [child].
///
/// If [textStyle.color] is a [MaterialStateColor], [MaterialStateColor.resolveColor]
/// is used for the following [MaterialState]s:
///
/// * [MaterialState.pressed].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
final TextStyle textStyle;

/// The color of the button's [Material].
Expand Down Expand Up @@ -231,61 +240,93 @@ class RawMaterialButton extends StatefulWidget {
}

class _RawMaterialButtonState extends State<RawMaterialButton> {
bool _highlight = false;
bool _focused = false;
bool _hovering = false;
final Set<MaterialState> _states = <MaterialState>{};

bool get _hovered => _states.contains(MaterialState.hovered);
bool get _focused => _states.contains(MaterialState.focused);
bool get _pressed => _states.contains(MaterialState.pressed);
bool get _disabled => _states.contains(MaterialState.disabled);

void _updateState(MaterialState state, bool value) {
value ? _states.add(state) : _states.remove(state);
}

void _handleHighlightChanged(bool value) {
if (_highlight != value) {
if (_pressed != value) {
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
setState(() {
_highlight = value;
_updateState(MaterialState.pressed, value);
if (widget.onHighlightChanged != null) {
widget.onHighlightChanged(value);
}
});
}
}

void _handleHoveredChanged(bool value) {
if (_hovered != value) {
setState(() {
_updateState(MaterialState.hovered, value);
});
}
}

void _handleFocusedChanged(bool value) {
if (_focused != value) {
setState(() {
_updateState(MaterialState.focused, value);
});
}
}

@override
void initState() {
super.initState();
_updateState(MaterialState.disabled, !widget.enabled);
}

@override
void didUpdateWidget(RawMaterialButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (_highlight && !widget.enabled) {
_highlight = false;
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
if (widget.onHighlightChanged != null) {
widget.onHighlightChanged(false);
}
_updateState(MaterialState.disabled, !widget.enabled);
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
// If the button is disabled while a press gesture is currently ongoing,
// InkWell makes a call to handleHighlightChanged. This causes an exception
// because it calls setState in the middle of a build. To preempt this, we
// manually update pressed to false when this situation occurs.
if (_disabled && _pressed) {
_handleHighlightChanged(false);
}
}

double _effectiveElevation() {
if (widget.enabled) {
// These conditionals are in order of precedence, so be careful about
// reorganizing them.
if (_highlight) {
return widget.highlightElevation;
}
if (_hovering) {
return widget.hoverElevation;
}
if (_focused) {
return widget.focusElevation;
}
return widget.elevation;
} else {
double get _effectiveElevation {
// These conditionals are in order of precedence, so be careful about
// reorganizing them.
if (_disabled) {
return widget.disabledElevation;
}
if (_pressed) {
return widget.highlightElevation;
}
if (_hovered) {
return widget.hoverElevation;
}
if (_focused) {
return widget.focusElevation;
}
return widget.elevation;
}

@override
Widget build(BuildContext context) {
final Color effectiveTextColor = MaterialStateColor.resolveColor(widget.textStyle?.color, _states);

final Widget result = Focus(
focusNode: widget.focusNode,
onFocusChange: (bool focused) => setState(() { _focused = focused; }),
onFocusChange: _handleFocusedChanged,
child: ConstrainedBox(
constraints: widget.constraints,
child: Material(
elevation: _effectiveElevation(),
textStyle: widget.textStyle,
elevation: _effectiveElevation,
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
shape: widget.shape,
color: widget.fillColor,
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
Expand All @@ -297,11 +338,11 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
highlightColor: widget.highlightColor,
focusColor: widget.focusColor,
hoverColor: widget.hoverColor,
onHover: (bool hovering) => setState(() => _hovering = hovering),
onHover: _handleHoveredChanged,
onTap: widget.onPressed,
customBorder: widget.shape,
child: IconTheme.merge(
data: IconThemeData(color: widget.textStyle?.color),
data: IconThemeData(color: effectiveTextColor),
child: Container(
padding: widget.padding,
child: Center(
Expand Down
7 changes: 7 additions & 0 deletions packages/flutter/lib/src/material/button_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'colors.dart';
import 'constants.dart';
import 'flat_button.dart';
import 'material_button.dart';
import 'material_state.dart';
import 'outline_button.dart';
import 'raised_button.dart';
import 'theme.dart';
Expand Down Expand Up @@ -479,7 +480,13 @@ class ButtonThemeData extends Diagnosticable {
/// Returns the button's [MaterialButton.disabledColor] if it is non-null.
/// Otherwise the color scheme's [ColorScheme.onSurface] color is returned
/// with its opacity set to 0.30 if [getBrightness] is dark, 0.38 otherwise.
///
/// If [MaterialButton.textColor] is a [MaterialStateColor], it will be used
/// as the `disabledTextColor`. It will be resolved in the
/// [MaterialState.disabled] state.
Color getDisabledTextColor(MaterialButton button) {
if (button.textColor is MaterialStateColor)
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
return button.textColor;
if (button.disabledTextColor != null)
return button.disabledTextColor;
return _getDisabledColor(button);
Expand Down
7 changes: 7 additions & 0 deletions packages/flutter/lib/src/material/material_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ class MaterialButton extends StatelessWidget {
/// The default text color depends on the button theme's text theme,
/// [ButtonThemeData.textTheme].
///
/// If [textColor] is a [MaterialStateColor], [disabledTextColor] will be
/// ignored.
///
/// See also:
///
/// * [disabledTextColor], the text color to use when the button has been
Expand All @@ -114,6 +117,10 @@ class MaterialButton extends StatelessWidget {
/// The default value is the theme's disabled color,
/// [ThemeData.disabledColor].
///
/// If [textColor] is a [MaterialStateColor], [disabledTextColor] will be
/// ignored. Instead, [textColor] resolved in the [MaterialState.disabled] state
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
/// will be used.
///
/// See also:
///
/// * [textColor] - The color to use for this button's text when the button is [enabled].
Expand Down
185 changes: 185 additions & 0 deletions packages/flutter/lib/src/material/material_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' show Color;

/// Interactive states that some of the Material widgets can take on when
/// receiving input from the user.
///
/// States are defined by https://material.io/design/interaction/states.html#usage.
///
/// Some Material widgets track their current state in a `Set<MaterialState>`.
///
/// See also:
/// * [MaterialStateColor], a color that has a `resolve` method that can
/// return a different color depending on the state of the widget that it
/// is used in.
enum MaterialState {
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
/// The state when the user drags their mouse cursor over the given widget.
///
/// See: https://material.io/design/interaction/states.html#hover.
hovered,

/// The state when the user navigates with the keyboard to a given widget.
///
/// This can also sometimes be triggered when a widget is tapped. For example,
/// when a [TextField] is tapped, it becomes [focused].
///
/// See: https://material.io/design/interaction/states.html#focus.
focused,

/// The state when the user is actively pressing down on the given widget.
///
/// See: https://material.io/design/interaction/states.html#pressed.
pressed,

/// The state when this widget is being dragged from one place to another by
/// the user.
///
/// https://material.io/design/interaction/states.html#dragged.
dragged,

/// The state when this item has been selected.
///
/// This applies to things that can be toggled (such as chips and checkboxes)
/// and things that are selected from a set of options (such as tabs and radio buttons).
///
/// See: https://material.io/design/interaction/states.html#selected.
selected,

/// The state when this widget disabled and can not be interacted with.
///
/// Disabled widgets should not respond to hover, focus, press, or drag
/// interactions.
///
/// See: https://material.io/design/interaction/states.html#disabled.
disabled,

/// The state when the widget has entered some form of invalid state.
///
/// See https://material.io/design/interaction/states.html#usage.
error,
}

/// Signature for the function that returns a color based on a given set of states.
typedef MaterialStateColorResolver = Color Function(Set<MaterialState> states);

/// Defines a [Color] whose value depends on changes in the state of a Material
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
/// component, based on a given set of [MaterialState]s.
///
/// This is useful for preserving the accessibility of text in different states
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
/// of a component. For example, in a [FlatButton] with blue text, the text will
/// become more difficult to read when the button is hovered, focused, or pressed,
/// because the contrast ratio between the button and the text will decrease. To
/// solve this, you can use [MaterialStateColor] to make the text darker when the
/// [FlatButton] is hovered, focused, or pressed.
///
/// To use a [MaterialStateColor], you can either:
/// 1. Create a subclass of [MaterialStateColor] and implement the abstract `resolve` method.
/// 2. Use [MaterialStateColor.resolveWith] and pass in a callback that
/// will be used to resolve the color in the given states.
///
/// This should only be used as parameters when they are documented to take
/// [MaterialStateColor], otherwise only the default state will be used.
///
/// {@tool sample}
///
/// This example shows how you could pass a `MaterialStateColor` to `FlatButton.textColor`.
/// Here, the text color will be `Colors.blue[900]` when the button is being
/// pressed, hovered, or focused. Otherwise, the text color will be `Colors.blue[900]`.
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
///
/// ```dart
/// Color getTextColor(Set<MaterialSet> states) {
/// final Set<MaterialState> interactiveStates = <MaterialState>{
johnsonmh marked this conversation as resolved.
Show resolved Hide resolved
/// MaterialState.pressed,
/// MaterialState.hovered,
/// MaterialState.focused,
/// };
/// if (states.any(interactiveStates.contains)) {
/// return Colors.blue[900];
/// }
/// return Colors.blue[600];
/// }
///
/// FlatButton(
/// ...
/// textColor: MaterialStateColor.resolveWith(getTextColor),
/// ),
/// ```
/// {@end-tool}
abstract class MaterialStateColor extends Color {
/// Creates a [MaterialStateColor].
///
/// If you want a `const` [MaterialStateColor], you'll need to extend
/// [MaterialStateColor] and override the [resolve] method. You'll also need
/// to provide a `defaultValue` to the super constructor, so that we can know
/// at compile-time what the value of the default [Color] is.
///
/// {@tool sample}
///
/// In this next example, we see how you can create a `MaterialStateColor` by
/// extending the abstract class and overriding the `resolve` method.
///
/// ```dart
/// class TextColor extends MaterialStateColor {
/// static const int _defaultColor = 0xcafefeed;
/// static const int _pressedColor = 0xdeadbeef;
///
/// const TextColor() : super(_defaultColor);
///
/// @override
/// Color resolve(Set<MaterialState> states) {
/// if (states.contains(MaterialState.pressed)) {
/// return const Color(_pressedColor);
/// }
/// return const Color(_defaultColor);
/// }
/// }
/// ```
/// {@end-tool}
const MaterialStateColor(int defaultValue) : super(defaultValue);

/// Creates a [MaterialStateColor] from a [MaterialStateColorResolver] callback function.
///
/// If used as a regular color, the color resolved in the default state (the
/// empty set of states) will be used.
///
/// The given callback parameter must return a non-null color in the default
/// state.
factory MaterialStateColor.resolveWith(MaterialStateColorResolver callback) => _MaterialStateColor(callback);

/// Returns a [Color] that's to be used when a Material component is in the
/// specified state.
Color resolve(Set<MaterialState> states);

/// Returns the color for the given set of states if `color` is a
/// [MaterialStateColor], otherwise returns the color itself.
///
/// This is useful for widgets that have parameters which can be [Color] or
/// [MaterialStateColor] values.
static Color resolveColor(Color color, Set<MaterialState> states) {
if (color is MaterialStateColor) {
return color.resolve(states);
}
return color;
}
}

/// A [MaterialStateColor] created from a [MaterialStateColorResolver] callback alone.
///
/// If used as a regular color, the color resolved in the default state will
/// be used.
///
/// Used by [MaterialStateColor.resolveWith].
class _MaterialStateColor extends MaterialStateColor {
_MaterialStateColor(this._resolve) : super(_resolve(_defaultStates).value);

final MaterialStateColorResolver _resolve;

/// The default state for a Material component, the empty set of interaction states.
static const Set<MaterialState> _defaultStates = <MaterialState>{};

@override
Color resolve(Set<MaterialState> states) => _resolve(states);
}
Loading