[Material] Support for hovered, focused, and pressed border color on OutlineButton
s (#34872)
* outline border implements material state property
This commit is contained in:
parent
d780c2cff6
commit
3f94159989
@ -87,7 +87,7 @@ 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]
|
||||
/// If [textStyle.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve]
|
||||
/// is used for the following [MaterialState]s:
|
||||
///
|
||||
/// * [MaterialState.pressed].
|
||||
@ -199,6 +199,14 @@ class RawMaterialButton extends StatefulWidget {
|
||||
///
|
||||
/// The button's highlight and splash are clipped to this shape. If the
|
||||
/// button has an elevation, then its drop shadow is defined by this shape.
|
||||
///
|
||||
/// If [shape] is a [MaterialStateProperty<ShapeBorder>], [MaterialStateProperty.resolve]
|
||||
/// is used for the following [MaterialState]s:
|
||||
///
|
||||
/// * [MaterialState.pressed].
|
||||
/// * [MaterialState.hovered].
|
||||
/// * [MaterialState.focused].
|
||||
/// * [MaterialState.disabled].
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// Defines the duration of animated changes for [shape] and [elevation].
|
||||
@ -317,7 +325,8 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color effectiveTextColor = MaterialStateColor.resolveColor(widget.textStyle?.color, _states);
|
||||
final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(widget.textStyle?.color, _states);
|
||||
final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
|
||||
|
||||
final Widget result = Focus(
|
||||
focusNode: widget.focusNode,
|
||||
@ -327,7 +336,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
|
||||
child: Material(
|
||||
elevation: _effectiveElevation,
|
||||
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
|
||||
shape: widget.shape,
|
||||
shape: effectiveShape,
|
||||
color: widget.fillColor,
|
||||
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
|
||||
animationDuration: widget.animationDuration,
|
||||
@ -340,7 +349,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
|
||||
hoverColor: widget.hoverColor,
|
||||
onHover: _handleHoveredChanged,
|
||||
onTap: widget.onPressed,
|
||||
customBorder: widget.shape,
|
||||
customBorder: effectiveShape,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: effectiveTextColor),
|
||||
child: Container(
|
||||
|
@ -481,11 +481,10 @@ class ButtonThemeData extends Diagnosticable {
|
||||
/// 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.
|
||||
/// If [MaterialButton.textColor] is a [MaterialStateProperty<Color>], 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)
|
||||
if (button.textColor is MaterialStateProperty<Color>)
|
||||
return button.textColor;
|
||||
if (button.disabledTextColor != null)
|
||||
return button.disabledTextColor;
|
||||
|
@ -100,8 +100,8 @@ 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.
|
||||
/// If [textColor] is a [MaterialStateProperty<Color>], [disabledTextColor]
|
||||
/// will be ignored.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -117,8 +117,8 @@ class MaterialButton extends StatelessWidget {
|
||||
/// The default value is the theme's disabled color,
|
||||
/// [ThemeData.disabledColor].
|
||||
///
|
||||
/// If [textColor] is a [MaterialStateColor], [disabledTextColor] will be
|
||||
/// ignored.
|
||||
/// If [textColor] is a [MaterialStateProperty<Color>], [disabledTextColor]
|
||||
/// will be ignored.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -62,8 +62,9 @@ enum MaterialState {
|
||||
error,
|
||||
}
|
||||
|
||||
/// Signature for the function that returns a color based on a given set of states.
|
||||
typedef MaterialStateColorResolver = Color Function(Set<MaterialState> states);
|
||||
/// Signature for the function that returns a value of type `T` based on a given
|
||||
/// set of states.
|
||||
typedef MaterialPropertyResolver<T> = T Function(Set<MaterialState> states);
|
||||
|
||||
/// Defines a [Color] whose value depends on a set of [MaterialState]s which
|
||||
/// represent the interactive state of a component.
|
||||
@ -109,7 +110,7 @@ typedef MaterialStateColorResolver = Color Function(Set<MaterialState> states);
|
||||
/// ),
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
abstract class MaterialStateColor extends Color {
|
||||
abstract class MaterialStateColor extends Color implements MaterialStateProperty<Color> {
|
||||
/// Creates a [MaterialStateColor].
|
||||
///
|
||||
/// If you want a `const` [MaterialStateColor], you'll need to extend
|
||||
@ -141,33 +142,24 @@ abstract class MaterialStateColor extends Color {
|
||||
/// {@end-tool}
|
||||
const MaterialStateColor(int defaultValue) : super(defaultValue);
|
||||
|
||||
/// Creates a [MaterialStateColor] from a [MaterialStateColorResolver] callback function.
|
||||
/// Creates a [MaterialStateColor] from a [MaterialPropertyResolver<Color>]
|
||||
/// 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);
|
||||
static MaterialStateColor resolveWith(MaterialPropertyResolver<Color> callback) => _MaterialStateColor(callback);
|
||||
|
||||
/// Returns a [Color] that's to be used when a Material component is in the
|
||||
/// specified state.
|
||||
@override
|
||||
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.
|
||||
/// A [MaterialStateColor] created from a [MaterialPropertyResolver<Color>]
|
||||
/// callback alone.
|
||||
///
|
||||
/// If used as a regular color, the color resolved in the default state will
|
||||
/// be used.
|
||||
@ -176,7 +168,7 @@ abstract class MaterialStateColor extends Color {
|
||||
class _MaterialStateColor extends MaterialStateColor {
|
||||
_MaterialStateColor(this._resolve) : super(_resolve(_defaultStates).value);
|
||||
|
||||
final MaterialStateColorResolver _resolve;
|
||||
final MaterialPropertyResolver<Color> _resolve;
|
||||
|
||||
/// The default state for a Material component, the empty set of interaction states.
|
||||
static const Set<MaterialState> _defaultStates = <MaterialState>{};
|
||||
@ -184,3 +176,46 @@ class _MaterialStateColor extends MaterialStateColor {
|
||||
@override
|
||||
Color resolve(Set<MaterialState> states) => _resolve(states);
|
||||
}
|
||||
|
||||
/// Interface for classes that can return a value of type `T` based on a set of
|
||||
/// [MaterialState]s.
|
||||
///
|
||||
/// For example, [MaterialStateColor] implements `MaterialStateProperty<Color>`
|
||||
/// because it has a `resolve` method that returns a different [Color] depending
|
||||
/// on the given set of [MaterialState]s.
|
||||
abstract class MaterialStateProperty<T> {
|
||||
|
||||
/// Returns a different value of type `T` depending on the given `states`.
|
||||
///
|
||||
/// Some widgets (such as [RawMaterialButton]) keep track of their set of
|
||||
/// [MaterialState]s, and will call `resolve` with the current states at build
|
||||
/// time for specified properties (such as [RawMaterialButton.textStyle]'s color).
|
||||
T resolve(Set<MaterialState> states);
|
||||
|
||||
/// Returns the value resolved in the given set of states if `value` is a
|
||||
/// [MaterialStateProperty], otherwise returns the value itself.
|
||||
///
|
||||
/// This is useful for widgets that have parameters which can optionally be a
|
||||
/// [MaterialStateProperty]. For example, [RaisedButton.textColor] can be a
|
||||
/// [Color] or a [MaterialStateProperty<Color>].
|
||||
static T resolveAs<T>(T value, Set<MaterialState> states) {
|
||||
if (value is MaterialStateProperty<T>) {
|
||||
final MaterialStateProperty<T> property = value;
|
||||
return property.resolve(states);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Convenience method for creating a [MaterialStateProperty] from a
|
||||
/// [MaterialPropertyResolver] function alone.
|
||||
static MaterialStateProperty<T> resolveWith<T>(MaterialPropertyResolver<T> callback) => _MaterialStateProperty<T>(callback);
|
||||
}
|
||||
|
||||
class _MaterialStateProperty<T> implements MaterialStateProperty<T> {
|
||||
_MaterialStateProperty(this._resolve);
|
||||
|
||||
final MaterialPropertyResolver<T> _resolve;
|
||||
|
||||
@override
|
||||
T resolve(Set<MaterialState> states) => _resolve(states);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'button_theme.dart';
|
||||
import 'colors.dart';
|
||||
import 'material_button.dart';
|
||||
import 'material_state.dart';
|
||||
import 'raised_button.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
@ -132,12 +133,16 @@ class OutlineButton extends MaterialButton {
|
||||
///
|
||||
/// By default the border's color does not change when the button
|
||||
/// is pressed.
|
||||
///
|
||||
/// This field is ignored if [borderSide.color] is a [MaterialStateProperty<Color>].
|
||||
final Color highlightedBorderColor;
|
||||
|
||||
/// The outline border's color when the button is not [enabled].
|
||||
///
|
||||
/// By default the outline border's color does not change when the
|
||||
/// button is disabled.
|
||||
///
|
||||
/// This field is ignored if [borderSide.color] is a [MaterialStateProperty<Color>].
|
||||
final Color disabledBorderColor;
|
||||
|
||||
/// Defines the color of the border when the button is enabled but not
|
||||
@ -148,6 +153,10 @@ class OutlineButton extends MaterialButton {
|
||||
///
|
||||
/// If null the default border's style is [BorderStyle.solid], its
|
||||
/// [BorderSide.width] is 1.0, and its color is a light shade of grey.
|
||||
///
|
||||
/// If [borderSide.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve]
|
||||
/// is used in all states and both [highlightedBorderColor] and [disabledBorderColor]
|
||||
/// are ignored.
|
||||
final BorderSide borderSide;
|
||||
|
||||
@override
|
||||
@ -370,18 +379,26 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
|
||||
return colorTween.evaluate(_fillAnimation);
|
||||
}
|
||||
|
||||
Color get _outlineColor {
|
||||
// If outline color is a `MaterialStateProperty`, it will be used in all
|
||||
// states, otherwise we determine the outline color in the current state.
|
||||
if (widget.borderSide?.color is MaterialStateProperty<Color>)
|
||||
return widget.borderSide.color;
|
||||
if (!widget.enabled)
|
||||
return widget.disabledBorderColor;
|
||||
if (_pressed)
|
||||
return widget.highlightedBorderColor;
|
||||
return widget.borderSide?.color;
|
||||
}
|
||||
|
||||
BorderSide _getOutline() {
|
||||
if (widget.borderSide?.style == BorderStyle.none)
|
||||
return widget.borderSide;
|
||||
|
||||
final Color specifiedColor = widget.enabled
|
||||
? (_pressed ? widget.highlightedBorderColor : null) ?? widget.borderSide?.color
|
||||
: widget.disabledBorderColor;
|
||||
|
||||
final Color themeColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.12);
|
||||
|
||||
return BorderSide(
|
||||
color: specifiedColor ?? themeColor,
|
||||
color: _outlineColor ?? themeColor,
|
||||
width: widget.borderSide?.width ?? 1.0,
|
||||
);
|
||||
}
|
||||
@ -433,7 +450,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
|
||||
|
||||
// Render the button's outline border using using the OutlineButton's
|
||||
// border parameters and the button or buttonTheme's shape.
|
||||
class _OutlineBorder extends ShapeBorder {
|
||||
class _OutlineBorder extends ShapeBorder implements MaterialStateProperty<ShapeBorder>{
|
||||
const _OutlineBorder({
|
||||
@required this.shape,
|
||||
@required this.side,
|
||||
@ -512,4 +529,12 @@ class _OutlineBorder extends ShapeBorder {
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(side, shape);
|
||||
|
||||
@override
|
||||
ShapeBorder resolve(Set<MaterialState> states) {
|
||||
return _OutlineBorder(
|
||||
shape: shape,
|
||||
side: side.copyWith(color: MaterialStateProperty.resolveAs<Color>(side.color, states),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -318,6 +318,138 @@ void main() {
|
||||
expect(textColor(), isNot(unusedDisabledTextColor));
|
||||
});
|
||||
|
||||
testWidgets('OutlineButton uses stateful color for border color in different states', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
const Color pressedColor = Color(1);
|
||||
const Color hoverColor = Color(2);
|
||||
const Color focusedColor = Color(3);
|
||||
const Color defaultColor = Color(4);
|
||||
|
||||
Color getBorderColor(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return pressedColor;
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return hoverColor;
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return focusedColor;
|
||||
}
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: OutlineButton(
|
||||
child: const Text('OutlineButton'),
|
||||
onPressed: () {},
|
||||
focusNode: focusNode,
|
||||
borderSide: BorderSide(color: MaterialStateColor.resolveWith(getBorderColor)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder outlineButton = find.byType(OutlineButton);
|
||||
|
||||
// Default, not disabled.
|
||||
expect(outlineButton, paints..path(color: defaultColor));
|
||||
|
||||
// Focused.
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
expect(outlineButton, paints..path(color: focusedColor));
|
||||
|
||||
// Hovered.
|
||||
final Offset center = tester.getCenter(find.byType(OutlineButton));
|
||||
final TestGesture gesture = await tester.createGesture(
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(center);
|
||||
await tester.pumpAndSettle();
|
||||
expect(outlineButton, paints..path(color: hoverColor));
|
||||
|
||||
// Highlighted (pressed).
|
||||
await gesture.down(center);
|
||||
await tester.pumpAndSettle();
|
||||
expect(outlineButton, paints..path(color: pressedColor));
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets('OutlineButton ignores highlightBorderColor if border color is stateful', (WidgetTester tester) async {
|
||||
const Color pressedColor = Color(1);
|
||||
const Color defaultColor = Color(2);
|
||||
const Color ignoredPressedColor = Color(3);
|
||||
|
||||
Color getBorderColor(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return pressedColor;
|
||||
}
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: OutlineButton(
|
||||
child: const Text('OutlineButton'),
|
||||
onPressed: () {},
|
||||
borderSide: BorderSide(color: MaterialStateColor.resolveWith(getBorderColor)),
|
||||
highlightedBorderColor: ignoredPressedColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder outlineButton = find.byType(OutlineButton);
|
||||
|
||||
// Default, not disabled.
|
||||
expect(outlineButton, paints..path(color: defaultColor));
|
||||
|
||||
// Highlighted (pressed).
|
||||
await tester.press(outlineButton);
|
||||
await tester.pumpAndSettle();
|
||||
expect(outlineButton, paints..path(color: pressedColor));
|
||||
});
|
||||
|
||||
testWidgets('OutlineButton ignores disabledBorderColor if border color is stateful', (WidgetTester tester) async {
|
||||
const Color disabledColor = Color(1);
|
||||
const Color defaultColor = Color(2);
|
||||
const Color ignoredDisabledColor = Color(3);
|
||||
|
||||
Color getBorderColor(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
return disabledColor;
|
||||
}
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: OutlineButton(
|
||||
child: const Text('OutlineButton'),
|
||||
onPressed: null,
|
||||
borderSide: BorderSide(color: MaterialStateColor.resolveWith(getBorderColor)),
|
||||
highlightedBorderColor: ignoredDisabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Disabled.
|
||||
expect(find.byType(OutlineButton), paints..path(color: disabledColor));
|
||||
});
|
||||
|
||||
testWidgets('Outline button responds to tap when enabled', (WidgetTester tester) async {
|
||||
int pressedCount = 0;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user