refactor: Add MaterialStateMixin (#82843)
This commit is contained in:
parent
b5fc79f9ec
commit
02b46b0d77
@ -96,6 +96,7 @@ 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/material_state_mixin.dart';
|
||||
export 'src/material/mergeable_material.dart';
|
||||
export 'src/material/navigation_rail.dart';
|
||||
export 'src/material/navigation_rail_theme.dart';
|
||||
|
@ -13,6 +13,7 @@ import 'constants.dart';
|
||||
import 'ink_well.dart';
|
||||
import 'material.dart';
|
||||
import 'material_state.dart';
|
||||
import 'material_state_mixin.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
|
||||
@ -314,75 +315,40 @@ class RawMaterialButton extends StatefulWidget {
|
||||
State<RawMaterialButton> createState() => _RawMaterialButtonState();
|
||||
}
|
||||
|
||||
class _RawMaterialButtonState extends State<RawMaterialButton> {
|
||||
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 (_pressed != value) {
|
||||
setState(() {
|
||||
_updateState(MaterialState.pressed, value);
|
||||
widget.onHighlightChanged?.call(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
class _RawMaterialButtonState extends State<RawMaterialButton> with MaterialStateMixin {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateState(MaterialState.disabled, !widget.enabled);
|
||||
setMaterialState(MaterialState.disabled, !widget.enabled);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(RawMaterialButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateState(MaterialState.disabled, !widget.enabled);
|
||||
setMaterialState(MaterialState.disabled, !widget.enabled);
|
||||
// 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);
|
||||
if (isDisabled && isPressed) {
|
||||
removeMaterialState(MaterialState.pressed);
|
||||
}
|
||||
}
|
||||
|
||||
double get _effectiveElevation {
|
||||
// These conditionals are in order of precedence, so be careful about
|
||||
// reorganizing them.
|
||||
if (_disabled) {
|
||||
if (isDisabled) {
|
||||
return widget.disabledElevation;
|
||||
}
|
||||
if (_pressed) {
|
||||
if (isPressed) {
|
||||
return widget.highlightElevation;
|
||||
}
|
||||
if (_hovered) {
|
||||
if (isHovered) {
|
||||
return widget.hoverElevation;
|
||||
}
|
||||
if (_focused) {
|
||||
if (isFocused) {
|
||||
return widget.focusElevation;
|
||||
}
|
||||
return widget.elevation;
|
||||
@ -390,13 +356,13 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color? effectiveTextColor = MaterialStateProperty.resolveAs<Color?>(widget.textStyle?.color, _states);
|
||||
final ShapeBorder? effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder?>(widget.shape, _states);
|
||||
final Color? effectiveTextColor = MaterialStateProperty.resolveAs<Color?>(widget.textStyle?.color, materialStates);
|
||||
final ShapeBorder? effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder?>(widget.shape, materialStates);
|
||||
final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment;
|
||||
final BoxConstraints effectiveConstraints = widget.visualDensity.effectiveConstraints(widget.constraints);
|
||||
final MouseCursor? effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(
|
||||
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
|
||||
_states,
|
||||
materialStates,
|
||||
);
|
||||
final EdgeInsetsGeometry padding = widget.padding.add(
|
||||
EdgeInsets.only(
|
||||
@ -421,14 +387,14 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
|
||||
child: InkWell(
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: _handleFocusedChanged,
|
||||
onFocusChange: updateMaterialState(MaterialState.focused),
|
||||
autofocus: widget.autofocus,
|
||||
onHighlightChanged: _handleHighlightChanged,
|
||||
onHighlightChanged: updateMaterialState(MaterialState.pressed, onChanged: widget.onHighlightChanged),
|
||||
splashColor: widget.splashColor,
|
||||
highlightColor: widget.highlightColor,
|
||||
focusColor: widget.focusColor,
|
||||
hoverColor: widget.hoverColor,
|
||||
onHover: _handleHoveredChanged,
|
||||
onHover: updateMaterialState(MaterialState.hovered),
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
enableFeedback: widget.enableFeedback,
|
||||
|
@ -14,6 +14,7 @@ import 'constants.dart';
|
||||
import 'ink_well.dart';
|
||||
import 'material.dart';
|
||||
import 'material_state.dart';
|
||||
import 'material_state_mixin.dart';
|
||||
import 'theme_data.dart';
|
||||
|
||||
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
@ -176,49 +177,15 @@ abstract class ButtonStyleButton extends StatefulWidget {
|
||||
/// * [TextButton], a simple button without a shadow.
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [OutlinedButton], similar to [TextButton], but with an outline.
|
||||
class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStateMixin {
|
||||
class _ButtonStyleState extends State<ButtonStyleButton> with MaterialStateMixin, TickerProviderStateMixin {
|
||||
AnimationController? _controller;
|
||||
double? _elevation;
|
||||
Color? _backgroundColor;
|
||||
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 (_pressed != value) {
|
||||
setState(() {
|
||||
_updateState(MaterialState.pressed, 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);
|
||||
setMaterialState(MaterialState.disabled, !widget.enabled);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -230,13 +197,13 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
|
||||
@override
|
||||
void didUpdateWidget(ButtonStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateState(MaterialState.disabled, !widget.enabled);
|
||||
setMaterialState(MaterialState.disabled, !widget.enabled);
|
||||
// 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);
|
||||
if (isDisabled && isPressed) {
|
||||
removeMaterialState(MaterialState.pressed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,7 +223,7 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
|
||||
|
||||
T? resolve<T>(MaterialStateProperty<T>? Function(ButtonStyle? style) getProperty) {
|
||||
return effectiveValue(
|
||||
(ButtonStyle? style) => getProperty(style)?.resolve(_states),
|
||||
(ButtonStyle? style) => getProperty(style)?.resolve(materialStates),
|
||||
);
|
||||
}
|
||||
|
||||
@ -367,13 +334,13 @@ class _ButtonStyleState extends State<ButtonStyleButton> with TickerProviderStat
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
onHighlightChanged: _handleHighlightChanged,
|
||||
onHover: _handleHoveredChanged,
|
||||
onHighlightChanged: updateMaterialState(MaterialState.pressed),
|
||||
onHover: updateMaterialState(MaterialState.hovered),
|
||||
mouseCursor: resolvedMouseCursor,
|
||||
enableFeedback: resolvedEnableFeedback,
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: _handleFocusedChanged,
|
||||
onFocusChange: updateMaterialState(MaterialState.focused),
|
||||
autofocus: widget.autofocus,
|
||||
splashFactory: resolvedSplashFactory,
|
||||
overlayColor: overlayColor,
|
||||
|
@ -17,6 +17,7 @@ import 'ink_well.dart';
|
||||
import 'material.dart';
|
||||
import 'material_localizations.dart';
|
||||
import 'material_state.dart';
|
||||
import 'material_state_mixin.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
import 'tooltip.dart';
|
||||
@ -1631,7 +1632,7 @@ class RawChip extends StatefulWidget
|
||||
State<RawChip> createState() => _RawChipState();
|
||||
}
|
||||
|
||||
class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip> {
|
||||
class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProviderStateMixin<RawChip> {
|
||||
static const Duration pressedAnimationDuration = Duration(milliseconds: 75);
|
||||
|
||||
late AnimationController selectController;
|
||||
@ -1644,8 +1645,6 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
||||
late Animation<double> enableAnimation;
|
||||
late Animation<double> selectionFade;
|
||||
|
||||
final Set<MaterialState> _states = <MaterialState>{};
|
||||
|
||||
final GlobalKey deleteIconKey = GlobalKey();
|
||||
|
||||
bool get hasDeleteButton => widget.onDeleted != null;
|
||||
@ -1664,8 +1663,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);
|
||||
setMaterialState(MaterialState.disabled, !widget.isEnabled);
|
||||
setMaterialState(MaterialState.selected, widget.selected);
|
||||
selectController = AnimationController(
|
||||
duration: _kSelectDuration,
|
||||
value: widget.selected == true ? 1.0 : 0.0,
|
||||
@ -1736,17 +1735,13 @@ 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;
|
||||
}
|
||||
setMaterialState(MaterialState.pressed, true);
|
||||
setState(() {
|
||||
_isTapping = true;
|
||||
_updateState(MaterialState.pressed, true);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1754,9 +1749,9 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
||||
if (!canTap) {
|
||||
return;
|
||||
}
|
||||
setMaterialState(MaterialState.pressed, false);
|
||||
setState(() {
|
||||
_isTapping = false;
|
||||
_updateState(MaterialState.pressed, false);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1764,32 +1759,20 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
||||
if (!canTap) {
|
||||
return;
|
||||
}
|
||||
setMaterialState(MaterialState.pressed, false);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
OutlinedBorder _getShape(ChipThemeData theme) {
|
||||
final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, _states)
|
||||
?? MaterialStateProperty.resolveAs<BorderSide?>(theme.side, _states);
|
||||
final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, _states)
|
||||
?? MaterialStateProperty.resolveAs<OutlinedBorder?>(theme.shape, _states)
|
||||
final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, materialStates)
|
||||
?? MaterialStateProperty.resolveAs<BorderSide?>(theme.side, materialStates);
|
||||
final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, materialStates)
|
||||
?? MaterialStateProperty.resolveAs<OutlinedBorder?>(theme.shape, materialStates)
|
||||
?? const StadiumBorder();
|
||||
return resolvedShape.copyWith(side: resolvedSide);
|
||||
}
|
||||
@ -1813,7 +1796,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isEnabled != widget.isEnabled) {
|
||||
setState(() {
|
||||
_updateState(MaterialState.disabled, !widget.isEnabled);
|
||||
setMaterialState(MaterialState.disabled, !widget.isEnabled);
|
||||
if (widget.isEnabled) {
|
||||
enableController.forward();
|
||||
} else {
|
||||
@ -1832,7 +1815,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
||||
}
|
||||
if (oldWidget.selected != widget.selected) {
|
||||
setState(() {
|
||||
_updateState(MaterialState.selected, widget.selected);
|
||||
setMaterialState(MaterialState.selected, widget.selected);
|
||||
if (widget.selected == true) {
|
||||
selectController.forward();
|
||||
} else {
|
||||
@ -1932,7 +1915,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
||||
final bool showCheckmark = widget.showCheckmark ?? chipTheme.showCheckmark ?? true;
|
||||
|
||||
final TextStyle effectiveLabelStyle = chipTheme.labelStyle.merge(widget.labelStyle);
|
||||
final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, _states);
|
||||
final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, materialStates);
|
||||
final TextStyle resolvedLabelStyle = effectiveLabelStyle.copyWith(color: resolvedLabelColor);
|
||||
final EdgeInsetsGeometry labelPadding = widget.labelPadding ?? chipTheme.labelPadding ?? _defaultLabelPadding;
|
||||
|
||||
@ -1943,14 +1926,14 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
||||
shape: resolvedShape,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
child: InkWell(
|
||||
onFocusChange: _handleFocus,
|
||||
onFocusChange: updateMaterialState(MaterialState.focused),
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
canRequestFocus: widget.isEnabled,
|
||||
onTap: canTap ? _handleTap : null,
|
||||
onTapDown: canTap ? _handleTapDown : null,
|
||||
onTapCancel: canTap ? _handleTapCancel : null,
|
||||
onHover: canTap ? _handleHover : null,
|
||||
onHover: canTap ? updateMaterialState(MaterialState.hovered) : null,
|
||||
splashFactory: _LocationAwareInkRippleFactory(
|
||||
hasDeleteButton,
|
||||
context,
|
||||
|
167
packages/flutter/lib/src/material/material_state_mixin.dart
Normal file
167
packages/flutter/lib/src/material/material_state_mixin.dart
Normal file
@ -0,0 +1,167 @@
|
||||
// Copyright 2014 The Flutter 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 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'material_state.dart';
|
||||
|
||||
/// Mixin for [State] classes that require knowledge of changing [MaterialState]
|
||||
/// values for their child widgets.
|
||||
///
|
||||
/// This mixin does nothing by mere application to a [State] class, but is
|
||||
/// helpful when writing `build` methods that include child [InkWell],
|
||||
/// [GestureDetector], [MouseRegion], or [Focus] widgets. Instead of manually
|
||||
/// creating handlers for each type of user interaction, such [State] classes can
|
||||
/// instead provide a `ValueChanged<bool>` function and allow [MaterialStateMixin]
|
||||
/// to manage the set of active [MaterialState]s, and the calling of [setState]
|
||||
/// as necessary.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// This example shows how to write a [StatefulWidget] that uses the
|
||||
/// [MaterialStateMixin] class to watch [MaterialState] values.
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyWidget extends StatefulWidget {
|
||||
/// const MyWidget({required this.color, required this.child, Key? key}) : super(key: key);
|
||||
///
|
||||
/// final MaterialStateColor color;
|
||||
/// final Widget child;
|
||||
///
|
||||
/// @override
|
||||
/// State<MyWidget> createState() => MyWidgetState();
|
||||
/// }
|
||||
///
|
||||
/// class MyWidgetState extends State<MyWidget> with MaterialStateMixin<MyWidget> {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return InkWell(
|
||||
/// onFocusChange: updateMaterialState(MaterialState.focused),
|
||||
/// child: Container(
|
||||
/// color: widget.color.resolve(materialStates),
|
||||
/// child: widget.child,
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
@optionalTypeArgs
|
||||
mixin MaterialStateMixin<T extends StatefulWidget> on State<T> {
|
||||
/// Managed set of active [MaterialState] values; designed to be passed to
|
||||
/// [MaterialStateProperty.resolve] methods.
|
||||
///
|
||||
/// To mutate and have [setState] called automatically for you, use
|
||||
/// [setMaterialState], [addMaterialState], or [removeMaterialState]. Directly
|
||||
/// mutating the set is possible, and may be necessary if you need to alter its
|
||||
/// list without calling [setState] (and thus triggering a re-render).
|
||||
///
|
||||
/// To check for a single condition, convenience getters [isPressed], [isHovered],
|
||||
/// [isFocused], etc, are available for each [MaterialState] value.
|
||||
@protected
|
||||
Set<MaterialState> materialStates = <MaterialState>{};
|
||||
|
||||
/// Callback factory which accepts a [MaterialState] value and returns a
|
||||
/// closure to mutate [materialStates] and call [setState].
|
||||
///
|
||||
/// Accepts an optional second named parameter, `onChanged`, which allows
|
||||
/// arbitrary functionality to be wired through the [MaterialStateMixin].
|
||||
/// If supplied, the [onChanged] function is only called when child widgets
|
||||
/// report events that make changes to the current set of [MaterialState]s.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// This example shows how to use the [updateMaterialState] callback factory
|
||||
/// in other widgets, including the optional [onChanged] callback.
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyWidget extends StatefulWidget {
|
||||
/// const MyWidget({this.onPressed, Key? key}) : super(key: key);
|
||||
///
|
||||
/// /// Something important this widget must do when pressed.
|
||||
/// final VoidCallback? onPressed;
|
||||
///
|
||||
/// @override
|
||||
/// State<MyWidget> createState() => MyWidgetState();
|
||||
/// }
|
||||
///
|
||||
/// class MyWidgetState extends State<MyWidget> with MaterialStateMixin<MyWidget> {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Container(
|
||||
/// color: isPressed ? Colors.black : Colors.white,
|
||||
/// child: InkWell(
|
||||
/// onHighlightChanged: updateMaterialState(
|
||||
/// MaterialState.pressed,
|
||||
/// onChanged: (bool val) {
|
||||
/// if (val) {
|
||||
/// widget.onPressed?.call();
|
||||
/// }
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
@protected
|
||||
ValueChanged<bool> updateMaterialState(MaterialState key, {ValueChanged<bool>? onChanged}) {
|
||||
return (bool value) {
|
||||
if (materialStates.contains(key) == value)
|
||||
return;
|
||||
setMaterialState(key, value);
|
||||
onChanged?.call(value);
|
||||
};
|
||||
}
|
||||
|
||||
/// Mutator to mark a [MaterialState] value as either active or inactive.
|
||||
@protected
|
||||
void setMaterialState(MaterialState _state, bool isSet) {
|
||||
return isSet ? addMaterialState(_state) : removeMaterialState(_state);
|
||||
}
|
||||
|
||||
/// Mutator to mark a [MaterialState] value as active.
|
||||
@protected
|
||||
void addMaterialState(MaterialState _state) {
|
||||
if (materialStates.add(_state))
|
||||
setState((){});
|
||||
}
|
||||
|
||||
/// Mutator to mark a [MaterialState] value as inactive.
|
||||
@protected
|
||||
void removeMaterialState(MaterialState _state) {
|
||||
if (materialStates.remove(_state))
|
||||
setState((){});
|
||||
}
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.disabled] to be active.
|
||||
bool get isDisabled => materialStates.contains(MaterialState.disabled);
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.dragged] to be active.
|
||||
bool get isDragged => materialStates.contains(MaterialState.dragged);
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.error] to be active.
|
||||
bool get isErrored => materialStates.contains(MaterialState.error);
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.focused] to be active.
|
||||
bool get isFocused => materialStates.contains(MaterialState.focused);
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.hovered] to be active.
|
||||
bool get isHovered => materialStates.contains(MaterialState.hovered);
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.pressed] to be active.
|
||||
bool get isPressed => materialStates.contains(MaterialState.pressed);
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.scrolledUnder] to be active.
|
||||
bool get isScrolledUnder => materialStates.contains(MaterialState.scrolledUnder);
|
||||
|
||||
/// Getter for whether this class considers [MaterialState.selected] to be active.
|
||||
bool get isSelected => materialStates.contains(MaterialState.selected);
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Set<MaterialState>>('materialStates', materialStates, defaultValue: <MaterialState>{}));
|
||||
}
|
||||
}
|
168
packages/flutter/test/material/material_state_mixin_test.dart
Normal file
168
packages/flutter/test/material/material_state_mixin_test.dart
Normal file
@ -0,0 +1,168 @@
|
||||
// Copyright 2014 The Flutter 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:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
const Key key = Key('testContainer');
|
||||
const Color trueColor = Colors.red;
|
||||
const Color falseColor = Colors.green;
|
||||
|
||||
/// Mock widget which plays the role of a button -- it can emit notifications
|
||||
/// that [MaterialState] values are now in or out of play.
|
||||
class _InnerWidget extends StatefulWidget {
|
||||
const _InnerWidget({required this.onValueChanged, required this.controller, Key? key}) : super(key: key);
|
||||
final ValueChanged<bool> onValueChanged;
|
||||
final StreamController<bool> controller;
|
||||
|
||||
@override
|
||||
_InnerWidgetState createState() => _InnerWidgetState();
|
||||
}
|
||||
|
||||
class _InnerWidgetState extends State<_InnerWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.stream.listen((bool val) => widget.onValueChanged(val));
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) => Container();
|
||||
}
|
||||
|
||||
class _MyWidget extends StatefulWidget {
|
||||
const _MyWidget({
|
||||
required this.controller,
|
||||
required this.evaluator,
|
||||
required this.materialState,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Wrapper around `MaterialStateMixin.isPressed/isHovered/isFocused/etc`.
|
||||
final bool Function(_MyWidgetState state) evaluator;
|
||||
|
||||
/// Stream passed down to the child [_InnerWidget] to begin the process.
|
||||
/// This plays the role of an actual user interaction in the wild, but allows
|
||||
/// us to engage the system without mocking pointers/hovers etc.
|
||||
final StreamController<bool> controller;
|
||||
|
||||
/// The value we're watching in the given test.
|
||||
final MaterialState materialState;
|
||||
|
||||
@override
|
||||
State createState() => _MyWidgetState();
|
||||
}
|
||||
|
||||
class _MyWidgetState extends State<_MyWidget> with MaterialStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
key: key,
|
||||
color: widget.evaluator(this) ? trueColor : falseColor,
|
||||
child: _InnerWidget(
|
||||
onValueChanged: updateMaterialState(widget.materialState),
|
||||
controller: widget.controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
||||
Future<void> _verify(WidgetTester tester, Widget widget, StreamController<bool> controller,) async {
|
||||
await tester.pumpWidget(MaterialApp(home: Scaffold(body: widget)));
|
||||
// Set the value to True
|
||||
controller.sink.add(true);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<Container>(find.byKey(key)).color, trueColor);
|
||||
|
||||
// Set the value to False
|
||||
controller.sink.add(false);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<Container>(find.byKey(key)).color, falseColor);
|
||||
}
|
||||
|
||||
testWidgets('MaterialState.pressed is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isPressed,
|
||||
materialState: MaterialState.pressed,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
|
||||
testWidgets('MaterialState.focused is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isFocused,
|
||||
materialState: MaterialState.focused,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
|
||||
testWidgets('MaterialState.hovered is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isHovered,
|
||||
materialState: MaterialState.hovered,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
|
||||
testWidgets('MaterialState.disabled is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isDisabled,
|
||||
materialState: MaterialState.disabled,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
|
||||
testWidgets('MaterialState.selected is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isSelected,
|
||||
materialState: MaterialState.selected,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
|
||||
testWidgets('MaterialState.scrolledUnder is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isScrolledUnder,
|
||||
materialState: MaterialState.scrolledUnder,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
|
||||
testWidgets('MaterialState.dragged is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isDragged,
|
||||
materialState: MaterialState.dragged,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
|
||||
testWidgets('MaterialState.error is tracked', (WidgetTester tester) async {
|
||||
final StreamController<bool> controller = StreamController<bool>();
|
||||
final _MyWidget widget = _MyWidget(
|
||||
controller: controller,
|
||||
evaluator: (_MyWidgetState state) => state.isErrored,
|
||||
materialState: MaterialState.error,
|
||||
);
|
||||
await _verify(tester, widget, controller);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user