WidgetState
mapping (#146043)
This pull request implements [enhanced enum](https://dart.dev/language/enums#declaring-enhanced-enums) features for the new `WidgetState` enum, in order to improve the developer experience when creating and using `WidgetStateProperty` objects. `WidgetState` now has a `.matchesSet()` method: ```dart // identical to "states.contains(WidgetState.error)" final bool hasError = WidgetState.error.isSatisfiedBy(states); ``` This addition allows for wide variety of `WidgetStateProperty` objects to be constructed in a simple manner. <br><br> ```dart // before final style = MaterialStateTextStyle.resolveWith((states) { if (states.contains(MaterialState.error)) { return TextStyle(color: Colors.red); } else if (states.contains(MaterialState.focused)) { return TextStyle(color: Colors.blue); } return TextStyle(color: Colors.black); }); // after final style = WidgetStateTextStyle.fromMap({ WidgetState.error: TextStyle(color: Colors.red), WidgetState.focused: TextStyle(color: Colors.blue), WidgetState.any: TextStyle(color: Colors.black), // "any" is a static const member, not an enum value }); ``` ```dart // before final color = MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.focused)) { return Colors.blue; } else if (!states.contains(MaterialState.disabled)) { return Colors.black; } return null; }); // after final color = WidgetStateProperty<Color?>.fromMap({ WidgetState.focused: Colors.blue, ~WidgetState.disabled: Colors.black, }); ``` ```dart // before const activeStates = [MaterialState.selected, MaterialState.focused, MaterialState.scrolledUnder]; final color = MaterialStateColor.resolveWith((states) { if (activeStates.any(states.contains)) { if (states.contains(MaterialState.hovered) { return Colors.blueAccent; } return Colors.blue; } return Colors.black; }); // after final active = WidgetState.selected | WidgetState.focused | WidgetState.scrolledUnder; final color = WidgetStateColor.fromMap({ active & WidgetState.hovered: Colors.blueAccent, active: Colors.blue, ~active: Colors.black, }); ``` <br> (fixes #146042, and also fixes #143488)
This commit is contained in:
parent
af0e01c370
commit
db80f4e713
@ -8,6 +8,89 @@ import 'package:flutter/services.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
// late Set<WidgetState> states;
|
||||
|
||||
/// This class allows [WidgetState] enum values to be combined
|
||||
/// using [WidgetStateOperators].
|
||||
///
|
||||
/// A [Map] with [WidgetStatesConstraint] objects as keys can be used
|
||||
/// in the [WidgetStateProperty.fromMap] constructor to resolve to
|
||||
/// one of its values, based on the first key that [isSatisfiedBy]
|
||||
/// the current set of states.
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetStateMap}
|
||||
abstract interface class WidgetStatesConstraint {
|
||||
/// Whether the provided [states] satisfy this object's criteria.
|
||||
///
|
||||
/// If the constraint is a single [WidgetState] object,
|
||||
/// it's satisfied by the set if the set contains the object.
|
||||
///
|
||||
/// The constraint can also be created using one or more operators, for example:
|
||||
///
|
||||
/// {@template flutter.widgets.WidgetStatesConstraint.isSatisfiedBy}
|
||||
/// ```dart
|
||||
/// final WidgetStatesConstraint constraint = WidgetState.focused | WidgetState.hovered;
|
||||
/// ```
|
||||
///
|
||||
/// In the above case, `constraint.isSatisfiedBy(states)` is equivalent to:
|
||||
///
|
||||
/// ```dart
|
||||
/// states.contains(WidgetState.focused) || states.contains(WidgetState.hovered);
|
||||
/// ```
|
||||
/// {@endtemplate}
|
||||
bool isSatisfiedBy(Set<WidgetState> states);
|
||||
}
|
||||
|
||||
// A private class, used in [WidgetStateOperators].
|
||||
class _WidgetStateOperation implements WidgetStatesConstraint {
|
||||
const _WidgetStateOperation(this._isSatisfiedBy);
|
||||
|
||||
final bool Function(Set<WidgetState> states) _isSatisfiedBy;
|
||||
|
||||
@override
|
||||
bool isSatisfiedBy(Set<WidgetState> states) => _isSatisfiedBy(states);
|
||||
}
|
||||
|
||||
/// These operators can be used inside a [WidgetStateMap] to combine states
|
||||
/// and find a match.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetStatesConstraint.isSatisfiedBy}
|
||||
///
|
||||
/// Since enums can't extend other classes, [WidgetState] instead `implements`
|
||||
/// the [WidgetStatesConstraint] interface. This `extension` ensures that
|
||||
/// the operators can be used without being directly inherited.
|
||||
extension WidgetStateOperators on WidgetStatesConstraint {
|
||||
/// Combines two [WidgetStatesConstraint] values using logical "and".
|
||||
WidgetStatesConstraint operator &(WidgetStatesConstraint other) {
|
||||
return _WidgetStateOperation(
|
||||
(Set<WidgetState> states) => isSatisfiedBy(states) && other.isSatisfiedBy(states),
|
||||
);
|
||||
}
|
||||
|
||||
/// Combines two [WidgetStatesConstraint] values using logical "or".
|
||||
WidgetStatesConstraint operator |(WidgetStatesConstraint other) {
|
||||
return _WidgetStateOperation(
|
||||
(Set<WidgetState> states) => isSatisfiedBy(states) || other.isSatisfiedBy(states),
|
||||
);
|
||||
}
|
||||
|
||||
/// Takes a [WidgetStatesConstraint] and applies the logical "not".
|
||||
WidgetStatesConstraint operator ~() {
|
||||
return _WidgetStateOperation(
|
||||
(Set<WidgetState> states) => !isSatisfiedBy(states),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A private class, used to create [WidgetState.any].
|
||||
class _AlwaysMatch implements WidgetStatesConstraint {
|
||||
const _AlwaysMatch();
|
||||
|
||||
@override
|
||||
bool isSatisfiedBy(Set<WidgetState> states) => true;
|
||||
}
|
||||
|
||||
/// Interactive states that some of the widgets can take on when receiving input
|
||||
/// from the user.
|
||||
@ -39,7 +122,7 @@ import 'package:flutter/services.dart';
|
||||
/// `WidgetStateProperty` which is used in APIs that need to accept either
|
||||
/// a [TextStyle] or a [WidgetStateProperty<TextStyle>].
|
||||
/// {@endtemplate}
|
||||
enum WidgetState {
|
||||
enum WidgetState implements WidgetStatesConstraint {
|
||||
/// The state when the user drags their mouse cursor over the given widget.
|
||||
///
|
||||
/// See: https://material.io/design/interaction/states.html#hover.
|
||||
@ -89,7 +172,17 @@ enum WidgetState {
|
||||
/// The state when the widget has entered some form of invalid state.
|
||||
///
|
||||
/// See https://material.io/design/interaction/states.html#usage.
|
||||
error,
|
||||
error;
|
||||
|
||||
/// {@template flutter.widgets.WidgetState.any}
|
||||
/// To prevent a situation where each [WidgetStatesConstraint]
|
||||
/// isn't satisfied by the given set of states, consier adding
|
||||
/// [WidgetState.any] as the final [WidgetStateMap] key.
|
||||
/// {@endtemplate}
|
||||
static const WidgetStatesConstraint any = _AlwaysMatch();
|
||||
|
||||
@override
|
||||
bool isSatisfiedBy(Set<WidgetState> states) => states.contains(this);
|
||||
}
|
||||
|
||||
/// Signature for the function that returns a value of type `T` based on a given
|
||||
@ -112,6 +205,7 @@ typedef WidgetPropertyResolver<T> = T Function(Set<WidgetState> states);
|
||||
/// 1. Create a subclass of [WidgetStateColor] and implement the abstract `resolve` method.
|
||||
/// 2. Use [WidgetStateColor.resolveWith] and pass in a callback that
|
||||
/// will be used to resolve the color in the given states.
|
||||
/// 3. Use [WidgetStateColor.fromMap] to assign a value using a [WidgetStateMap].
|
||||
///
|
||||
/// If a [WidgetStateColor] is used for a property or a parameter that doesn't
|
||||
/// support resolving [WidgetStateProperty<Color>]s, then its default color
|
||||
@ -160,7 +254,17 @@ abstract class WidgetStateColor extends Color implements WidgetStateProperty<Col
|
||||
///
|
||||
/// The given callback parameter must return a non-null color in the default
|
||||
/// state.
|
||||
static WidgetStateColor resolveWith(WidgetPropertyResolver<Color> callback) => _WidgetStateColor(callback);
|
||||
factory WidgetStateColor.resolveWith(WidgetPropertyResolver<Color> callback) = _WidgetStateColor;
|
||||
|
||||
/// Creates a [WidgetStateColor] from a [WidgetStateMap<Color>].
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetStateProperty.fromMap}
|
||||
///
|
||||
/// If used as a regular color, the first key that matches an empty
|
||||
/// [Set] of [WidgetState]s will be selected.
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetState.any}
|
||||
factory WidgetStateColor.fromMap(WidgetStateMap<Color> map) = _WidgetStateColorMapper;
|
||||
|
||||
/// Returns a [Color] that's to be used when a component is in the specified
|
||||
/// state.
|
||||
@ -182,6 +286,18 @@ class _WidgetStateColor extends WidgetStateColor {
|
||||
Color resolve(Set<WidgetState> states) => _resolve(states);
|
||||
}
|
||||
|
||||
class _WidgetStateColorMapper extends WidgetStateColor {
|
||||
_WidgetStateColorMapper(this.map)
|
||||
: super(_WidgetStateMapper<Color>(map).resolve(_defaultStates).value);
|
||||
|
||||
final WidgetStateMap<Color> map;
|
||||
|
||||
static const Set<WidgetState> _defaultStates = <WidgetState>{};
|
||||
|
||||
@override
|
||||
Color resolve(Set<WidgetState> states) => _WidgetStateMapper<Color>(map).resolve(states);
|
||||
}
|
||||
|
||||
class _WidgetStateColorTransparent extends WidgetStateColor {
|
||||
const _WidgetStateColorTransparent() : super(0x00000000);
|
||||
|
||||
@ -348,8 +464,30 @@ abstract class WidgetStateBorderSide extends BorderSide implements WidgetStatePr
|
||||
/// ```
|
||||
const factory WidgetStateBorderSide.resolveWith(WidgetPropertyResolver<BorderSide?> callback) = _WidgetStateBorderSide;
|
||||
|
||||
/// Returns a [BorderSide] that's to be used when a Material component is
|
||||
/// in the specified state. Return null to defer to the default value of the
|
||||
/// Creates a [WidgetStateBorderSide] from a [WidgetStateMap].
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetStateProperty.fromMap}
|
||||
///
|
||||
/// If used as a regular [BorderSide], the first key that matches an empty
|
||||
/// [Set] of [WidgetState]s will be selected.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```dart
|
||||
/// const Chip(
|
||||
/// label: Text('Transceiver'),
|
||||
/// side: WidgetStateBorderSide.fromMap(<WidgetStatesConstraint, BorderSide?>{
|
||||
/// WidgetState.selected: BorderSide(color: Colors.red),
|
||||
/// // returns null if not selected, deferring to default theme/widget value.
|
||||
/// }),
|
||||
/// ),
|
||||
/// ```
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetState.any}
|
||||
const factory WidgetStateBorderSide.fromMap(WidgetStateMap<BorderSide?> map) = _WidgetBorderSideMapper;
|
||||
|
||||
/// Returns a [BorderSide] that's to be used when a Widget is in the
|
||||
/// specified state. Return null to defer to the default value of the
|
||||
/// widget or theme.
|
||||
@override
|
||||
BorderSide? resolve(Set<WidgetState> states);
|
||||
@ -401,6 +539,15 @@ class _WidgetStateBorderSide extends WidgetStateBorderSide {
|
||||
BorderSide? resolve(Set<WidgetState> states) => _resolve(states);
|
||||
}
|
||||
|
||||
class _WidgetBorderSideMapper extends WidgetStateBorderSide {
|
||||
const _WidgetBorderSideMapper(this.map);
|
||||
|
||||
final WidgetStateMap<BorderSide?> map;
|
||||
|
||||
@override
|
||||
BorderSide? resolve(Set<WidgetState> states) => _WidgetStateMapper<BorderSide?>(map).resolve(states);
|
||||
}
|
||||
|
||||
/// Defines an [OutlinedBorder] whose value depends on a set of [WidgetState]s
|
||||
/// which represent the interactive state of a component.
|
||||
///
|
||||
@ -452,6 +599,7 @@ abstract class WidgetStateOutlinedBorder extends OutlinedBorder implements Widge
|
||||
/// 1. Create a subclass of [WidgetStateTextStyle] and implement the abstract `resolve` method.
|
||||
/// 2. Use [WidgetStateTextStyle.resolveWith] and pass in a callback that
|
||||
/// will be used to resolve the color in the given states.
|
||||
/// 3. Use [WidgetStateTextStyle.fromMap] to assign a style using a [WidgetStateMap].
|
||||
///
|
||||
/// If a [WidgetStateTextStyle] is used for a property or a parameter that doesn't
|
||||
/// support resolving [WidgetStateProperty<TextStyle>]s, then its default color
|
||||
@ -480,6 +628,16 @@ abstract class WidgetStateTextStyle extends TextStyle implements WidgetStateProp
|
||||
/// state.
|
||||
const factory WidgetStateTextStyle.resolveWith(WidgetPropertyResolver<TextStyle> callback) = _WidgetStateTextStyle;
|
||||
|
||||
/// Creates a [WidgetStateTextStyle] from a [WidgetStateMap].
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetStateProperty.fromMap}
|
||||
///
|
||||
/// If used as a regular text style, the first key that matches an empty
|
||||
/// [Set] of [WidgetState]s will be selected.
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetState.any}
|
||||
const factory WidgetStateTextStyle.fromMap(WidgetStateMap<TextStyle> map) = _WidgetTextStyleMapper;
|
||||
|
||||
/// Returns a [TextStyle] that's to be used when a component is in the
|
||||
/// specified state.
|
||||
@override
|
||||
@ -495,6 +653,15 @@ class _WidgetStateTextStyle extends WidgetStateTextStyle {
|
||||
TextStyle resolve(Set<WidgetState> states) => _resolve(states);
|
||||
}
|
||||
|
||||
class _WidgetTextStyleMapper extends WidgetStateTextStyle {
|
||||
const _WidgetTextStyleMapper(this.map);
|
||||
|
||||
final WidgetStateMap<TextStyle> map;
|
||||
|
||||
@override
|
||||
TextStyle resolve(Set<WidgetState> states) => _WidgetStateMapper<TextStyle>(map).resolve(states);
|
||||
}
|
||||
|
||||
/// Interface for classes that [resolve] to a value of type `T` based
|
||||
/// on a widget's interactive "state", which is defined as a set
|
||||
/// of [WidgetState]s.
|
||||
@ -519,12 +686,26 @@ class _WidgetStateTextStyle extends WidgetStateTextStyle {
|
||||
/// `WidgetStateProperty`.
|
||||
/// {@macro flutter.widgets.WidgetStateProperty.implementations}
|
||||
abstract class WidgetStateProperty<T> {
|
||||
/// Returns a value of type `T` that depends on [states].
|
||||
/// This abstract constructor allows extending the class.
|
||||
///
|
||||
/// Widgets like [TextButton] and [ElevatedButton] apply this method to their
|
||||
/// current [WidgetState]s to compute colors and other visual parameters
|
||||
/// at build time.
|
||||
T resolve(Set<WidgetState> states);
|
||||
/// [WidgetStateProperty] is designed as an interface, so this constructor
|
||||
/// is only needed for backward compatibility.
|
||||
WidgetStateProperty();
|
||||
|
||||
/// Creates a property that resolves using a [WidgetStateMap].
|
||||
///
|
||||
/// {@template flutter.widgets.WidgetStateProperty.fromMap}
|
||||
/// This constructor's [resolve] method finds the first [MapEntry] whose
|
||||
/// key is satisfied by the set of states, and returns its associated value.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Returns `null` if no keys match, or if [T] is non-nullable,
|
||||
/// the method throws an [ArgumentError].
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetState.any}
|
||||
///
|
||||
/// {@macro flutter.widgets.WidgetStateMap}
|
||||
const factory WidgetStateProperty.fromMap(WidgetStateMap<T> map) = _WidgetStateMapper<T>;
|
||||
|
||||
/// Resolves the value for the given set of states if `value` is a
|
||||
/// [WidgetStateProperty], otherwise returns the value itself.
|
||||
@ -567,6 +748,13 @@ abstract class WidgetStateProperty<T> {
|
||||
}
|
||||
return _LerpProperties<T>(a, b, t, lerpFunction);
|
||||
}
|
||||
|
||||
/// Returns a value of type `T` that depends on [states].
|
||||
///
|
||||
/// Widgets like [TextButton] and [ElevatedButton] apply this method to their
|
||||
/// current [WidgetState]s to compute colors and other visual parameters
|
||||
/// at build time.
|
||||
T resolve(Set<WidgetState> states);
|
||||
}
|
||||
|
||||
class _LerpProperties<T> implements WidgetStateProperty<T?> {
|
||||
@ -594,6 +782,96 @@ class _WidgetStatePropertyWith<T> implements WidgetStateProperty<T> {
|
||||
T resolve(Set<WidgetState> states) => _resolve(states);
|
||||
}
|
||||
|
||||
/// A [Map] used to resolve to a single value of type `T` based on
|
||||
/// the current set of Widget states.
|
||||
///
|
||||
/// {@template flutter.widgets.WidgetStateMap}
|
||||
/// Example:
|
||||
///
|
||||
/// ```dart
|
||||
/// // This WidgetStateMap<Color?> resolves to null if no keys match.
|
||||
/// WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
|
||||
/// WidgetState.error: Colors.red,
|
||||
/// WidgetState.hovered & WidgetState.focused: Colors.blueAccent,
|
||||
/// WidgetState.focused: Colors.blue,
|
||||
/// ~WidgetState.disabled: Colors.black,
|
||||
/// });
|
||||
///
|
||||
/// // The same can be accomplished with a WidgetPropertyResolver,
|
||||
/// // but it's more verbose:
|
||||
/// WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) {
|
||||
/// if (states.contains(WidgetState.error)) {
|
||||
/// return Colors.red;
|
||||
/// } else if (states.contains(WidgetState.hovered) && states.contains(WidgetState.focused)) {
|
||||
/// return Colors.blueAccent;
|
||||
/// } else if (states.contains(WidgetState.focused)) {
|
||||
/// return Colors.blue;
|
||||
/// } else if (!states.contains(WidgetState.disabled)) {
|
||||
/// return Colors.black;
|
||||
/// }
|
||||
/// return null;
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// A widget state combination can be stored in a variable,
|
||||
/// and [WidgetState.any] can be used for non-nullable types to ensure
|
||||
/// that there's a match:
|
||||
///
|
||||
/// ```dart
|
||||
/// final WidgetStatesConstraint selectedError = WidgetState.selected & WidgetState.error;
|
||||
///
|
||||
/// final WidgetStateProperty<Color> color = WidgetStateProperty<Color>.fromMap(
|
||||
/// <WidgetStatesConstraint, Color>{
|
||||
/// selectedError & WidgetState.hovered: Colors.redAccent,
|
||||
/// selectedError: Colors.red,
|
||||
/// WidgetState.any: Colors.black,
|
||||
/// },
|
||||
/// );
|
||||
///
|
||||
/// // The (more verbose) WidgetPropertyResolver implementation:
|
||||
/// final WidgetStateProperty<Color> colorResolveWith = WidgetStateProperty.resolveWith<Color>(
|
||||
/// (Set<WidgetState> states) {
|
||||
/// if (states.containsAll(<WidgetState>{WidgetState.selected, WidgetState.error})) {
|
||||
/// if (states.contains(WidgetState.hovered)) {
|
||||
/// return Colors.redAccent;
|
||||
/// }
|
||||
/// return Colors.red;
|
||||
/// }
|
||||
/// return Colors.black;
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
/// {@endtemplate}
|
||||
typedef WidgetStateMap<T> = Map<WidgetStatesConstraint, T>;
|
||||
|
||||
// A private class, used to create the [WidgetStateProperty.fromMap] constructor.
|
||||
class _WidgetStateMapper<T> implements WidgetStateProperty<T> {
|
||||
const _WidgetStateMapper(this.map);
|
||||
|
||||
final WidgetStateMap<T> map;
|
||||
|
||||
@override
|
||||
T resolve(Set<WidgetState> states) {
|
||||
for (final MapEntry<WidgetStatesConstraint, T> entry in map.entries) {
|
||||
if (entry.key.isSatisfiedBy(states)) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return null as T;
|
||||
} on TypeError {
|
||||
throw ArgumentError(
|
||||
'The current set of material states is $states.\n'
|
||||
'None of the provided map keys matched this set, '
|
||||
'and the type "$T" is non-nullable.\n'
|
||||
'Consider using "WidgetStateProperty<$T?>.fromMap()", '
|
||||
'or adding the "WidgetState.any" key to this map.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience class for creating a [WidgetStateProperty] that
|
||||
/// resolves to the given value for all states.
|
||||
///
|
||||
|
@ -20,6 +20,23 @@ void main() {
|
||||
expect(value.resolve(<WidgetState>{WidgetState.error}), WidgetState.error);
|
||||
});
|
||||
|
||||
test('WidgetStateProperty.map()', () {
|
||||
final WidgetStatesConstraint active = WidgetState.hovered | WidgetState.focused | WidgetState.pressed;
|
||||
final WidgetStateProperty<String?> value = WidgetStateProperty<String?>.fromMap(
|
||||
<WidgetStatesConstraint, String?>{
|
||||
active & WidgetState.error: 'active error',
|
||||
WidgetState.disabled | WidgetState.error: 'kinda sus',
|
||||
~(WidgetState.dragged | WidgetState.selected) & ~active: 'this is boring',
|
||||
active: 'active',
|
||||
},
|
||||
);
|
||||
expect(value.resolve(<WidgetState>{WidgetState.focused, WidgetState.error}), 'active error');
|
||||
expect(value.resolve(<WidgetState>{WidgetState.scrolledUnder}), 'this is boring');
|
||||
expect(value.resolve(<WidgetState>{WidgetState.disabled}), 'kinda sus');
|
||||
expect(value.resolve(<WidgetState>{WidgetState.hovered}), 'active');
|
||||
expect(value.resolve(<WidgetState>{WidgetState.dragged}), null);
|
||||
});
|
||||
|
||||
test('WidgetStateProperty.all()', () {
|
||||
final WidgetStateProperty<int> value = WidgetStateProperty.all<int>(123);
|
||||
expect(value.resolve(<WidgetState>{WidgetState.hovered}), 123);
|
||||
|
Loading…
x
Reference in New Issue
Block a user