Unverified Commit 50dc84bc authored by MH Johnson's avatar MH Johnson Committed by GitHub

[Material] Add support for customizing active + disabled state color for...

[Material] Add support for customizing active + disabled state color for selection controls. (#68831)

* Add new MaterialStateProperty param to selection controls
parent 018467cd
...@@ -63,6 +63,7 @@ class Checkbox extends StatefulWidget { ...@@ -63,6 +63,7 @@ class Checkbox extends StatefulWidget {
required this.onChanged, required this.onChanged,
this.mouseCursor, this.mouseCursor,
this.activeColor, this.activeColor,
this.fillColor,
this.checkColor, this.checkColor,
this.focusColor, this.focusColor,
this.hoverColor, this.hoverColor,
...@@ -130,8 +131,23 @@ class Checkbox extends StatefulWidget { ...@@ -130,8 +131,23 @@ class Checkbox extends StatefulWidget {
/// The color to use when this checkbox is checked. /// The color to use when this checkbox is checked.
/// ///
/// Defaults to [ThemeData.toggleableActiveColor]. /// Defaults to [ThemeData.toggleableActiveColor].
///
/// If [fillColor] returns a non-null color in the [MaterialState.selected]
/// state, it will be used instead of this color.
final Color? activeColor; final Color? activeColor;
/// The color that fills the checkbox when it is checked, in all
/// [MaterialState]s.
///
/// If this is provided, it will be used over [activeColor].
///
/// Resolves in the following states:
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
final MaterialStateProperty<Color?>? fillColor;
/// The color to use for the check icon when this checkbox is checked. /// The color to use for the check icon when this checkbox is checked.
/// ///
/// Defaults to Color(0xFFFFFFFF) /// Defaults to Color(0xFFFFFFFF)
...@@ -236,6 +252,38 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -236,6 +252,38 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
} }
} }
Set<MaterialState> get _states => <MaterialState>{
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.value == null || widget.value!) MaterialState.selected,
};
MaterialStateProperty<Color?> get _widgetFillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return widget.activeColor;
}
return null;
});
}
MaterialStateProperty<Color> get _defaultFillColor {
final ThemeData themeData = Theme.of(context);
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return themeData.disabledColor;
}
if (states.contains(MaterialState.selected)) {
return themeData.toggleableActiveColor;
}
return themeData.unselectedWidgetColor;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -253,13 +301,18 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -253,13 +301,18 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
final BoxConstraints additionalConstraints = BoxConstraints.tight(size); final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>( final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable, widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{ _states,
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.tristate || widget.value!) MaterialState.selected,
},
); );
// Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between.
final Set<MaterialState> activeStates = _states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = _states..remove(MaterialState.selected);
final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates)
?? _widgetFillColor.resolve(activeStates)
?? _defaultFillColor.resolve(activeStates);
final Color effectiveInactiveColor = widget.fillColor?.resolve(inactiveStates)
?? _widgetFillColor.resolve(inactiveStates)
?? _defaultFillColor.resolve(inactiveStates);
return FocusableActionDetector( return FocusableActionDetector(
actions: _actionMap, actions: _actionMap,
...@@ -274,9 +327,9 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -274,9 +327,9 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
return _CheckboxRenderObjectWidget( return _CheckboxRenderObjectWidget(
value: widget.value, value: widget.value,
tristate: widget.tristate, tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor, activeColor: effectiveActiveColor,
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF), checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
inactiveColor: enabled ? themeData.unselectedWidgetColor : themeData.disabledColor, inactiveColor: effectiveInactiveColor,
focusColor: widget.focusColor ?? themeData.focusColor, focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor, hoverColor: widget.hoverColor ?? themeData.hoverColor,
splashRadius: widget.splashRadius ?? kRadialReactionRadius, splashRadius: widget.splashRadius ?? kRadialReactionRadius,
...@@ -434,9 +487,7 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -434,9 +487,7 @@ class _RenderCheckbox extends RenderToggleable {
// value == true or null. // value == true or null.
Color _colorAt(double t) { Color _colorAt(double t) {
// As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor. // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
return onChanged == null return t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0)!;
? inactiveColor
: (t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0)!);
} }
// White stroke used to paint the check and dash. // White stroke used to paint the check and dash.
......
...@@ -112,6 +112,7 @@ class Radio<T> extends StatefulWidget { ...@@ -112,6 +112,7 @@ class Radio<T> extends StatefulWidget {
this.mouseCursor, this.mouseCursor,
this.toggleable = false, this.toggleable = false,
this.activeColor, this.activeColor,
this.fillColor,
this.focusColor, this.focusColor,
this.hoverColor, this.hoverColor,
this.splashRadius, this.splashRadius,
...@@ -240,8 +241,23 @@ class Radio<T> extends StatefulWidget { ...@@ -240,8 +241,23 @@ class Radio<T> extends StatefulWidget {
/// The color to use when this radio button is selected. /// The color to use when this radio button is selected.
/// ///
/// Defaults to [ThemeData.toggleableActiveColor]. /// Defaults to [ThemeData.toggleableActiveColor].
///
/// If [fillColor] returns a non-null color in the [MaterialState.selected]
/// state, it will be used instead of this color.
final Color? activeColor; final Color? activeColor;
/// The color that fills the checkbox when it is checked, in all
/// [MaterialState]s.
///
/// If this is provided, it will be used over [activeColor].
///
/// Resolves in the following states:
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
final MaterialStateProperty<Color?>? fillColor;
/// Configures the minimum size of the tap target. /// Configures the minimum size of the tap target.
/// ///
/// Defaults to [ThemeData.materialTapTargetSize]. /// Defaults to [ThemeData.materialTapTargetSize].
...@@ -318,10 +334,6 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -318,10 +334,6 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
} }
} }
Color _getInactiveColor(ThemeData themeData) {
return enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
}
void _handleChanged(bool? selected) { void _handleChanged(bool? selected) {
if (selected == null) { if (selected == null) {
widget.onChanged!(null); widget.onChanged!(null);
...@@ -332,6 +344,40 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -332,6 +344,40 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
} }
} }
bool get _selected => widget.value == widget.groupValue;
Set<MaterialState> get _states => <MaterialState>{
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (_selected) MaterialState.selected,
};
MaterialStateProperty<Color?> get _widgetFillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return widget.activeColor;
}
return null;
});
}
MaterialStateProperty<Color> get _defaultFillColor {
final ThemeData themeData = Theme.of(context);
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return themeData.disabledColor;
}
if (states.contains(MaterialState.selected)) {
return themeData.toggleableActiveColor;
}
return themeData.unselectedWidgetColor;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -347,16 +393,20 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -347,16 +393,20 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
} }
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment; size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size); final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final bool selected = widget.value == widget.groupValue;
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>( final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable, widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{ _states,
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (selected) MaterialState.selected,
},
); );
// Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between.
final Set<MaterialState> activeStates = _states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = _states..remove(MaterialState.selected);
final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates)
?? _widgetFillColor.resolve(activeStates)
?? _defaultFillColor.resolve(activeStates);
final Color effectiveInactiveColor = widget.fillColor?.resolve(inactiveStates)
?? _widgetFillColor.resolve(inactiveStates)
?? _defaultFillColor.resolve(inactiveStates);
return FocusableActionDetector( return FocusableActionDetector(
actions: _actionMap, actions: _actionMap,
...@@ -369,9 +419,9 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -369,9 +419,9 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return _RadioRenderObjectWidget( return _RadioRenderObjectWidget(
selected: selected, selected: _selected,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor, activeColor: effectiveActiveColor,
inactiveColor: _getInactiveColor(themeData), inactiveColor: effectiveInactiveColor,
focusColor: widget.focusColor ?? themeData.focusColor, focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor, hoverColor: widget.hoverColor ?? themeData.hoverColor,
splashRadius: widget.splashRadius ?? kRadialReactionRadius, splashRadius: widget.splashRadius ?? kRadialReactionRadius,
...@@ -493,11 +543,10 @@ class _RenderRadio extends RenderToggleable { ...@@ -493,11 +543,10 @@ class _RenderRadio extends RenderToggleable {
paintRadialReaction(canvas, offset, size.center(Offset.zero)); paintRadialReaction(canvas, offset, size.center(Offset.zero));
final Offset center = (offset & size).center; final Offset center = (offset & size).center;
final Color radioColor = onChanged != null ? activeColor : inactiveColor;
// Outer circle // Outer circle
final Paint paint = Paint() final Paint paint = Paint()
..color = Color.lerp(inactiveColor, radioColor, position.value)! ..color = Color.lerp(inactiveColor, activeColor, position.value)!
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 2.0; ..strokeWidth = 2.0;
canvas.drawCircle(center, _kOuterRadius, paint); canvas.drawCircle(center, _kOuterRadius, paint);
......
...@@ -76,6 +76,8 @@ class Switch extends StatefulWidget { ...@@ -76,6 +76,8 @@ class Switch extends StatefulWidget {
this.onActiveThumbImageError, this.onActiveThumbImageError,
this.inactiveThumbImage, this.inactiveThumbImage,
this.onInactiveThumbImageError, this.onInactiveThumbImageError,
this.thumbColor,
this.trackColor,
this.materialTapTargetSize, this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor, this.mouseCursor,
...@@ -118,6 +120,8 @@ class Switch extends StatefulWidget { ...@@ -118,6 +120,8 @@ class Switch extends StatefulWidget {
this.inactiveThumbImage, this.inactiveThumbImage,
this.onInactiveThumbImageError, this.onInactiveThumbImageError,
this.materialTapTargetSize, this.materialTapTargetSize,
this.thumbColor,
this.trackColor,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor, this.mouseCursor,
this.focusColor, this.focusColor,
...@@ -163,6 +167,9 @@ class Switch extends StatefulWidget { ...@@ -163,6 +167,9 @@ class Switch extends StatefulWidget {
/// The color to use when this switch is on. /// The color to use when this switch is on.
/// ///
/// Defaults to [ThemeData.toggleableActiveColor]. /// Defaults to [ThemeData.toggleableActiveColor].
///
/// If [thumbColor] returns a non-null color in the [MaterialState.selected]
/// state, it will be used instead of this color.
final Color? activeColor; final Color? activeColor;
/// The color to use on the track when this switch is on. /// The color to use on the track when this switch is on.
...@@ -170,6 +177,9 @@ class Switch extends StatefulWidget { ...@@ -170,6 +177,9 @@ class Switch extends StatefulWidget {
/// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%. /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%.
/// ///
/// Ignored if this switch is created with [Switch.adaptive]. /// Ignored if this switch is created with [Switch.adaptive].
///
/// If [trackColor] returns a non-null color in the [MaterialState.selected]
/// state, it will be used instead of this color.
final Color? activeTrackColor; final Color? activeTrackColor;
/// The color to use on the thumb when this switch is off. /// The color to use on the thumb when this switch is off.
...@@ -177,6 +187,9 @@ class Switch extends StatefulWidget { ...@@ -177,6 +187,9 @@ class Switch extends StatefulWidget {
/// Defaults to the colors described in the Material design specification. /// Defaults to the colors described in the Material design specification.
/// ///
/// Ignored if this switch is created with [Switch.adaptive]. /// Ignored if this switch is created with [Switch.adaptive].
///
/// If [thumbColor] returns a non-null color in the default state, it will be
/// used instead of this color.
final Color? inactiveThumbColor; final Color? inactiveThumbColor;
/// The color to use on the track when this switch is off. /// The color to use on the track when this switch is off.
...@@ -184,6 +197,9 @@ class Switch extends StatefulWidget { ...@@ -184,6 +197,9 @@ class Switch extends StatefulWidget {
/// Defaults to the colors described in the Material design specification. /// Defaults to the colors described in the Material design specification.
/// ///
/// Ignored if this switch is created with [Switch.adaptive]. /// Ignored if this switch is created with [Switch.adaptive].
///
/// If [trackColor] returns a non-null color in the default state, it will be
/// used instead of this color.
final Color? inactiveTrackColor; final Color? inactiveTrackColor;
/// An image to use on the thumb of this switch when the switch is on. /// An image to use on the thumb of this switch when the switch is on.
...@@ -204,6 +220,30 @@ class Switch extends StatefulWidget { ...@@ -204,6 +220,30 @@ class Switch extends StatefulWidget {
/// [inactiveThumbImage]. /// [inactiveThumbImage].
final ImageErrorListener? onInactiveThumbImageError; final ImageErrorListener? onInactiveThumbImageError;
/// The color of this [Switch]'s thumb.
///
/// If this is non-null, it will be used over [activeColor] and
/// [inactiveThumbColor].
///
/// Resolved in the following states:
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
final MaterialStateProperty<Color?>? thumbColor;
/// The color of this [Switch]'s track.
///
/// If this is non-null, it will be used over [activeTrackColor] and
/// [inactiveTrackColor].
///
/// Resolved in the following states:
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
final MaterialStateProperty<Color?>? trackColor;
/// Configures the minimum size of the tap target. /// Configures the minimum size of the tap target.
/// ///
/// Defaults to [ThemeData.materialTapTargetSize]. /// Defaults to [ThemeData.materialTapTargetSize].
...@@ -310,34 +350,98 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -310,34 +350,98 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
setState(() {}); setState(() {});
} }
Set<MaterialState> get _states => <MaterialState>{
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.value) MaterialState.selected,
};
MaterialStateProperty<Color?> get _widgetThumbColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return widget.inactiveThumbColor;
}
if (states.contains(MaterialState.selected)) {
return widget.activeColor;
}
return widget.inactiveThumbColor;
});
}
MaterialStateProperty<Color> get _defaultThumbColor {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return isDark ? Colors.grey.shade800 : Colors.grey.shade400;
}
if (states.contains(MaterialState.selected)) {
return theme.toggleableActiveColor;
}
return isDark ? Colors.grey.shade400 : Colors.grey.shade50;
});
}
MaterialStateProperty<Color?> get _widgetTrackColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return widget.inactiveTrackColor;
}
if (states.contains(MaterialState.selected)) {
return widget.activeTrackColor;
}
return widget.inactiveTrackColor;
});
}
MaterialStateProperty<Color> get _defaultTrackColor {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
const Color black32 = Color(0x52000000); // Black with 32% opacity
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return isDark ? Colors.white10 : Colors.black12;
}
if (states.contains(MaterialState.selected)) {
final Set<MaterialState> activeState = states..add(MaterialState.selected);
final Color activeColor = _widgetThumbColor.resolve(activeState) ?? _defaultThumbColor.resolve(activeState);
return activeColor.withAlpha(0x80);
}
return isDark ? Colors.white30 : black32;
});
}
Widget buildMaterialSwitch(BuildContext context) { Widget buildMaterialSwitch(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor; // Colors need to be resolved in selected and non selected states separately
final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80); // so that they can be lerped between.
final Set<MaterialState> activeStates = _states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = _states..remove(MaterialState.selected);
final Color effectiveActiveThumbColor = widget.thumbColor?.resolve(activeStates)
?? _widgetThumbColor.resolve(activeStates)
?? _defaultThumbColor.resolve(activeStates);
final Color effectiveInactiveThumbColor = widget.thumbColor?.resolve(inactiveStates)
?? _widgetThumbColor.resolve(inactiveStates)
?? _defaultThumbColor.resolve(inactiveStates);
final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates)
?? _widgetTrackColor.resolve(activeStates)
?? _defaultTrackColor.resolve(activeStates);
final Color effectiveInactiveTrackColor = widget.trackColor?.resolve(inactiveStates)
?? _widgetTrackColor.resolve(inactiveStates)
?? _defaultTrackColor.resolve(inactiveStates);
final Color hoverColor = widget.hoverColor ?? theme.hoverColor; final Color hoverColor = widget.hoverColor ?? theme.hoverColor;
final Color focusColor = widget.focusColor ?? theme.focusColor; final Color focusColor = widget.focusColor ?? theme.focusColor;
final Color inactiveThumbColor;
final Color inactiveTrackColor;
if (enabled) {
const Color black32 = Color(0x52000000); // Black with 32% opacity
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
} else {
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
}
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>( final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable, widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{ _states,
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.value) MaterialState.selected,
},
); );
return FocusableActionDetector( return FocusableActionDetector(
...@@ -353,8 +457,9 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -353,8 +457,9 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
return _SwitchRenderObjectWidget( return _SwitchRenderObjectWidget(
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
value: widget.value, value: widget.value,
activeColor: activeThumbColor, activeColor: effectiveActiveThumbColor,
inactiveColor: inactiveThumbColor, inactiveColor: effectiveInactiveThumbColor,
surfaceColor: theme.colorScheme.surface,
hoverColor: hoverColor, hoverColor: hoverColor,
focusColor: focusColor, focusColor: focusColor,
splashRadius: widget.splashRadius ?? kRadialReactionRadius, splashRadius: widget.splashRadius ?? kRadialReactionRadius,
...@@ -362,8 +467,8 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -362,8 +467,8 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
onActiveThumbImageError: widget.onActiveThumbImageError, onActiveThumbImageError: widget.onActiveThumbImageError,
inactiveThumbImage: widget.inactiveThumbImage, inactiveThumbImage: widget.inactiveThumbImage,
onInactiveThumbImageError: widget.onInactiveThumbImageError, onInactiveThumbImageError: widget.onInactiveThumbImageError,
activeTrackColor: activeTrackColor, activeTrackColor: effectiveActiveTrackColor,
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: effectiveInactiveTrackColor,
configuration: createLocalImageConfiguration(context), configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged, onChanged: widget.onChanged,
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)), additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
...@@ -442,6 +547,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -442,6 +547,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
required this.hasFocus, required this.hasFocus,
required this.hovering, required this.hovering,
required this.state, required this.state,
required this.surfaceColor,
}) : super(key: key); }) : super(key: key);
final bool value; final bool value;
...@@ -463,6 +569,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -463,6 +569,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final bool hasFocus; final bool hasFocus;
final bool hovering; final bool hovering;
final _SwitchState state; final _SwitchState state;
final Color surfaceColor;
@override @override
_RenderSwitch createRenderObject(BuildContext context) { _RenderSwitch createRenderObject(BuildContext context) {
...@@ -487,6 +594,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -487,6 +594,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
hasFocus: hasFocus, hasFocus: hasFocus,
hovering: hovering, hovering: hovering,
state: state, state: state,
surfaceColor: surfaceColor,
); );
} }
...@@ -512,7 +620,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -512,7 +620,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..dragStartBehavior = dragStartBehavior ..dragStartBehavior = dragStartBehavior
..hasFocus = hasFocus ..hasFocus = hasFocus
..hovering = hovering ..hovering = hovering
..vsync = state; ..vsync = state
..surfaceColor = surfaceColor;
} }
void _handleValueChanged(bool? value) { void _handleValueChanged(bool? value) {
...@@ -549,6 +658,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -549,6 +658,7 @@ class _RenderSwitch extends RenderToggleable {
required bool hasFocus, required bool hasFocus,
required bool hovering, required bool hovering,
required this.state, required this.state,
required Color surfaceColor,
}) : assert(textDirection != null), }) : assert(textDirection != null),
_activeThumbImage = activeThumbImage, _activeThumbImage = activeThumbImage,
_onActiveThumbImageError = onActiveThumbImageError, _onActiveThumbImageError = onActiveThumbImageError,
...@@ -558,6 +668,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -558,6 +668,7 @@ class _RenderSwitch extends RenderToggleable {
_inactiveTrackColor = inactiveTrackColor, _inactiveTrackColor = inactiveTrackColor,
_configuration = configuration, _configuration = configuration,
_textDirection = textDirection, _textDirection = textDirection,
_surfaceColor = surfaceColor,
super( super(
value: value, value: value,
tristate: false, tristate: false,
...@@ -665,6 +776,16 @@ class _RenderSwitch extends RenderToggleable { ...@@ -665,6 +776,16 @@ class _RenderSwitch extends RenderToggleable {
_drag.dragStartBehavior = value; _drag.dragStartBehavior = value;
} }
Color get surfaceColor => _surfaceColor;
Color _surfaceColor;
set surfaceColor(Color value) {
assert(value != null);
if (value == _surfaceColor)
return;
_surfaceColor = value;
markNeedsPaint();
}
_SwitchState state; _SwitchState state;
@override @override
...@@ -779,13 +900,12 @@ class _RenderSwitch extends RenderToggleable { ...@@ -779,13 +900,12 @@ class _RenderSwitch extends RenderToggleable {
break; break;
} }
final Color trackColor = isEnabled final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)!;
? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)! final Color lerpedThumbColor = Color.lerp(inactiveColor, activeColor, currentValue)!;
: inactiveTrackColor; // Blend the thumb color against a `surfaceColor` background in case the
// thumbColor is not opaque. This way we do not see through the thumb to the
final Color thumbColor = isEnabled // track underneath.
? Color.lerp(inactiveColor, activeColor, currentValue)! final Color thumbColor = Color.alphaBlend(lerpedThumbColor, surfaceColor);
: inactiveColor;
final ImageProvider? thumbImage = isEnabled final ImageProvider? thumbImage = isEnabled
? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage) ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
......
...@@ -794,6 +794,112 @@ void main() { ...@@ -794,6 +794,112 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('Checkbox fill color resolves in enabled/disabled states', (WidgetTester tester) async {
const Color activeEnabledFillColor = Color(0xFF000001);
const Color activeDisabledFillColor = Color(0xFF000002);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return activeDisabledFillColor;
}
return activeEnabledFillColor;
}
final MaterialStateProperty<Color> fillColor =
MaterialStateColor.resolveWith(getFillColor);
Widget buildFrame({required bool enabled}) {
return Material(
child: Theme(
data: ThemeData(),
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: true,
fillColor: fillColor,
onChanged: enabled ? (bool? value) { } : null,
);
},
),
),
);
}
RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
}
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: activeEnabledFillColor));
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: activeDisabledFillColor));
});
testWidgets('Checkbox fill color resolves in hovered/focused states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'checkbox');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredFillColor = Color(0xFF000001);
const Color focusedFillColor = Color(0xFF000002);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredFillColor;
}
if (states.contains(MaterialState.focused)) {
return focusedFillColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> fillColor =
MaterialStateColor.resolveWith(getFillColor);
Widget buildFrame() {
return Material(
child: Theme(
data: ThemeData(),
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
focusNode: focusNode,
autofocus: true,
value: true,
fillColor: fillColor,
onChanged: (bool? value) { },
);
},
),
),
);
}
RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
}
await tester.pumpWidget(buildFrame());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(getCheckboxRenderer(), paints..rrect(color: focusedFillColor));
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: hoveredFillColor));
});
} }
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor { class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
......
...@@ -747,4 +747,192 @@ void main() { ...@@ -747,4 +747,192 @@ void main() {
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
}); });
testWidgets('Radio button fill color resolves in enabled/disabled states', (WidgetTester tester) async {
const Color activeEnabledFillColor = Color(0xFF000001);
const Color activeDisabledFillColor = Color(0xFF000002);
const Color inactiveEnabledFillColor = Color(0xFF000003);
const Color inactiveDisabledFillColor = Color(0xFF000004);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return activeDisabledFillColor;
}
return inactiveDisabledFillColor;
}
if (states.contains(MaterialState.selected)) {
return activeEnabledFillColor;
}
return inactiveEnabledFillColor;
}
final MaterialStateProperty<Color> fillColor =
MaterialStateColor.resolveWith(getFillColor);
int? groupValue = 0;
const Key radioKey = Key('radio');
Widget buildApp({required bool enabled}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Container(
width: 100,
height: 100,
color: Colors.white,
child: Radio<int>(
key: radioKey,
value: 0,
fillColor: fillColor,
onChanged: enabled ? (int? newValue) {
setState(() {
groupValue = newValue;
});
} : null,
groupValue: groupValue,
),
);
}),
),
),
);
}
await tester.pumpWidget(buildApp(enabled: true));
// Selected and enabled.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: activeEnabledFillColor)
..circle(color: activeEnabledFillColor),
);
// Check when the radio isn't selected.
groupValue = 1;
await tester.pumpWidget(buildApp(enabled: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: inactiveEnabledFillColor, style: PaintingStyle.stroke, strokeWidth: 2.0)
);
// Check when the radio is selected, but disabled.
groupValue = 0;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: activeDisabledFillColor)
..circle(color: activeDisabledFillColor),
);
// Check when the radio is unselected and disabled.
groupValue = 1;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: inactiveDisabledFillColor, style: PaintingStyle.stroke, strokeWidth: 2.0),
);
});
testWidgets('Checkbox fill color resolves in hovered/focused states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'checkbox');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredFillColor = Color(0xFF000001);
const Color focusedFillColor = Color(0xFF000002);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredFillColor;
}
if (states.contains(MaterialState.focused)) {
return focusedFillColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> fillColor =
MaterialStateColor.resolveWith(getFillColor);
int? groupValue = 0;
const Key radioKey = Key('radio');
Widget buildApp() {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Container(
width: 100,
height: 100,
color: Colors.white,
child: Radio<int>(
autofocus: true,
focusNode: focusNode,
key: radioKey,
value: 0,
fillColor: fillColor,
onChanged: (int? newValue) {
setState(() {
groupValue = newValue;
});
},
groupValue: groupValue,
),
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: Colors.black12)
..circle(color: focusedFillColor),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(radioKey)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byKey(radioKey))),
paints
..rect(
color: const Color(0xffffffff),
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
..circle(color: Colors.black12)
..circle(color: hoveredFillColor),
);
});
} }
...@@ -1088,4 +1088,413 @@ void main() { ...@@ -1088,4 +1088,413 @@ void main() {
expect(updatedSwitchRenderObject.position.isCompleted, false); expect(updatedSwitchRenderObject.position.isCompleted, false);
expect(updatedSwitchRenderObject.position.isDismissed, false); expect(updatedSwitchRenderObject.position.isDismissed, false);
}); });
testWidgets('Switch thumb color resolves in active/enabled states', (WidgetTester tester) async {
const Color activeEnabledThumbColor = Color(0xFF000001);
const Color activeDisabledThumbColor = Color(0xFF000002);
const Color inactiveEnabledThumbColor = Color(0xFF000003);
const Color inactiveDisabledThumbColor = Color(0xFF000004);
Color getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return activeDisabledThumbColor;
}
return inactiveDisabledThumbColor;
}
if (states.contains(MaterialState.selected)) {
return activeEnabledThumbColor;
}
return inactiveEnabledThumbColor;
}
final MaterialStateProperty<Color> thumbColor =
MaterialStateColor.resolveWith(getThumbColor);
Widget buildSwitch({required bool enabled, required bool active}) {
return Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
thumbColor: thumbColor,
value: active,
onChanged: enabled ? (_) { } : null,
),
),
);
},
),
);
}
await tester.pumpWidget(buildSwitch(enabled: false, active: false));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: inactiveDisabledThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
await tester.pumpWidget(buildSwitch(enabled: false, active: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: activeDisabledThumbColor),
reason: 'Active disabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: true, active: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x52000000), // Black with 32% opacity,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: inactiveEnabledThumbColor),
reason: 'Inactive enabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: false, active: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: inactiveDisabledThumbColor),
reason: 'Inactive disabled switch should match these colors',
);
});
testWidgets('Switch thumb color resolves in hovered/focused states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredThumbColor = Color(0xFF000001);
const Color focusedThumbColor = Color(0xFF000002);
Color getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredThumbColor;
}
if (states.contains(MaterialState.focused)) {
return focusedThumbColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> thumbColor =
MaterialStateColor.resolveWith(getThumbColor);
Widget buildSwitch() {
return Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
focusNode: focusNode,
autofocus: true,
value: true,
thumbColor: thumbColor,
onChanged: (_) { },
),
),
);
},
),
);
}
await tester.pumpWidget(buildSwitch());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: focusedThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: hoveredThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
});
testWidgets('Track color resolves in active/enabled states', (WidgetTester tester) async {
const Color activeEnabledTrackColor = Color(0xFF000001);
const Color activeDisabledTrackColor = Color(0xFF000002);
const Color inactiveEnabledTrackColor = Color(0xFF000003);
const Color inactiveDisabledTrackColor = Color(0xFF000004);
Color getTrackColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return activeDisabledTrackColor;
}
return inactiveDisabledTrackColor;
}
if (states.contains(MaterialState.selected)) {
return activeEnabledTrackColor;
}
return inactiveEnabledTrackColor;
}
final MaterialStateProperty<Color> trackColor =
MaterialStateColor.resolveWith(getTrackColor);
Widget buildSwitch({required bool enabled, required bool active}) {
return Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
trackColor: trackColor,
value: active,
onChanged: enabled ? (_) { } : null,
),
),
);
},
),
);
}
await tester.pumpWidget(buildSwitch(enabled: false, active: false));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: inactiveDisabledTrackColor,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive disabled switch track should use this value',
);
await tester.pumpWidget(buildSwitch(enabled: false, active: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: activeDisabledTrackColor,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Active disabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: true, active: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: inactiveEnabledTrackColor,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive enabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: false, active: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: inactiveDisabledTrackColor,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive disabled switch should match these colors',
);
});
testWidgets('Switch track color resolves in hovered/focused states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredTrackColor = Color(0xFF000001);
const Color focusedTrackColor = Color(0xFF000002);
Color getTrackColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredTrackColor;
}
if (states.contains(MaterialState.focused)) {
return focusedTrackColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> trackColor =
MaterialStateColor.resolveWith(getTrackColor);
Widget buildSwitch() {
return Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
focusNode: focusNode,
autofocus: true,
value: true,
trackColor: trackColor,
onChanged: (_) { },
),
),
);
},
),
);
}
await tester.pumpWidget(buildSwitch());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: focusedTrackColor,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive enabled switch should match these colors',
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: hoveredTrackColor,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive enabled switch should match these colors',
);
});
testWidgets('Switch thumb color is blended against surface color', (WidgetTester tester) async {
final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60);
final ThemeData theme = ThemeData.light();
Color getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return activeDisabledThumbColor;
}
return Colors.black;
}
final MaterialStateProperty<Color> thumbColor =
MaterialStateColor.resolveWith(getThumbColor);
Widget buildSwitch({required bool enabled, required bool active}) {
return Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Theme(
data: theme,
child: Material(
child: Center(
child: Switch(
thumbColor: thumbColor,
value: active,
onChanged: enabled ? (_) { } : null,
),
),
),
);
},
),
);
}
await tester.pumpWidget(buildSwitch(enabled: false, active: true));
final Color expectedThumbColor = Color.alphaBlend(activeDisabledThumbColor, theme.colorScheme.surface);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: expectedThumbColor),
reason: 'Active disabled thumb color should be blended on top of surface color',
);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment