Revert "Toggable Refactor (#76745)" (#77068)

This reverts commit 14552a96c1.
This commit is contained in:
Michael Goderbauer 2021-03-02 08:10:37 -08:00 committed by GitHub
parent 9964e8fe38
commit 328a262ed6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1347 additions and 1088 deletions

View file

@ -290,39 +290,56 @@ class Checkbox extends StatefulWidget {
_CheckboxState createState() => _CheckboxState();
}
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, ToggleableStateMixin {
final _CheckboxPainter _painter = _CheckboxPainter();
bool? _previousValue;
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
late Map<Type, Action<Intent>> _actionMap;
@override
void initState() {
super.initState();
_previousValue = widget.value;
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
};
}
@override
void didUpdateWidget(Checkbox oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
_previousValue = oldWidget.value;
animateToValue();
void _actionHandler(ActivateIntent intent) {
if (widget.onChanged != null) {
switch (widget.value) {
case false:
widget.onChanged!(true);
break;
case true:
widget.onChanged!(widget.tristate ? null : false);
break;
case null:
widget.onChanged!(false);
break;
}
}
final RenderObject renderObject = context.findRenderObject()!;
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
setState(() { _focused = focused; });
}
}
@override
void dispose() {
_painter.dispose();
super.dispose();
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
}
}
@override
ValueChanged<bool?>? get onChanged => widget.onChanged;
@override
bool get tristate => widget.tristate;
@override
bool? get value => widget.value;
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) {
@ -369,17 +386,14 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
break;
}
size += effectiveVisualDensity.baseSizeAdjustment;
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? themeData.checkboxTheme.mouseCursor?.resolve(states)
?? MaterialStateMouseCursor.clickable.resolve(states);
});
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states)
?? themeData.checkboxTheme.mouseCursor?.resolve(_states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states);
// 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 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)
?? themeData.checkboxTheme.fillColor?.resolve(activeStates)
@ -389,13 +403,13 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
?? themeData.checkboxTheme.fillColor?.resolve(inactiveStates)
?? _defaultFillColor.resolve(inactiveStates);
final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
final Set<MaterialState> focusedStates = _states..add(MaterialState.focused);
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? themeData.checkboxTheme.overlayColor?.resolve(focusedStates)
?? themeData.focusColor;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
final Set<MaterialState> hoveredStates = _states..add(MaterialState.hovered);
final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor
?? themeData.checkboxTheme.overlayColor?.resolve(hoveredStates)
@ -411,96 +425,195 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
?? themeData.checkboxTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveColor.withAlpha(kRadialReactionAlpha);
final Color effectiveCheckColor = widget.checkColor
?? themeData.checkboxTheme.checkColor?.resolve(states)
final Color effectiveCheckColor = widget.checkColor
?? themeData.checkboxTheme.checkColor?.resolve(_states)
?? const Color(0xFFFFFFFF);
return Semantics(
checked: widget.value == true,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
size: size,
painter: _painter
..position = position
..reaction = reaction
..reactionFocusFade = reactionFocusFade
..reactionHoverFade = reactionHoverFade
..inactiveReactionColor = effectiveInactivePressedOverlayColor
..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor
..splashRadius = widget.splashRadius ?? themeData.checkboxTheme.splashRadius ?? kRadialReactionRadius
..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered)
..activeColor = effectiveActiveColor
..inactiveColor = effectiveInactiveColor
..checkColor = effectiveCheckColor
..value = value
..previousValue = _previousValue
..shape = widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(1.0))
)
..side = widget.side ?? themeData.checkboxTheme.side,
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: Builder(
builder: (BuildContext context) {
return _CheckboxRenderObjectWidget(
value: widget.value,
tristate: widget.tristate,
activeColor: effectiveActiveColor,
checkColor: effectiveCheckColor,
inactiveColor: effectiveInactiveColor,
focusColor: effectiveFocusOverlayColor,
hoverColor: effectiveHoverOverlayColor,
reactionColor: effectiveActivePressedOverlayColor,
inactiveReactionColor: effectiveInactivePressedOverlayColor,
splashRadius: widget.splashRadius ?? themeData.checkboxTheme.splashRadius ?? kRadialReactionRadius,
onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: _focused,
hovering: _hovering,
side: widget.side ?? themeData.checkboxTheme.side,
shape: widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(1.0)),
),
);
},
),
);
}
}
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
const _CheckboxRenderObjectWidget({
Key? key,
required this.value,
required this.tristate,
required this.activeColor,
required this.checkColor,
required this.inactiveColor,
required this.focusColor,
required this.hoverColor,
required this.reactionColor,
required this.inactiveReactionColor,
required this.splashRadius,
required this.onChanged,
required this.vsync,
required this.additionalConstraints,
required this.hasFocus,
required this.hovering,
required this.shape,
required this.side,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null),
assert(inactiveColor != null),
assert(vsync != null),
super(key: key);
final bool? value;
final bool tristate;
final bool hasFocus;
final bool hovering;
final Color activeColor;
final Color checkColor;
final Color inactiveColor;
final Color focusColor;
final Color hoverColor;
final Color reactionColor;
final Color inactiveReactionColor;
final double splashRadius;
final ValueChanged<bool?>? onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
final OutlinedBorder shape;
final BorderSide? side;
@override
_RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox(
value: value,
tristate: tristate,
activeColor: activeColor,
checkColor: checkColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
vsync: vsync,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
shape: shape,
side: side,
);
@override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject
// The `tristate` must be changed before `value` due to the assertion at
// the beginning of `set value`.
..tristate = tristate
..value = value
..activeColor = activeColor
..checkColor = checkColor
..inactiveColor = inactiveColor
..focusColor = focusColor
..hoverColor = hoverColor
..reactionColor = reactionColor
..inactiveReactionColor = inactiveReactionColor
..splashRadius = splashRadius
..onChanged = onChanged
..additionalConstraints = additionalConstraints
..vsync = vsync
..hasFocus = hasFocus
..hovering = hovering
..shape = shape
..side = side;
}
}
const double _kEdgeSize = Checkbox.width;
const double _kStrokeWidth = 2.0;
class _CheckboxPainter extends ToggleablePainter {
Color get checkColor => _checkColor!;
Color? _checkColor;
set checkColor(Color value) {
if (_checkColor == value) {
class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({
bool? value,
required bool tristate,
required Color activeColor,
required this.checkColor,
required Color inactiveColor,
Color? focusColor,
Color? hoverColor,
Color? reactionColor,
Color? inactiveReactionColor,
required double splashRadius,
required BoxConstraints additionalConstraints,
ValueChanged<bool?>? onChanged,
required bool hasFocus,
required bool hovering,
required this.shape,
required this.side,
required TickerProvider vsync,
}) : _oldValue = value,
super(
value: value,
tristate: tristate,
activeColor: activeColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
vsync: vsync,
hasFocus: hasFocus,
hovering: hovering,
);
bool? _oldValue;
Color checkColor;
OutlinedBorder shape;
BorderSide? side;
@override
set value(bool? newValue) {
if (newValue == value)
return;
}
_checkColor = value;
notifyListeners();
_oldValue = value;
super.value = newValue;
}
bool? get value => _value;
bool? _value;
set value(bool? value) {
if (_value == value) {
return;
}
_value = value;
notifyListeners();
}
bool? get previousValue => _previousValue;
bool? _previousValue;
set previousValue(bool? value) {
if (_previousValue == value) {
return;
}
_previousValue = value;
notifyListeners();
}
OutlinedBorder get shape => _shape!;
OutlinedBorder? _shape;
set shape(OutlinedBorder value) {
if (_shape == value) {
return;
}
_shape = value;
notifyListeners();
}
BorderSide? get side => _side;
BorderSide? _side;
set side(BorderSide? value) {
if (_side == value) {
return;
}
_side = value;
notifyListeners();
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isChecked = value == true;
}
// The square outer bounds of the checkbox at t, with the specified origin.
@ -531,11 +644,10 @@ class _CheckboxPainter extends ToggleablePainter {
void _drawBorder(Canvas canvas, Rect outer, double t, Paint paint) {
assert(t >= 0.0 && t <= 0.5);
OutlinedBorder resolvedShape = shape;
if (side == null) {
resolvedShape = resolvedShape.copyWith(side: BorderSide(width: 2, color: paint.color));
shape = shape.copyWith(side: BorderSide(width: 2, color: paint.color));
}
resolvedShape.copyWith(side: side).paint(canvas, outer);
shape.copyWith(side: side).paint(canvas, outer);
}
void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
@ -574,18 +686,19 @@ class _CheckboxPainter extends ToggleablePainter {
}
@override
void paint(Canvas canvas, Size size) {
paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
paintRadialReaction(canvas, offset, size.center(Offset.zero));
final Paint strokePaint = _createStrokePaint();
final Offset origin = size / 2.0 - const Size.square(_kEdgeSize) / 2.0 as Offset;
final Offset origin = offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0 as Offset);
final AnimationStatus status = position.status;
final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed
? position.value
: 1.0 - position.value;
// Four cases: false to null, false to true, null to false, true to false
if (previousValue == false || value == false) {
if (_oldValue == false || value == false) {
final double t = value == false ? 1.0 - tNormalized : tNormalized;
final Rect outer = _outerRectAt(origin, t);
final Path emptyCheckboxPath = shape.copyWith(side: side).getOuterPath(outer);
@ -597,7 +710,7 @@ class _CheckboxPainter extends ToggleablePainter {
canvas.drawPath(emptyCheckboxPath, paint);
final double tShrink = (t - 0.5) * 2.0;
if (previousValue == null || value == null)
if (_oldValue == null || value == null)
_drawDash(canvas, origin, tShrink, strokePaint);
else
_drawCheck(canvas, origin, tShrink, strokePaint);
@ -609,7 +722,7 @@ class _CheckboxPainter extends ToggleablePainter {
if (tNormalized <= 0.5) {
final double tShrink = 1.0 - tNormalized * 2.0;
if (previousValue == true)
if (_oldValue == true)
_drawCheck(canvas, origin, tShrink, strokePaint);
else
_drawDash(canvas, origin, tShrink, strokePaint);

View file

@ -353,14 +353,45 @@ class Radio<T> extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
bool get _selected => value == groupValue;
@override
_RadioState<T> createState() => _RadioState<T>();
}
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, ToggleableStateMixin {
final _RadioPainter _painter = _RadioPainter();
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
late Map<Type, Action<Intent>> _actionMap;
@override
void initState() {
super.initState();
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: _actionHandler,
),
};
}
void _actionHandler(ActivateIntent intent) {
if (widget.onChanged != null) {
widget.onChanged!(widget.value);
}
final RenderObject renderObject = context.findRenderObject()!;
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
bool _focused = false;
void _handleHighlightChanged(bool focused) {
if (_focused != focused) {
setState(() { _focused = focused; });
}
}
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (_hovering != hovering) {
setState(() { _hovering = hovering; });
}
}
void _handleChanged(bool? selected) {
if (selected == null) {
@ -372,28 +403,14 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
}
}
@override
void didUpdateWidget(Radio<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget._selected != oldWidget._selected) {
animateToValue();
}
}
bool get _selected => widget.value == widget.groupValue;
@override
void dispose() {
_painter.dispose();
super.dispose();
}
@override
ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
@override
bool get tristate => widget.toggleable;
@override
bool? get value => widget._selected;
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) {
@ -440,17 +457,15 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
break;
}
size += effectiveVisualDensity.baseSizeAdjustment;
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? themeData.radioTheme.mouseCursor?.resolve(states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
});
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states)
?? themeData.radioTheme.mouseCursor?.resolve(_states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states);
// 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 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)
?? themeData.radioTheme.fillColor?.resolve(activeStates)
@ -460,13 +475,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
?? themeData.radioTheme.fillColor?.resolve(inactiveStates)
?? _defaultFillColor.resolve(inactiveStates);
final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
final Set<MaterialState> focusedStates = _states..add(MaterialState.focused);
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? themeData.radioTheme.overlayColor?.resolve(focusedStates)
?? themeData.focusColor;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
final Set<MaterialState> hoveredStates = _states..add(MaterialState.hovered);
final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor
?? themeData.radioTheme.overlayColor?.resolve(hoveredStates)
@ -482,40 +497,156 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
?? themeData.radioTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveColor.withAlpha(kRadialReactionAlpha);
return Semantics(
inMutuallyExclusiveGroup: true,
checked: widget._selected,
child: buildToggleable(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
mouseCursor: effectiveMouseCursor,
size: size,
painter: _painter
..position = position
..reaction = reaction
..reactionFocusFade = reactionFocusFade
..reactionHoverFade = reactionHoverFade
..inactiveReactionColor = effectiveInactivePressedOverlayColor
..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor
..splashRadius = widget.splashRadius ?? themeData.radioTheme.splashRadius ?? kRadialReactionRadius
..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered)
..activeColor = effectiveActiveColor
..inactiveColor = effectiveInactiveColor
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
mouseCursor: effectiveMouseCursor,
enabled: enabled,
onShowFocusHighlight: _handleHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
child: Builder(
builder: (BuildContext context) {
return _RadioRenderObjectWidget(
selected: _selected,
activeColor: effectiveActiveColor,
inactiveColor: effectiveInactiveColor,
focusColor: effectiveFocusOverlayColor,
hoverColor: effectiveHoverOverlayColor,
reactionColor: effectiveActivePressedOverlayColor,
inactiveReactionColor: effectiveInactivePressedOverlayColor,
splashRadius: widget.splashRadius ?? themeData.radioTheme.splashRadius ?? kRadialReactionRadius,
onChanged: enabled ? _handleChanged : null,
toggleable: widget.toggleable,
additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: _focused,
hovering: _hovering,
);
},
),
);
}
}
class _RadioPainter extends ToggleablePainter {
@override
void paint(Canvas canvas, Size size) {
paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));
class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
const _RadioRenderObjectWidget({
Key? key,
required this.selected,
required this.activeColor,
required this.inactiveColor,
required this.focusColor,
required this.hoverColor,
required this.reactionColor,
required this.inactiveReactionColor,
required this.additionalConstraints,
this.onChanged,
required this.toggleable,
required this.vsync,
required this.hasFocus,
required this.hovering,
required this.splashRadius,
}) : assert(selected != null),
assert(activeColor != null),
assert(inactiveColor != null),
assert(vsync != null),
assert(toggleable != null),
super(key: key);
final Offset center = (Offset.zero & size).center;
final bool selected;
final bool hasFocus;
final bool hovering;
final Color inactiveColor;
final Color activeColor;
final Color focusColor;
final Color hoverColor;
final Color reactionColor;
final Color inactiveReactionColor;
final double splashRadius;
final ValueChanged<bool?>? onChanged;
final bool toggleable;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
@override
_RenderRadio createRenderObject(BuildContext context) => _RenderRadio(
value: selected,
activeColor: activeColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
tristate: toggleable,
vsync: vsync,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
);
@override
void updateRenderObject(BuildContext context, _RenderRadio renderObject) {
renderObject
..value = selected
..activeColor = activeColor
..inactiveColor = inactiveColor
..focusColor = focusColor
..hoverColor = hoverColor
..reactionColor = reactionColor
..inactiveReactionColor = inactiveReactionColor
..splashRadius = splashRadius
..onChanged = onChanged
..tristate = toggleable
..additionalConstraints = additionalConstraints
..vsync = vsync
..hasFocus = hasFocus
..hovering = hovering;
}
}
class _RenderRadio extends RenderToggleable {
_RenderRadio({
required bool value,
required Color activeColor,
required Color inactiveColor,
required Color focusColor,
required Color hoverColor,
required Color reactionColor,
required Color inactiveReactionColor,
required double splashRadius,
required ValueChanged<bool?>? onChanged,
required bool tristate,
required BoxConstraints additionalConstraints,
required TickerProvider vsync,
required bool hasFocus,
required bool hovering,
}) : super(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
tristate: tristate,
additionalConstraints: additionalConstraints,
vsync: vsync,
hasFocus: hasFocus,
hovering: hovering,
);
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
paintRadialReaction(canvas, offset, size.center(Offset.zero));
final Offset center = (offset & size).center;
// Outer circle
final Paint paint = Paint()
@ -530,4 +661,12 @@ class _RadioPainter extends ToggleablePainter {
canvas.drawCircle(center, _kInnerRadius * position.value, paint);
}
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isInMutuallyExclusiveGroup = true
..isChecked = value == true;
}
}

View file

@ -51,7 +51,7 @@ enum _SwitchType { material, adaptive }
/// * [Radio], for selecting among a set of explicit values.
/// * [Slider], for selecting a value in a range.
/// * <https://material.io/design/components/selection-controls.html#switches>
class Switch extends StatelessWidget {
class Switch extends StatefulWidget {
/// Creates a material design switch.
///
/// The switch itself does not maintain any state. Instead, when the state of
@ -359,8 +359,52 @@ class Switch extends StatelessWidget {
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
Size _getSwitchSize(ThemeData theme) {
final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
@override
_SwitchState createState() => _SwitchState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
}
}
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
late Map<Type, Action<Intent>> _actionMap;
@override
void initState() {
super.initState();
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
};
}
void _actionHandler(ActivateIntent intent) {
if (widget.onChanged != null) {
widget.onChanged!(!widget.value);
}
final RenderObject renderObject = context.findRenderObject()!;
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
setState(() { _focused = focused; });
}
}
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
}
}
Size getSwitchSize(ThemeData theme) {
final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize
?? theme.switchTheme.materialTapTargetSize
?? theme.materialTapTargetSize;
switch (effectiveMaterialTapTargetSize) {
@ -371,173 +415,20 @@ class Switch extends StatelessWidget {
}
}
Widget _buildCupertinoSwitch(BuildContext context) {
final Size size = _getSwitchSize(Theme.of(context));
return Focus(
focusNode: focusNode,
autofocus: autofocus,
child: Container(
width: size.width, // Same size as the Material switch.
height: size.height,
alignment: Alignment.center,
child: CupertinoSwitch(
dragStartBehavior: dragStartBehavior,
value: value,
onChanged: onChanged,
activeColor: activeColor,
trackColor: inactiveTrackColor
),
),
);
bool get enabled => widget.onChanged != null;
void _didFinishDragging() {
// The user has finished dragging the thumb of this switch. Rebuild the switch
// to update the animation.
setState(() {});
}
Widget _buildMaterialSwitch(BuildContext context) {
return _MaterialSwitch(
value: value,
onChanged: onChanged,
size: _getSwitchSize(Theme.of(context)),
activeColor: activeColor,
activeTrackColor: activeTrackColor,
inactiveThumbColor: inactiveThumbColor,
inactiveTrackColor: inactiveTrackColor,
activeThumbImage: activeThumbImage,
onActiveThumbImageError: onActiveThumbImageError,
inactiveThumbImage: inactiveThumbImage,
onInactiveThumbImageError: onInactiveThumbImageError,
thumbColor: thumbColor,
trackColor: trackColor,
materialTapTargetSize: materialTapTargetSize,
dragStartBehavior: dragStartBehavior,
mouseCursor: mouseCursor,
focusColor: focusColor,
hoverColor: hoverColor,
overlayColor: overlayColor,
splashRadius: splashRadius,
focusNode: focusNode,
autofocus: autofocus,
);
}
@override
Widget build(BuildContext context) {
switch (_switchType) {
case _SwitchType.material:
return _buildMaterialSwitch(context);
case _SwitchType.adaptive: {
final ThemeData theme = Theme.of(context);
assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _buildMaterialSwitch(context);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _buildCupertinoSwitch(context);
}
}
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
}
}
class _MaterialSwitch extends StatefulWidget {
const _MaterialSwitch({
Key? key,
required this.value,
required this.onChanged,
required this.size,
this.activeColor,
this.activeTrackColor,
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.thumbColor,
this.trackColor,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.focusNode,
this.autofocus = false,
}) : assert(dragStartBehavior != null),
assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
super(key: key);
final bool value;
final ValueChanged<bool>? onChanged;
final Color? activeColor;
final Color? activeTrackColor;
final Color? inactiveThumbColor;
final Color? inactiveTrackColor;
final ImageProvider? activeThumbImage;
final ImageErrorListener? onActiveThumbImageError;
final ImageProvider? inactiveThumbImage;
final ImageErrorListener? onInactiveThumbImageError;
final MaterialStateProperty<Color?>? thumbColor;
final MaterialStateProperty<Color?>? trackColor;
final MaterialTapTargetSize? materialTapTargetSize;
final DragStartBehavior dragStartBehavior;
final MouseCursor? mouseCursor;
final Color? focusColor;
final Color? hoverColor;
final MaterialStateProperty<Color?>? overlayColor;
final double? splashRadius;
final FocusNode? focusNode;
final bool autofocus;
final Size size;
@override
State<StatefulWidget> createState() => _MaterialSwitchState();
}
class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderStateMixin, ToggleableStateMixin {
final _SwitchPainter _painter = _SwitchPainter();
@override
void didUpdateWidget(_MaterialSwitch oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
// During a drag we may have modified the curve, reset it if its possible
// to do without visual discontinuation.
if (position.value == 0.0 || position.value == 1.0) {
position
..curve = Curves.easeIn
..reverseCurve = Curves.easeOut;
}
animateToValue();
}
}
@override
void dispose() {
_painter.dispose();
super.dispose();
}
@override
ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
@override
bool get tristate => false;
@override
bool? get value => widget.value;
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) {
@ -596,68 +487,14 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
});
}
double get _trackInnerLength => widget.size.width - _kSwitchMinSize;
void _handleDragStart(DragStartDetails details) {
if (isInteractive)
reactionController.forward();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
position
..curve = Curves.linear
..reverseCurve = null;
final double delta = details.primaryDelta! / _trackInnerLength;
switch (Directionality.of(context)) {
case TextDirection.rtl:
positionController.value -= delta;
break;
case TextDirection.ltr:
positionController.value += delta;
break;
}
}
}
bool _needsPositionAnimation = false;
void _handleDragEnd(DragEndDetails details) {
if (position.value >= 0.5 != widget.value) {
widget.onChanged!(!widget.value);
// Wait with finishing the animation until widget.value has changed to
// !widget.value as part of the widget.onChanged call above.
setState(() {
_needsPositionAnimation = true;
});
} else {
animateToValue();
}
reactionController.reverse();
}
void _handleChanged(bool? value) {
assert(value != null);
assert(widget.onChanged != null);
widget.onChanged!(value!);
}
@override
Widget build(BuildContext context) {
Widget buildMaterialSwitch(BuildContext context) {
assert(debugCheckHasMaterial(context));
if (_needsPositionAnimation) {
_needsPositionAnimation = false;
animateToValue();
}
final ThemeData theme = Theme.of(context);
// 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 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)
?? theme.switchTheme.thumbColor?.resolve(activeStates)
@ -675,13 +512,14 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
?? theme.switchTheme.trackColor?.resolve(inactiveStates)
?? _defaultTrackColor.resolve(inactiveStates);
final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
final Set<MaterialState> focusedStates = _states..add(MaterialState.focused);
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? theme.switchTheme.overlayColor?.resolve(focusedStates)
?? theme.focusColor;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
final Set<MaterialState> hoveredStates = _states..add(MaterialState.hovered);
final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor
?? theme.switchTheme.overlayColor?.resolve(hoveredStates)
@ -697,65 +535,277 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
?? theme.switchTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? theme.switchTheme.mouseCursor?.resolve(states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
});
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states)
?? theme.switchTheme.mouseCursor?.resolve(_states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states);
return Semantics(
toggled: widget.value,
child: GestureDetector(
excludeFromSemantics: true,
onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd,
dragStartBehavior: widget.dragStartBehavior,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
size: widget.size,
painter: _painter
..position = position
..reaction = reaction
..reactionFocusFade = reactionFocusFade
..reactionHoverFade = reactionHoverFade
..inactiveReactionColor = effectiveInactivePressedOverlayColor
..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor
..splashRadius = widget.splashRadius ?? theme.switchTheme.splashRadius ?? kRadialReactionRadius
..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered)
..activeColor = effectiveActiveThumbColor
..inactiveColor = effectiveInactiveThumbColor
..activeThumbImage = widget.activeThumbImage
..onActiveThumbImageError = widget.onActiveThumbImageError
..inactiveThumbImage = widget.inactiveThumbImage
..onInactiveThumbImageError = widget.onInactiveThumbImageError
..activeTrackColor = effectiveActiveTrackColor
..inactiveTrackColor = effectiveInactiveTrackColor
..configuration = createLocalImageConfiguration(context)
..isInteractive = isInteractive
..trackInnerLength = _trackInnerLength
..textDirection = Directionality.of(context)
..surfaceColor = theme.colorScheme.surface
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: Builder(
builder: (BuildContext context) {
return _SwitchRenderObjectWidget(
dragStartBehavior: widget.dragStartBehavior,
value: widget.value,
activeColor: effectiveActiveThumbColor,
inactiveColor: effectiveInactiveThumbColor,
surfaceColor: theme.colorScheme.surface,
focusColor: effectiveFocusOverlayColor,
hoverColor: effectiveHoverOverlayColor,
reactionColor: effectiveActivePressedOverlayColor,
inactiveReactionColor: effectiveInactivePressedOverlayColor,
splashRadius: widget.splashRadius ?? theme.switchTheme.splashRadius ?? kRadialReactionRadius,
activeThumbImage: widget.activeThumbImage,
onActiveThumbImageError: widget.onActiveThumbImageError,
inactiveThumbImage: widget.inactiveThumbImage,
onInactiveThumbImageError: widget.onInactiveThumbImageError,
activeTrackColor: effectiveActiveTrackColor,
inactiveTrackColor: effectiveInactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged,
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
hasFocus: _focused,
hovering: _hovering,
state: this,
);
},
),
);
}
Widget buildCupertinoSwitch(BuildContext context) {
final Size size = getSwitchSize(Theme.of(context));
return Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
child: Container(
width: size.width, // Same size as the Material switch.
height: size.height,
alignment: Alignment.center,
child: CupertinoSwitch(
dragStartBehavior: widget.dragStartBehavior,
value: widget.value,
onChanged: widget.onChanged,
activeColor: widget.activeColor,
trackColor: widget.inactiveTrackColor
),
),
);
}
@override
Widget build(BuildContext context) {
switch (widget._switchType) {
case _SwitchType.material:
return buildMaterialSwitch(context);
case _SwitchType.adaptive: {
final ThemeData theme = Theme.of(context);
assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return buildMaterialSwitch(context);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return buildCupertinoSwitch(context);
}
}
}
}
}
class _SwitchPainter extends ToggleablePainter {
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
const _SwitchRenderObjectWidget({
Key? key,
required this.value,
required this.activeColor,
required this.inactiveColor,
required this.hoverColor,
required this.focusColor,
required this.reactionColor,
required this.inactiveReactionColor,
required this.splashRadius,
required this.activeThumbImage,
required this.onActiveThumbImageError,
required this.inactiveThumbImage,
required this.onInactiveThumbImageError,
required this.activeTrackColor,
required this.inactiveTrackColor,
required this.configuration,
required this.onChanged,
required this.additionalConstraints,
required this.dragStartBehavior,
required this.hasFocus,
required this.hovering,
required this.state,
required this.surfaceColor,
}) : super(key: key);
final bool value;
final Color activeColor;
final Color inactiveColor;
final Color hoverColor;
final Color focusColor;
final Color reactionColor;
final Color inactiveReactionColor;
final double splashRadius;
final ImageProvider? activeThumbImage;
final ImageErrorListener? onActiveThumbImageError;
final ImageProvider? inactiveThumbImage;
final ImageErrorListener? onInactiveThumbImageError;
final Color activeTrackColor;
final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool>? onChanged;
final BoxConstraints additionalConstraints;
final DragStartBehavior dragStartBehavior;
final bool hasFocus;
final bool hovering;
final _SwitchState state;
final Color surfaceColor;
@override
_RenderSwitch createRenderObject(BuildContext context) {
return _RenderSwitch(
dragStartBehavior: dragStartBehavior,
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
hoverColor: hoverColor,
focusColor: focusColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
activeThumbImage: activeThumbImage,
onActiveThumbImageError: onActiveThumbImageError,
inactiveThumbImage: inactiveThumbImage,
onInactiveThumbImageError: onInactiveThumbImageError,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
onChanged: onChanged != null ? _handleValueChanged : null,
textDirection: Directionality.of(context),
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
state: state,
surfaceColor: surfaceColor,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
renderObject
..value = value
..activeColor = activeColor
..inactiveColor = inactiveColor
..hoverColor = hoverColor
..focusColor = focusColor
..reactionColor = reactionColor
..inactiveReactionColor = inactiveReactionColor
..splashRadius = splashRadius
..activeThumbImage = activeThumbImage
..onActiveThumbImageError = onActiveThumbImageError
..inactiveThumbImage = inactiveThumbImage
..onInactiveThumbImageError = onInactiveThumbImageError
..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
..onChanged = onChanged != null ? _handleValueChanged : null
..textDirection = Directionality.of(context)
..additionalConstraints = additionalConstraints
..dragStartBehavior = dragStartBehavior
..hasFocus = hasFocus
..hovering = hovering
..vsync = state
..surfaceColor = surfaceColor;
}
void _handleValueChanged(bool? value) {
// Wrap the onChanged callback because the RenderToggleable supports tri-state
// values (i.e. value can be null), but the Switch doesn't. We pass false
// for the tristate param to RenderToggleable, so value should never
// be null.
assert(value != null);
if (onChanged != null) {
onChanged!(value!);
}
}
}
class _RenderSwitch extends RenderToggleable {
_RenderSwitch({
required bool value,
required Color activeColor,
required Color inactiveColor,
required Color hoverColor,
required Color focusColor,
required Color reactionColor,
required Color inactiveReactionColor,
required double splashRadius,
required ImageProvider? activeThumbImage,
required ImageErrorListener? onActiveThumbImageError,
required ImageProvider? inactiveThumbImage,
required ImageErrorListener? onInactiveThumbImageError,
required Color activeTrackColor,
required Color inactiveTrackColor,
required ImageConfiguration configuration,
required BoxConstraints additionalConstraints,
required TextDirection textDirection,
required ValueChanged<bool?>? onChanged,
required DragStartBehavior dragStartBehavior,
required bool hasFocus,
required bool hovering,
required this.state,
required Color surfaceColor,
}) : assert(textDirection != null),
_activeThumbImage = activeThumbImage,
_onActiveThumbImageError = onActiveThumbImageError,
_inactiveThumbImage = inactiveThumbImage,
_onInactiveThumbImageError = onInactiveThumbImageError,
_activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor,
_configuration = configuration,
_textDirection = textDirection,
_surfaceColor = surfaceColor,
super(
value: value,
tristate: false,
activeColor: activeColor,
inactiveColor: inactiveColor,
hoverColor: hoverColor,
focusColor: focusColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
vsync: state,
) {
_drag = HorizontalDragGestureRecognizer()
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..dragStartBehavior = dragStartBehavior;
}
ImageProvider? get activeThumbImage => _activeThumbImage;
ImageProvider? _activeThumbImage;
set activeThumbImage(ImageProvider? value) {
if (value == _activeThumbImage)
return;
_activeThumbImage = value;
notifyListeners();
markNeedsPaint();
}
ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError;
@ -765,7 +815,7 @@ class _SwitchPainter extends ToggleablePainter {
return;
}
_onActiveThumbImageError = value;
notifyListeners();
markNeedsPaint();
}
ImageProvider? get inactiveThumbImage => _inactiveThumbImage;
@ -774,7 +824,7 @@ class _SwitchPainter extends ToggleablePainter {
if (value == _inactiveThumbImage)
return;
_inactiveThumbImage = value;
notifyListeners();
markNeedsPaint();
}
ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError;
@ -784,77 +834,132 @@ class _SwitchPainter extends ToggleablePainter {
return;
}
_onInactiveThumbImageError = value;
notifyListeners();
markNeedsPaint();
}
Color get activeTrackColor => _activeTrackColor!;
Color? _activeTrackColor;
Color get activeTrackColor => _activeTrackColor;
Color _activeTrackColor;
set activeTrackColor(Color value) {
assert(value != null);
if (value == _activeTrackColor)
return;
_activeTrackColor = value;
notifyListeners();
markNeedsPaint();
}
Color get inactiveTrackColor => _inactiveTrackColor!;
Color? _inactiveTrackColor;
Color get inactiveTrackColor => _inactiveTrackColor;
Color _inactiveTrackColor;
set inactiveTrackColor(Color value) {
assert(value != null);
if (value == _inactiveTrackColor)
return;
_inactiveTrackColor = value;
notifyListeners();
markNeedsPaint();
}
ImageConfiguration get configuration => _configuration!;
ImageConfiguration? _configuration;
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
assert(value != null);
if (value == _configuration)
return;
_configuration = value;
notifyListeners();
markNeedsPaint();
}
TextDirection get textDirection => _textDirection!;
TextDirection? _textDirection;
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection == value)
return;
_textDirection = value;
notifyListeners();
markNeedsPaint();
}
Color get surfaceColor => _surfaceColor!;
Color? _surfaceColor;
DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
set dragStartBehavior(DragStartBehavior value) {
assert(value != null);
if (_drag.dragStartBehavior == value)
return;
_drag.dragStartBehavior = value;
}
Color get surfaceColor => _surfaceColor;
Color _surfaceColor;
set surfaceColor(Color value) {
assert(value != null);
if (value == _surfaceColor)
return;
_surfaceColor = value;
notifyListeners();
markNeedsPaint();
}
bool get isInteractive => _isInteractive!;
bool? _isInteractive;
set isInteractive(bool value) {
if (value == _isInteractive) {
return;
_SwitchState state;
@override
set value(bool? newValue) {
assert(value != null);
super.value = newValue;
// The widget is rebuilt and we have pending position animation to play.
if (_needsPositionAnimation) {
_needsPositionAnimation = false;
position.reverseCurve = null;
if (newValue!)
positionController.forward();
else
positionController.reverse();
}
_isInteractive = value;
notifyListeners();
}
double get trackInnerLength => _trackInnerLength!;
double? _trackInnerLength;
set trackInnerLength(double value) {
if (value == _trackInnerLength) {
return;
@override
void detach() {
_cachedThumbPainter?.dispose();
_cachedThumbPainter = null;
super.detach();
}
double get _trackInnerLength => size.width - _kSwitchMinSize;
late HorizontalDragGestureRecognizer _drag;
bool _needsPositionAnimation = false;
void _handleDragStart(DragStartDetails details) {
if (isInteractive)
reactionController.forward();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
position.reverseCurve = null;
final double delta = details.primaryDelta! / _trackInnerLength;
switch (textDirection) {
case TextDirection.rtl:
positionController.value -= delta;
break;
case TextDirection.ltr:
positionController.value += delta;
break;
}
}
_trackInnerLength = value;
notifyListeners();
}
void _handleDragEnd(DragEndDetails details) {
_needsPositionAnimation = true;
if (position.value >= 0.5 != value)
onChanged!(!value!);
reactionController.reverse();
state._didFinishDragging();
}
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && onChanged != null)
_drag.addPointer(event);
super.handleEvent(event, entry);
}
Color? _cachedThumbColor;
@ -879,12 +984,19 @@ class _SwitchPainter extends ToggleablePainter {
// are already in the middle of painting. (In fact, doing so would trigger
// an assert).
if (!_isPainting)
notifyListeners();
markNeedsPaint();
}
@override
void paint(Canvas canvas, Size size) {
final bool isEnabled = isInteractive;
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isToggled = value == true;
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final bool isEnabled = onChanged != null;
final double currentValue = position.value;
final double visualPosition;
@ -917,8 +1029,8 @@ class _SwitchPainter extends ToggleablePainter {
..color = trackColor;
const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
final Rect trackRect = Rect.fromLTWH(
trackHorizontalPadding,
(size.height - _kTrackHeight) / 2.0,
offset.dx + trackHorizontalPadding,
offset.dy + (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight,
);
@ -926,11 +1038,11 @@ class _SwitchPainter extends ToggleablePainter {
canvas.drawRRect(trackRRect, paint);
final Offset thumbPosition = Offset(
kRadialReactionRadius + visualPosition * trackInnerLength,
kRadialReactionRadius + visualPosition * _trackInnerLength,
size.height / 2.0,
);
paintRadialReaction(canvas: canvas, origin: thumbPosition);
paintRadialReaction(canvas, offset, thumbPosition);
try {
_isPainting = true;
@ -947,7 +1059,7 @@ class _SwitchPainter extends ToggleablePainter {
final double radius = _kThumbRadius - inset;
thumbPainter.paint(
canvas,
thumbPosition - Offset(radius, radius),
thumbPosition + offset - Offset(radius, radius),
configuration.copyWith(size: Size.fromRadius(radius)),
);
} finally {

View file

@ -6,10 +6,9 @@ import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/scheduler.dart';
import 'constants.dart';
import 'material_state.dart';
// Duration of the animation that moves the toggle from one state to another.
const Duration _kToggleDuration = Duration(milliseconds: 200);
@ -17,21 +16,92 @@ const Duration _kToggleDuration = Duration(milliseconds: 200);
// Duration of the fade animation for the reaction when focus and hover occur.
const Duration _kReactionFadeDuration = Duration(milliseconds: 50);
/// A mixin for [StatefulWidget]s that implement material-themed toggleable
/// controls with toggle animations (e.g. [Switch]es, [Checkbox]es, and
/// [Radio]s).
/// A base class for material style toggleable controls with toggle animations.
///
/// The mixin implements the logic for toggling the control (e.g. when tapped)
/// and provides a series of animation controllers to transition the control
/// from one state to another. It does not have any opinion about the visual
/// representation of the toggleable widget. The visuals are defined by a
/// [CustomPainter] passed to the [buildToggleable]. [State] objects using this
/// mixin should call that method from their [build] method.
///
/// This mixin is used to implement the material components for [Switch],
/// [Checkbox], and [Radio] controls.
@optionalTypeArgs
mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin<S> {
/// This class handles storing the current value, dispatching ValueChanged on a
/// tap gesture and driving a changed animation. Subclasses are responsible for
/// painting.
abstract class RenderToggleable extends RenderConstrainedBox {
/// Creates a toggleable render object.
///
/// The [activeColor], and [inactiveColor] arguments must not be
/// null. The [value] can only be null if tristate is true.
RenderToggleable({
required bool? value,
bool tristate = false,
required Color activeColor,
required Color inactiveColor,
Color? hoverColor,
Color? focusColor,
Color? reactionColor,
Color? inactiveReactionColor,
required double splashRadius,
ValueChanged<bool?>? onChanged,
required BoxConstraints additionalConstraints,
required TickerProvider vsync,
bool hasFocus = false,
bool hovering = false,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null),
assert(inactiveColor != null),
assert(vsync != null),
_value = value,
_tristate = tristate,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_hoverColor = hoverColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_focusColor = focusColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_reactionColor = reactionColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_inactiveReactionColor = inactiveReactionColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_splashRadius = splashRadius,
_onChanged = onChanged,
_hasFocus = hasFocus,
_hovering = hovering,
_vsync = vsync,
super(additionalConstraints: additionalConstraints) {
_tap = TapGestureRecognizer()
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapUp = _handleTapUp
..onTapCancel = _handleTapCancel;
_positionController = AnimationController(
duration: _kToggleDuration,
value: value == false ? 0.0 : 1.0,
vsync: vsync,
);
_position = CurvedAnimation(
parent: _positionController,
curve: Curves.linear,
)..addListener(markNeedsPaint);
_reactionController = AnimationController(
duration: kRadialReactionDuration,
vsync: vsync,
);
_reaction = CurvedAnimation(
parent: _reactionController,
curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint);
_reactionHoverFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: hovering || hasFocus ? 1.0 : 0.0,
vsync: vsync,
);
_reactionHoverFade = CurvedAnimation(
parent: _reactionHoverFadeController,
curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint);
_reactionFocusFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: hovering || hasFocus ? 1.0 : 0.0,
vsync: vsync,
);
_reactionFocusFade = CurvedAnimation(
parent: _reactionFocusFadeController,
curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint);
}
/// Used by subclasses to manipulate the visual value of the control.
///
/// Some controls respond to user input by updating their visual value. For
@ -39,15 +109,16 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
/// dragged. These controls manipulate this animation controller to update
/// their [position] and eventually trigger an [onChanged] callback when the
/// animation reaches either 0.0 or 1.0.
@protected
AnimationController get positionController => _positionController;
late AnimationController _positionController;
/// The visual value of the control.
///
/// When the control is inactive, the [value] is false and this animation has
/// the value 0.0. When the control is active, the value is either true or
/// tristate is true and the value is null. When the control is active the
/// animation has a value of 1.0. When the control is changing from inactive
/// the value 0.0. When the control is active, the value either true or tristate
/// is true and the value is null. When the control is active the animation
/// has a value of 1.0. When the control is changing from inactive
/// to active (or vice versa), [value] is the target value and this animation
/// gradually updates from 0.0 to 1.0 (or vice versa).
CurvedAnimation get position => _position;
@ -58,66 +129,84 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
/// Some controls have a radial ink reaction to user input. This animation
/// controller can be used to start or stop these ink reactions.
///
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
/// may be used.
/// Subclasses should call [paintRadialReaction] to actually paint the radial
/// reaction.
@protected
AnimationController get reactionController => _reactionController;
late AnimationController _reactionController;
/// The visual value of the radial reaction animation.
///
/// Some controls have a radial ink reaction to user input. This animation
/// controls the progress of these ink reactions.
///
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
/// may be used.
Animation<double> get reaction => _reaction;
late Animation<double> _reaction;
/// Controls the radial reaction's opacity animation for hover changes.
///
/// Some controls have a radial ink reaction to pointer hover. This animation
/// controls these ink reaction fade-ins and
/// fade-outs.
///
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
/// may be used.
Animation<double> get reactionHoverFade => _reactionHoverFade;
late Animation<double> _reactionHoverFade;
late AnimationController _reactionHoverFadeController;
/// Controls the radial reaction's opacity animation for focus changes.
/// Used by subclasses to control the radial reaction's opacity animation for
/// [hasFocus] changes.
///
/// Some controls have a radial ink reaction to focus. This animation
/// controls these ink reaction fade-ins and fade-outs.
/// controller can be used to start or stop these ink reaction fade-ins and
/// fade-outs.
///
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
/// may be used.
Animation<double> get reactionFocusFade => _reactionFocusFade;
late Animation<double> _reactionFocusFade;
/// Subclasses should call [paintRadialReaction] to actually paint the radial
/// reaction.
@protected
AnimationController get reactionFocusFadeController => _reactionFocusFadeController;
late AnimationController _reactionFocusFadeController;
late Animation<double> _reactionFocusFade;
/// Whether [value] of this control can be changed by user interaction.
/// Used by subclasses to control the radial reaction's opacity animation for
/// [hovering] changes.
///
/// The control is considered interactive if the [onChanged] callback is
/// non-null. If the callback is null, then the control is disabled, and
/// non-interactive. A disabled checkbox, for example, is displayed using a
/// grey color and its value cannot be changed.
bool get isInteractive => onChanged != null;
/// Some controls have a radial ink reaction to pointer hover. This animation
/// controller can be used to start or stop these ink reaction fade-ins and
/// fade-outs.
///
/// Subclasses should call [paintRadialReaction] to actually paint the radial
/// reaction.
@protected
AnimationController get reactionHoverFadeController => _reactionHoverFadeController;
late AnimationController _reactionHoverFadeController;
late Animation<double> _reactionHoverFade;
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap),
};
/// True if this toggleable has the input focus.
bool get hasFocus => _hasFocus;
bool _hasFocus;
set hasFocus(bool value) {
assert(value != null);
if (value == _hasFocus)
return;
_hasFocus = value;
if (_hasFocus) {
_reactionFocusFadeController.forward();
} else {
_reactionFocusFadeController.reverse();
}
markNeedsPaint();
}
/// Called when the control changes value.
///
/// If the control is tapped, [onChanged] is called immediately with the new
/// value.
///
/// The control is considered interactive (see [isInteractive]) if this
/// callback is non-null. If the callback is null, then the control is
/// disabled, and non-interactive. A disabled checkbox, for example, is
/// displayed using a grey color and its value cannot be changed.
ValueChanged<bool?>? get onChanged;
/// True if this toggleable is being hovered over by a pointer.
bool get hovering => _hovering;
bool _hovering;
set hovering(bool value) {
assert(value != null);
if (value == _hovering)
return;
_hovering = value;
if (_hovering) {
_reactionHoverFadeController.forward();
} else {
_reactionHoverFadeController.reverse();
}
markNeedsPaint();
}
/// The [TickerProvider] for the [AnimationController]s that run the animations.
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
assert(value != null);
if (value == _vsync)
return;
_vsync = value;
positionController.resync(vsync);
reactionController.resync(vsync);
}
/// False if this control is "inactive" (not checked, off, or unselected).
///
@ -128,62 +217,17 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
/// When the value changes, this object starts the [positionController] and
/// [position] animations to animate the visual appearance of the control to
/// the new value.
bool? get value;
/// If true, [value] can be true, false, or null, otherwise [value] must
/// be true or false.
///
/// When [tristate] is true and [value] is null, then the control is
/// considered to be in its third or "indeterminate" state.
bool get tristate;
@override
void initState() {
super.initState();
_positionController = AnimationController(
duration: _kToggleDuration,
value: value == false ? 0.0 : 1.0,
vsync: this,
);
_position = CurvedAnimation(
parent: _positionController,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
);
_reactionController = AnimationController(
duration: kRadialReactionDuration,
vsync: this,
);
_reaction = CurvedAnimation(
parent: _reactionController,
curve: Curves.fastOutSlowIn,
);
_reactionHoverFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: _hovering || _focused ? 1.0 : 0.0,
vsync: this,
);
_reactionHoverFade = CurvedAnimation(
parent: _reactionHoverFadeController,
curve: Curves.fastOutSlowIn,
);
_reactionFocusFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: _hovering || _focused ? 1.0 : 0.0,
vsync: this,
);
_reactionFocusFade = CurvedAnimation(
parent: _reactionFocusFadeController,
curve: Curves.fastOutSlowIn,
);
}
/// Runs the [position] animation to transition the Toggleable's appearance
/// to match [value].
///
/// This method must be called whenever [value] changes to ensure that the
/// visual representation of the Toggleable matches the current [value].
void animateToValue() {
bool? get value => _value;
bool? _value;
set value(bool? value) {
assert(tristate || value != null);
if (value == _value)
return;
_value = value;
markNeedsSemanticsUpdate();
_position
..curve = Curves.easeIn
..reverseCurve = Curves.easeOut;
if (tristate) {
if (value == null)
_positionController.value = 0.0;
@ -199,32 +243,196 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
}
}
@override
void dispose() {
_positionController.dispose();
_reactionController.dispose();
_reactionHoverFadeController.dispose();
_reactionFocusFadeController.dispose();
super.dispose();
/// If true, [value] can be true, false, or null, otherwise [value] must
/// be true or false.
///
/// When [tristate] is true and [value] is null, then the control is
/// considered to be in its third or "indeterminate" state.
bool get tristate => _tristate;
bool _tristate;
set tristate(bool value) {
assert(tristate != null);
if (value == _tristate)
return;
_tristate = value;
markNeedsSemanticsUpdate();
}
/// The most recent [Offset] at which a pointer touched the Toggleable.
/// The color that should be used in the active state (i.e., when [value] is true).
///
/// This is null if currently no pointer is touching the Toggleable or if
/// [isInteractive] is false.
Offset? get downPosition => _downPosition;
/// For example, a checkbox should use this color when checked.
Color get activeColor => _activeColor;
Color _activeColor;
set activeColor(Color value) {
assert(value != null);
if (value == _activeColor)
return;
_activeColor = value;
markNeedsPaint();
}
/// The color that should be used in the inactive state (i.e., when [value] is false).
///
/// For example, a checkbox should use this color when unchecked.
Color get inactiveColor => _inactiveColor;
Color _inactiveColor;
set inactiveColor(Color value) {
assert(value != null);
if (value == _inactiveColor)
return;
_inactiveColor = value;
markNeedsPaint();
}
/// The color that should be used for the reaction when [hovering] is true.
///
/// Used when the toggleable needs to change the reaction color/transparency,
/// when it is being hovered over.
///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color get hoverColor => _hoverColor;
Color _hoverColor;
set hoverColor(Color value) {
assert(value != null);
if (value == _hoverColor)
return;
_hoverColor = value;
markNeedsPaint();
}
/// The color that should be used for the reaction when [hasFocus] is true.
///
/// Used when the toggleable needs to change the reaction color/transparency,
/// when it has focus.
///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color get focusColor => _focusColor;
Color _focusColor;
set focusColor(Color value) {
assert(value != null);
if (value == _focusColor)
return;
_focusColor = value;
markNeedsPaint();
}
/// The color that should be used for the reaction when the toggleable is
/// active.
///
/// Used when the toggleable needs to change the reaction color/transparency
/// that is displayed when the toggleable is active and tapped.
///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color? get reactionColor => _reactionColor;
Color? _reactionColor;
set reactionColor(Color? value) {
assert(value != null);
if (value == _reactionColor)
return;
_reactionColor = value;
markNeedsPaint();
}
/// The color that should be used for the reaction when the toggleable is
/// inactive.
///
/// Used when the toggleable needs to change the reaction color/transparency
/// that is displayed when the toggleable is inactive and tapped.
///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color? get inactiveReactionColor => _inactiveReactionColor;
Color? _inactiveReactionColor;
set inactiveReactionColor(Color? value) {
assert(value != null);
if (value == _inactiveReactionColor)
return;
_inactiveReactionColor = value;
markNeedsPaint();
}
/// The splash radius for the radial reaction.
double get splashRadius => _splashRadius;
double _splashRadius;
set splashRadius(double value) {
if (value == _splashRadius)
return;
_splashRadius = value;
markNeedsPaint();
}
/// Called when the control changes value.
///
/// If the control is tapped, [onChanged] is called immediately with the new
/// value.
///
/// The control is considered interactive (see [isInteractive]) if this
/// callback is non-null. If the callback is null, then the control is
/// disabled, and non-interactive. A disabled checkbox, for example, is
/// displayed using a grey color and its value cannot be changed.
ValueChanged<bool?>? get onChanged => _onChanged;
ValueChanged<bool?>? _onChanged;
set onChanged(ValueChanged<bool?>? value) {
if (value == _onChanged)
return;
final bool wasInteractive = isInteractive;
_onChanged = value;
if (wasInteractive != isInteractive) {
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
/// Whether [value] of this control can be changed by user interaction.
///
/// The control is considered interactive if the [onChanged] callback is
/// non-null. If the callback is null, then the control is disabled, and
/// non-interactive. A disabled checkbox, for example, is displayed using a
/// grey color and its value cannot be changed.
bool get isInteractive => onChanged != null;
late TapGestureRecognizer _tap;
Offset? _downPosition;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
if (value == false)
_positionController.reverse();
else
_positionController.forward();
if (isInteractive) {
switch (_reactionController.status) {
case AnimationStatus.forward:
_reactionController.forward();
break;
case AnimationStatus.reverse:
_reactionController.reverse();
break;
case AnimationStatus.dismissed:
case AnimationStatus.completed:
// nothing to do
break;
}
}
}
@override
void detach() {
_positionController.stop();
_reactionController.stop();
_reactionHoverFadeController.stop();
_reactionFocusFadeController.stop();
super.detach();
}
void _handleTapDown(TapDownDetails details) {
if (isInteractive) {
setState(() {
_downPosition = details.localPosition;
});
_downPosition = globalToLocal(details.globalPosition);
_reactionController.forward();
}
}
void _handleTap([Intent? _]) {
void _handleTap() {
if (!isInteractive)
return;
switch (value) {
@ -238,358 +446,76 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
onChanged!(false);
break;
}
context.findRenderObject()!.sendSemanticsEvent(const TapSemanticEvent());
sendSemanticsEvent(const TapSemanticEvent());
}
void _handleTapEnd([TapUpDetails? _]) {
if (_downPosition != null) {
setState(() { _downPosition = null; });
}
_reactionController.reverse();
void _handleTapUp(TapUpDetails details) {
_downPosition = null;
if (isInteractive)
_reactionController.reverse();
}
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
setState(() { _focused = focused; });
if (focused) {
_reactionFocusFadeController.forward();
} else {
_reactionFocusFadeController.reverse();
}
}
void _handleTapCancel() {
_downPosition = null;
if (isInteractive)
_reactionController.reverse();
}
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
if (hovering) {
_reactionHoverFadeController.forward();
} else {
_reactionHoverFadeController.reverse();
}
}
}
@override
bool hitTestSelf(Offset position) => true;
/// Describes the current [MaterialState] of the Toggleable.
///
/// The returned set will include:
///
/// * [MaterialState.disabled], if [isInteractive] is false
/// * [MaterialState.hovered], if a pointer is hovering over the Toggleable
/// * [MaterialState.focused], if the Toggleable has input focus
/// * [MaterialState.selected], if [value] is true or null
Set<MaterialState> get states => <MaterialState>{
if (!isInteractive) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (value != false) MaterialState.selected,
};
/// Typically wraps a `painter` that draws the actual visuals of the
/// Toggleable with logic to toggle it.
///
/// Consider providing a subclass of [ToggleablePainter] as a `painter`, which
/// implements logic to draw a radial ink reaction for this control. The
/// painter is usually configured with the [reaction], [position],
/// [reactionHoverFade], and [reactionFocusFade] animation provided by this
/// mixin. It is expected to draw the visuals of the Toggleable based on the
/// current value of these animations. The animations are triggered by
/// this mixin to transition the Toggleable from one state to another.
///
/// This method must be called from the [build] method of the [State] class
/// that uses this mixin. The returned [Widget] must be returned from the
/// build method - potentially after wrapping it in other widgets.
Widget buildToggleable({
FocusNode? focusNode,
bool autofocus = false,
required MaterialStateProperty<MouseCursor> mouseCursor,
required Size size,
required CustomPainter painter,
}) {
return FocusableActionDetector(
actions: _actionMap,
focusNode: focusNode,
autofocus: autofocus,
enabled: isInteractive,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
mouseCursor: mouseCursor.resolve(states),
child: GestureDetector(
excludeFromSemantics: !isInteractive,
onTapDown: _handleTapDown,
onTap: _handleTap,
onTapUp: _handleTapEnd,
onTapCancel: _handleTapEnd,
child: Semantics(
enabled: isInteractive,
child: CustomPaint(
size: size,
painter: painter,
),
),
),
);
}
}
/// A base class for a [CustomPainter] that may be passed to
/// [ToggleableStateMixin.buildToggleable] to draw the visual representation of
/// a Toggleable.
///
/// Subclasses must implement the [paint] method to draw the actual visuals of
/// the Toggleable. In their [paint] method subclasses may call
/// [paintRadialReaction] to draw a radial ink reaction for this control.
abstract class ToggleablePainter extends ChangeNotifier implements CustomPainter {
/// The visual value of the control.
///
/// Usually set to [ToggleableStateMixin.position].
Animation<double> get position => _position!;
Animation<double>? _position;
set position(Animation<double> value) {
if (value == _position) {
return;
}
_position?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_position = value;
notifyListeners();
}
/// The visual value of the radial reaction animation.
///
/// Usually set to [ToggleableStateMixin.reaction].
Animation<double> get reaction => _reaction!;
Animation<double>? _reaction;
set reaction(Animation<double> value) {
if (value == _reaction) {
return;
}
_reaction?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_reaction = value;
notifyListeners();
}
/// Controls the radial reaction's opacity animation for focus changes.
///
/// Usually set to [ToggleableStateMixin.reactionFocusFade].
Animation<double> get reactionFocusFade => _reactionFocusFade!;
Animation<double>? _reactionFocusFade;
set reactionFocusFade(Animation<double> value) {
if (value == _reactionFocusFade) {
return;
}
_reactionFocusFade?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_reactionFocusFade = value;
notifyListeners();
}
/// Controls the radial reaction's opacity animation for hover changes.
///
/// Usually set to [ToggleableStateMixin.reactionHoverFade].
Animation<double> get reactionHoverFade => _reactionHoverFade!;
Animation<double>? _reactionHoverFade;
set reactionHoverFade(Animation<double> value) {
if (value == _reactionHoverFade) {
return;
}
_reactionHoverFade?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_reactionHoverFade = value;
notifyListeners();
}
/// The color that should be used in the active state (i.e., when
/// [ToggleableStateMixin.value] is true).
///
/// For example, a checkbox should use this color when checked.
Color get activeColor => _activeColor!;
Color? _activeColor;
set activeColor(Color value) {
if (_activeColor == value) {
return;
}
_activeColor = value;
notifyListeners();
}
/// The color that should be used in the inactive state (i.e., when
/// [ToggleableStateMixin.value] is false).
///
/// For example, a checkbox should use this color when unchecked.
Color get inactiveColor => _inactiveColor!;
Color? _inactiveColor;
set inactiveColor(Color value) {
if (_inactiveColor == value) {
return;
}
_inactiveColor = value;
notifyListeners();
}
/// The color that should be used for the reaction when the toggleable is
/// inactive.
///
/// Used when the toggleable needs to change the reaction color/transparency
/// that is displayed when the toggleable is inactive and tapped.
Color get inactiveReactionColor => _inactiveReactionColor!;
Color? _inactiveReactionColor;
set inactiveReactionColor(Color value) {
if (value == _inactiveReactionColor) {
return;
}
_inactiveReactionColor = value;
notifyListeners();
}
/// The color that should be used for the reaction when the toggleable is
/// active.
///
/// Used when the toggleable needs to change the reaction color/transparency
/// that is displayed when the toggleable is active and tapped.
Color get reactionColor => _reactionColor!;
Color? _reactionColor;
set reactionColor(Color value) {
if (value == _reactionColor) {
return;
}
_reactionColor = value;
notifyListeners();
}
/// The color that should be used for the reaction when [isHovered] is true.
///
/// Used when the toggleable needs to change the reaction color/transparency,
/// when it is being hovered over.
Color get hoverColor => _hoverColor!;
Color? _hoverColor;
set hoverColor(Color value) {
if (value == _hoverColor) {
return;
}
_hoverColor = value;
notifyListeners();
}
/// The color that should be used for the reaction when [isFocused] is true.
///
/// Used when the toggleable needs to change the reaction color/transparency,
/// when it has focus.
Color get focusColor => _focusColor!;
Color? _focusColor;
set focusColor(Color value) {
if (value == _focusColor) {
return;
}
_focusColor = value;
notifyListeners();
}
/// The splash radius for the radial reaction.
double get splashRadius => _splashRadius!;
double? _splashRadius;
set splashRadius(double value) {
if (value == _splashRadius) {
return;
}
_splashRadius = value;
notifyListeners();
}
/// The [Offset] within the Toggleable at which a pointer touched the Toggleable.
///
/// This is null if currently no pointer is touching the Toggleable.
///
/// Usually set to [ToggleableStateMixin.downPosition].
Offset? get downPosition => _downPosition;
Offset? _downPosition;
set downPosition(Offset? value) {
if (value == _downPosition) {
return;
}
_downPosition = value;
notifyListeners();
}
/// True if this toggleable has the input focus.
bool get isFocused => _isFocused!;
bool? _isFocused;
set isFocused(bool? value) {
if (value == _isFocused) {
return;
}
_isFocused = value;
notifyListeners();
}
/// True if this toggleable is being hovered over by a pointer.
bool get isHovered => _isHovered!;
bool? _isHovered;
set isHovered(bool? value) {
if (value == _isHovered) {
return;
}
_isHovered = value;
notifyListeners();
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && isInteractive)
_tap.addPointer(event);
}
/// Used by subclasses to paint the radial ink reaction for this control.
///
/// The reaction is painted on the given canvas at the given offset. The
/// origin is the center point of the reaction (usually distinct from the
/// [downPosition] at which the user interacted with the control).
void paintRadialReaction({
required Canvas canvas,
Offset offset = Offset.zero,
required Offset origin,
}) {
if (!reaction.isDismissed || !reactionFocusFade.isDismissed || !reactionHoverFade.isDismissed) {
/// point at which the user interacted with the control, which is handled
/// automatically).
void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) {
final Paint reactionPaint = Paint()
..color = Color.lerp(
Color.lerp(
Color.lerp(inactiveReactionColor, reactionColor, position.value),
Color.lerp(inactiveReactionColor, reactionColor, _position.value),
hoverColor,
reactionHoverFade.value,
_reactionHoverFade.value,
),
focusColor,
reactionFocusFade.value,
_reactionFocusFade.value,
)!;
final Offset center = Offset.lerp(downPosition ?? origin, origin, reaction.value)!;
final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value)!;
final Animatable<double> radialReactionRadiusTween = Tween<double>(
begin: 0.0,
end: splashRadius,
);
final double reactionRadius = isFocused || isHovered
final double reactionRadius = hasFocus || hovering
? splashRadius
: radialReactionRadiusTween.evaluate(reaction);
: radialReactionRadiusTween.evaluate(_reaction);
if (reactionRadius > 0.0) {
canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
}
}
}
@override
void dispose() {
_position?.removeListener(notifyListeners);
_reaction?.removeListener(notifyListeners);
_reactionFocusFade?.removeListener(notifyListeners);
_reactionHoverFade?.removeListener(notifyListeners);
super.dispose();
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isEnabled = isInteractive;
if (isInteractive)
config.onTap = _handleTap;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
@override
bool? hitTest(Offset position) => null;
@override
SemanticsBuilderCallback? get semanticsBuilder => null;
@override
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => false;
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true));
properties.add(FlagProperty('isInteractive', value: isInteractive, ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true));
}
}

View file

@ -99,7 +99,7 @@ void main() {
),
));
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
// isFocusable is delayed by 1 frame.
@ -108,7 +108,7 @@ void main() {
await tester.pump();
// isFocusable should be false now after the 1 frame delay.
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
));
@ -120,7 +120,7 @@ void main() {
),
));
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isChecked: true,
@ -290,7 +290,7 @@ void main() {
);
await tester.tap(find.byType(Checkbox));
final RenderObject object = tester.firstRenderObject(find.byType(Checkbox));
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
expect(checkboxValue, true);
expect(semanticEvent, <String, dynamic>{
@ -319,8 +319,10 @@ void main() {
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
}
await tester.pumpWidget(buildFrame(false));
@ -375,8 +377,10 @@ void main() {
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
}
await tester.pumpWidget(buildFrame(checkColor: checkColor));
@ -449,10 +453,11 @@ void main() {
paints
..circle(color: Colors.orange[500])
..drrect(
color: const Color(0x8a000000),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)),
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, const Radius.circular(-1.0)),
),
color: const Color(0x8a000000),
outer: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)),
inner: RRect.fromLTRBR(393.0,
293.0, 407.0, 307.0, const Radius.circular(-1.0))),
);
// Check what happens when disabled.
@ -464,10 +469,11 @@ void main() {
Material.of(tester.element(find.byType(Checkbox))),
paints
..drrect(
color: const Color(0x61000000),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)),
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, const Radius.circular(-1.0)),
),
color: const Color(0x61000000),
outer: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)),
inner: RRect.fromLTRBR(393.0,
293.0, 407.0, 307.0, const Radius.circular(-1.0))),
);
});
@ -819,8 +825,10 @@ void main() {
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
}
await tester.pumpWidget(buildFrame(enabled: true));
@ -870,8 +878,10 @@ void main() {
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
}
await tester.pumpWidget(buildFrame());
@ -927,9 +937,11 @@ void main() {
paints
..drrect(
color: const Color(0xfff44336),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(5)),
inner: RRect.fromLTRBR(19.0, 19.0, 29.0, 29.0, const Radius.circular(1)),
),
outer: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(5)),
inner: RRect.fromLTRBR(
395.0, 295.0, 405.0, 305.0, const Radius.circular(1)))
,
);
});
@ -1172,29 +1184,6 @@ void main() {
await gesture.up();
});
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
Widget buildCheckbox(bool show) {
return MaterialApp(
home: Material(
child: Center(
child: show ? Checkbox(value: true, onChanged: (_) { }) : Container(),
),
),
);
}
await tester.pumpWidget(buildCheckbox(true));
final Offset center = tester.getCenter(find.byType(Checkbox));
// Put a pointer down on the screen.
final TestGesture gesture = await tester.startGesture(center);
await tester.pump();
// While the pointer is down, the widget disappears.
await tester.pumpWidget(buildCheckbox(false));
expect(find.byType(Checkbox), findsNothing);
// Release pointer after widget disappeared.
gesture.up();
});
}
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {

View file

@ -308,7 +308,7 @@ void main() {
));
await tester.tap(find.byKey(key));
final RenderObject object = tester.firstRenderObject(find.byKey(key));
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
expect(radioValue, 1);
expect(semanticEvent, <String, dynamic>{
@ -1078,29 +1078,4 @@ void main() {
reason: 'Hovered Radio should use overlay color $hoverOverlayColor over $hoverColor',
);
});
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
final Key key = UniqueKey();
Widget buildRadio(bool show) {
return MaterialApp(
home: Material(
child: Center(
child: show ? Radio<bool>(key: key, value: true, groupValue: false, onChanged: (_) { }) : Container(),
)
),
);
}
await tester.pumpWidget(buildRadio(true));
final Offset center = tester.getCenter(find.byKey(key));
// Put a pointer down on the screen.
final TestGesture gesture = await tester.startGesture(center);
await tester.pump();
// While the pointer is down, the widget disappears.
await tester.pumpWidget(buildRadio(false));
expect(find.byKey(key), findsNothing);
// Release pointer after widget disappeared.
gesture.up();
});
}

View file

@ -37,7 +37,7 @@ void main() {
expect(log, equals(<dynamic>[false, '-', false]));
});
testWidgets('SwitchListTile semantics test', (WidgetTester tester) async {
testWidgets('SwitchListTile control test', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(wrap(
child: Column(

View file

@ -301,7 +301,8 @@ void main() {
paints
..rrect(
color: const Color(0x52000000), // Black with 32% opacity
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -316,7 +317,8 @@ void main() {
paints
..rrect(
color: Colors.blue[600]!.withAlpha(0x80),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -349,7 +351,8 @@ void main() {
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -380,7 +383,8 @@ void main() {
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -423,7 +427,8 @@ void main() {
paints
..rrect(
color: Colors.blue[500],
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -437,7 +442,8 @@ void main() {
paints
..rrect(
color: Colors.green[500],
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -522,19 +528,19 @@ void main() {
await gesture.up();
await tester.pump();
expect(value, isFalse);
final ToggleableStateMixin state = tester.state<ToggleableStateMixin>(
final RenderToggleable renderObject = tester.renderObject<RenderToggleable>(
find.descendant(
of: find.byType(Switch),
matching: find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch',
(Widget widget) => widget.runtimeType.toString() == '_SwitchRenderObjectWidget',
),
),
);
expect(state.position.value, lessThan(0.5));
expect(renderObject.position.value, lessThan(0.5));
await tester.pump();
await tester.pumpAndSettle();
expect(value, isFalse);
expect(state.position.value, 0);
expect(renderObject.position.value, 0);
// Move past the middle.
gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
@ -543,12 +549,12 @@ void main() {
await gesture.up();
await tester.pump();
expect(value, isTrue);
expect(state.position.value, greaterThan(0.5));
expect(renderObject.position.value, greaterThan(0.5));
await tester.pump();
await tester.pumpAndSettle();
expect(value, isTrue);
expect(state.position.value, 1.0);
expect(renderObject.position.value, 1.0);
// Now move back to the left, the revert animation should play.
gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
@ -557,12 +563,12 @@ void main() {
await gesture.up();
await tester.pump();
expect(value, isTrue);
expect(state.position.value, lessThan(0.5));
expect(renderObject.position.value, lessThan(0.5));
await tester.pump();
await tester.pumpAndSettle();
expect(value, isTrue);
expect(state.position.value, 1.0);
expect(renderObject.position.value, 1.0);
});
testWidgets('switch has semantic events', (WidgetTester tester) async {
@ -595,7 +601,7 @@ void main() {
),
);
await tester.tap(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
expect(value, true);
expect(semanticEvent, <String, dynamic>{
@ -744,7 +750,8 @@ void main() {
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
@ -762,7 +769,8 @@ void main() {
paints
..rrect(
color: const Color(0x52000000),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
@ -780,7 +788,8 @@ void main() {
paints
..rrect(
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -845,7 +854,8 @@ void main() {
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -865,7 +875,8 @@ void main() {
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
@ -881,7 +892,8 @@ void main() {
paints
..rrect(
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1058,7 +1070,8 @@ void main() {
),
);
final ToggleableStateMixin oldSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));
final RenderToggleable oldSwitchRenderObject = tester
.renderObject(find.byWidgetPredicate((Widget widget) => widget is LeafRenderObjectWidget));
stateSetter(() { value = false; });
await tester.pump();
@ -1066,12 +1079,14 @@ void main() {
stateSetter(() { enabled = false; });
await tester.pump();
final ToggleableStateMixin updatedSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));
final RenderToggleable updatedSwitchRenderObject = tester
.renderObject(find.byWidgetPredicate((Widget widget) => widget is LeafRenderObjectWidget));
expect(updatedSwitchState.isInteractive, false);
expect(updatedSwitchState, oldSwitchState);
expect(updatedSwitchState.position.isCompleted, false);
expect(updatedSwitchState.position.isDismissed, false);
expect(updatedSwitchRenderObject.isInteractive, false);
expect(updatedSwitchRenderObject, oldSwitchRenderObject);
expect(updatedSwitchRenderObject.position.isCompleted, false);
expect(updatedSwitchRenderObject.position.isDismissed, false);
});
testWidgets('Switch thumb color resolves in active/enabled states', (WidgetTester tester) async {
@ -1122,7 +1137,8 @@ void main() {
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1138,7 +1154,8 @@ void main() {
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1154,7 +1171,8 @@ void main() {
paints
..rrect(
color: const Color(0x52000000), // Black with 32% opacity,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1170,7 +1188,8 @@ void main() {
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1227,7 +1246,8 @@ void main() {
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1248,7 +1268,8 @@ void main() {
paints
..rrect(
color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1306,7 +1327,8 @@ void main() {
paints
..rrect(
color: inactiveDisabledTrackColor,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
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',
);
@ -1318,7 +1340,8 @@ void main() {
paints
..rrect(
color: activeDisabledTrackColor,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Active disabled switch should match these colors',
);
@ -1330,7 +1353,8 @@ void main() {
paints
..rrect(
color: inactiveEnabledTrackColor,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive enabled switch should match these colors',
);
@ -1342,7 +1366,8 @@ void main() {
paints
..rrect(
color: inactiveDisabledTrackColor,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive disabled switch should match these colors',
);
});
@ -1395,7 +1420,8 @@ void main() {
paints
..rrect(
color: focusedTrackColor,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive enabled switch should match these colors',
);
@ -1411,7 +1437,8 @@ void main() {
paints
..rrect(
color: hoveredTrackColor,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
rrect: RRect.fromLTRBR(
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0))),
reason: 'Inactive enabled switch should match these colors',
);
});
@ -1461,7 +1488,8 @@ void main() {
paints
..rrect(
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
@ -1610,27 +1638,4 @@ void main() {
reason: 'Hovered Switch should use overlay color $hoverOverlayColor over $hoverColor',
);
});
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
Widget buildSwitch(bool show) {
return MaterialApp(
home: Material(
child: Center(
child: show ? Switch(value: true, onChanged: (_) { }) : Container(),
),
),
);
}
await tester.pumpWidget(buildSwitch(true));
final Offset center = tester.getCenter(find.byType(Switch));
// Put a pointer down on the screen.
final TestGesture gesture = await tester.startGesture(center);
await tester.pump();
// While the pointer is down, the widget disappears.
await tester.pumpWidget(buildSwitch(false));
expect(find.byType(Switch), findsNothing);
// Release pointer after widget disappeared.
gesture.up();
});
}