Unverified Commit 328a262e authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

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

This reverts commit 14552a96.
parent 9964e8fe
...@@ -290,39 +290,56 @@ class Checkbox extends StatefulWidget { ...@@ -290,39 +290,56 @@ class Checkbox extends StatefulWidget {
_CheckboxState createState() => _CheckboxState(); _CheckboxState createState() => _CheckboxState();
} }
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, ToggleableStateMixin { class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
final _CheckboxPainter _painter = _CheckboxPainter(); bool get enabled => widget.onChanged != null;
bool? _previousValue; late Map<Type, Action<Intent>> _actionMap;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_previousValue = widget.value; _actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
};
} }
@override void _actionHandler(ActivateIntent intent) {
void didUpdateWidget(Checkbox oldWidget) { if (widget.onChanged != null) {
super.didUpdateWidget(oldWidget); switch (widget.value) {
if (oldWidget.value != widget.value) { case false:
_previousValue = oldWidget.value; widget.onChanged!(true);
animateToValue(); 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());
} }
@override bool _focused = false;
void dispose() { void _handleFocusHighlightChanged(bool focused) {
_painter.dispose(); if (focused != _focused) {
super.dispose(); setState(() { _focused = focused; });
}
} }
@override bool _hovering = false;
ValueChanged<bool?>? get onChanged => widget.onChanged; void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
@override setState(() { _hovering = hovering; });
bool get tristate => widget.tristate; }
}
@override Set<MaterialState> get _states => <MaterialState>{
bool? get value => widget.value; if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.value == null || widget.value!) MaterialState.selected,
};
MaterialStateProperty<Color?> get _widgetFillColor { MaterialStateProperty<Color?> get _widgetFillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
...@@ -369,17 +386,14 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg ...@@ -369,17 +386,14 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
break; break;
} }
size += effectiveVisualDensity.baseSizeAdjustment; size += effectiveVisualDensity.baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) { final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states)
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ?? themeData.checkboxTheme.mouseCursor?.resolve(_states)
?? themeData.checkboxTheme.mouseCursor?.resolve(states) ?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states);
?? MaterialStateMouseCursor.clickable.resolve(states);
});
// Colors need to be resolved in selected and non selected states separately // Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between. // so that they can be lerped between.
final Set<MaterialState> activeStates = states..add(MaterialState.selected); final Set<MaterialState> activeStates = _states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected); final Set<MaterialState> inactiveStates = _states..remove(MaterialState.selected);
final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates) final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates)
?? _widgetFillColor.resolve(activeStates) ?? _widgetFillColor.resolve(activeStates)
?? themeData.checkboxTheme.fillColor?.resolve(activeStates) ?? themeData.checkboxTheme.fillColor?.resolve(activeStates)
...@@ -389,13 +403,13 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg ...@@ -389,13 +403,13 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
?? themeData.checkboxTheme.fillColor?.resolve(inactiveStates) ?? themeData.checkboxTheme.fillColor?.resolve(inactiveStates)
?? _defaultFillColor.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) final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor ?? widget.focusColor
?? themeData.checkboxTheme.overlayColor?.resolve(focusedStates) ?? themeData.checkboxTheme.overlayColor?.resolve(focusedStates)
?? themeData.focusColor; ?? 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) final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor ?? widget.hoverColor
?? themeData.checkboxTheme.overlayColor?.resolve(hoveredStates) ?? themeData.checkboxTheme.overlayColor?.resolve(hoveredStates)
...@@ -411,96 +425,195 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg ...@@ -411,96 +425,195 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
?? themeData.checkboxTheme.overlayColor?.resolve(inactivePressedStates) ?? themeData.checkboxTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveColor.withAlpha(kRadialReactionAlpha); ?? effectiveActiveColor.withAlpha(kRadialReactionAlpha);
final Color effectiveCheckColor = widget.checkColor final Color effectiveCheckColor = widget.checkColor
?? themeData.checkboxTheme.checkColor?.resolve(states) ?? themeData.checkboxTheme.checkColor?.resolve(_states)
?? const Color(0xFFFFFFFF); ?? const Color(0xFFFFFFFF);
return Semantics( return FocusableActionDetector(
checked: widget.value == true, actions: _actionMap,
child: buildToggleable( focusNode: widget.focusNode,
mouseCursor: effectiveMouseCursor, autofocus: widget.autofocus,
focusNode: widget.focusNode, enabled: enabled,
autofocus: widget.autofocus, onShowFocusHighlight: _handleFocusHighlightChanged,
size: size, onShowHoverHighlight: _handleHoverChanged,
painter: _painter mouseCursor: effectiveMouseCursor,
..position = position child: Builder(
..reaction = reaction builder: (BuildContext context) {
..reactionFocusFade = reactionFocusFade return _CheckboxRenderObjectWidget(
..reactionHoverFade = reactionHoverFade value: widget.value,
..inactiveReactionColor = effectiveInactivePressedOverlayColor tristate: widget.tristate,
..reactionColor = effectiveActivePressedOverlayColor activeColor: effectiveActiveColor,
..hoverColor = effectiveHoverOverlayColor checkColor: effectiveCheckColor,
..focusColor = effectiveFocusOverlayColor inactiveColor: effectiveInactiveColor,
..splashRadius = widget.splashRadius ?? themeData.checkboxTheme.splashRadius ?? kRadialReactionRadius focusColor: effectiveFocusOverlayColor,
..downPosition = downPosition hoverColor: effectiveHoverOverlayColor,
..isFocused = states.contains(MaterialState.focused) reactionColor: effectiveActivePressedOverlayColor,
..isHovered = states.contains(MaterialState.hovered) inactiveReactionColor: effectiveInactivePressedOverlayColor,
..activeColor = effectiveActiveColor splashRadius: widget.splashRadius ?? themeData.checkboxTheme.splashRadius ?? kRadialReactionRadius,
..inactiveColor = effectiveInactiveColor onChanged: widget.onChanged,
..checkColor = effectiveCheckColor additionalConstraints: additionalConstraints,
..value = value vsync: this,
..previousValue = _previousValue hasFocus: _focused,
..shape = widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder( hovering: _hovering,
borderRadius: BorderRadius.all(Radius.circular(1.0)) side: widget.side ?? themeData.checkboxTheme.side,
) shape: widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder(
..side = widget.side ?? themeData.checkboxTheme.side, borderRadius: BorderRadius.all(Radius.circular(1.0)),
),
);
},
), ),
); );
} }
} }
const double _kEdgeSize = Checkbox.width; class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
const double _kStrokeWidth = 2.0; 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);
class _CheckboxPainter extends ToggleablePainter { final bool? value;
Color get checkColor => _checkColor!; final bool tristate;
Color? _checkColor; final bool hasFocus;
set checkColor(Color value) { final bool hovering;
if (_checkColor == value) { final Color activeColor;
return; final Color checkColor;
} final Color inactiveColor;
_checkColor = value; final Color focusColor;
notifyListeners(); 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;
bool? get value => _value; @override
bool? _value; _RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox(
set value(bool? value) { value: value,
if (_value == value) { tristate: tristate,
return; activeColor: activeColor,
} checkColor: checkColor,
_value = value; inactiveColor: inactiveColor,
notifyListeners(); focusColor: focusColor,
} hoverColor: hoverColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
vsync: vsync,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
shape: shape,
side: side,
);
bool? get previousValue => _previousValue; @override
bool? _previousValue; void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
set previousValue(bool? value) { renderObject
if (_previousValue == value) { // The `tristate` must be changed before `value` due to the assertion at
return; // the beginning of `set value`.
} ..tristate = tristate
_previousValue = value; ..value = value
notifyListeners(); ..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 _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;
OutlinedBorder get shape => _shape!; @override
OutlinedBorder? _shape; set value(bool? newValue) {
set shape(OutlinedBorder value) { if (newValue == value)
if (_shape == value) {
return; return;
} _oldValue = value;
_shape = value; super.value = newValue;
notifyListeners();
} }
BorderSide? get side => _side; @override
BorderSide? _side; void describeSemanticsConfiguration(SemanticsConfiguration config) {
set side(BorderSide? value) { super.describeSemanticsConfiguration(config);
if (_side == value) { config.isChecked = value == true;
return;
}
_side = value;
notifyListeners();
} }
// The square outer bounds of the checkbox at t, with the specified origin. // The square outer bounds of the checkbox at t, with the specified origin.
...@@ -531,11 +644,10 @@ class _CheckboxPainter extends ToggleablePainter { ...@@ -531,11 +644,10 @@ class _CheckboxPainter extends ToggleablePainter {
void _drawBorder(Canvas canvas, Rect outer, double t, Paint paint) { void _drawBorder(Canvas canvas, Rect outer, double t, Paint paint) {
assert(t >= 0.0 && t <= 0.5); assert(t >= 0.0 && t <= 0.5);
OutlinedBorder resolvedShape = shape;
if (side == null) { 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) { void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
...@@ -574,18 +686,19 @@ class _CheckboxPainter extends ToggleablePainter { ...@@ -574,18 +686,19 @@ class _CheckboxPainter extends ToggleablePainter {
} }
@override @override
void paint(Canvas canvas, Size size) { void paint(PaintingContext context, Offset offset) {
paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero)); final Canvas canvas = context.canvas;
paintRadialReaction(canvas, offset, size.center(Offset.zero));
final Paint strokePaint = _createStrokePaint(); 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 AnimationStatus status = position.status;
final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed
? position.value ? position.value
: 1.0 - position.value; : 1.0 - position.value;
// Four cases: false to null, false to true, null to false, true to false // 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 double t = value == false ? 1.0 - tNormalized : tNormalized;
final Rect outer = _outerRectAt(origin, t); final Rect outer = _outerRectAt(origin, t);
final Path emptyCheckboxPath = shape.copyWith(side: side).getOuterPath(outer); final Path emptyCheckboxPath = shape.copyWith(side: side).getOuterPath(outer);
...@@ -597,7 +710,7 @@ class _CheckboxPainter extends ToggleablePainter { ...@@ -597,7 +710,7 @@ class _CheckboxPainter extends ToggleablePainter {
canvas.drawPath(emptyCheckboxPath, paint); canvas.drawPath(emptyCheckboxPath, paint);
final double tShrink = (t - 0.5) * 2.0; final double tShrink = (t - 0.5) * 2.0;
if (previousValue == null || value == null) if (_oldValue == null || value == null)
_drawDash(canvas, origin, tShrink, strokePaint); _drawDash(canvas, origin, tShrink, strokePaint);
else else
_drawCheck(canvas, origin, tShrink, strokePaint); _drawCheck(canvas, origin, tShrink, strokePaint);
...@@ -609,7 +722,7 @@ class _CheckboxPainter extends ToggleablePainter { ...@@ -609,7 +722,7 @@ class _CheckboxPainter extends ToggleablePainter {
if (tNormalized <= 0.5) { if (tNormalized <= 0.5) {
final double tShrink = 1.0 - tNormalized * 2.0; final double tShrink = 1.0 - tNormalized * 2.0;
if (previousValue == true) if (_oldValue == true)
_drawCheck(canvas, origin, tShrink, strokePaint); _drawCheck(canvas, origin, tShrink, strokePaint);
else else
_drawDash(canvas, origin, tShrink, strokePaint); _drawDash(canvas, origin, tShrink, strokePaint);
......
...@@ -353,47 +353,64 @@ class Radio<T> extends StatefulWidget { ...@@ -353,47 +353,64 @@ class Radio<T> extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus} /// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus; final bool autofocus;
bool get _selected => value == groupValue;
@override @override
_RadioState<T> createState() => _RadioState<T>(); _RadioState<T> createState() => _RadioState<T>();
} }
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, ToggleableStateMixin { class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
final _RadioPainter _painter = _RadioPainter(); bool get enabled => widget.onChanged != null;
late Map<Type, Action<Intent>> _actionMap;
void _handleChanged(bool? selected) { @override
if (selected == null) { void initState() {
widget.onChanged!(null); super.initState();
return; _actionMap = <Type, Action<Intent>>{
} ActivateIntent: CallbackAction<ActivateIntent>(
if (selected) { onInvoke: _actionHandler,
),
};
}
void _actionHandler(ActivateIntent intent) {
if (widget.onChanged != null) {
widget.onChanged!(widget.value); widget.onChanged!(widget.value);
} }
final RenderObject renderObject = context.findRenderObject()!;
renderObject.sendSemanticsEvent(const TapSemanticEvent());
} }
@override bool _focused = false;
void didUpdateWidget(Radio<T> oldWidget) { void _handleHighlightChanged(bool focused) {
super.didUpdateWidget(oldWidget); if (_focused != focused) {
if (widget._selected != oldWidget._selected) { setState(() { _focused = focused; });
animateToValue();
} }
} }
@override bool _hovering = false;
void dispose() { void _handleHoverChanged(bool hovering) {
_painter.dispose(); if (_hovering != hovering) {
super.dispose(); setState(() { _hovering = hovering; });
}
} }
@override void _handleChanged(bool? selected) {
ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null; if (selected == null) {
widget.onChanged!(null);
return;
}
if (selected) {
widget.onChanged!(widget.value);
}
}
@override bool get _selected => widget.value == widget.groupValue;
bool get tristate => widget.toggleable;
@override Set<MaterialState> get _states => <MaterialState>{
bool? get value => widget._selected; if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (_selected) MaterialState.selected,
};
MaterialStateProperty<Color?> get _widgetFillColor { MaterialStateProperty<Color?> get _widgetFillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
...@@ -440,17 +457,15 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg ...@@ -440,17 +457,15 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
break; break;
} }
size += effectiveVisualDensity.baseSizeAdjustment; size += effectiveVisualDensity.baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) { final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states)
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ?? themeData.radioTheme.mouseCursor?.resolve(_states)
?? themeData.radioTheme.mouseCursor?.resolve(states) ?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states);
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
});
// Colors need to be resolved in selected and non selected states separately // Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between. // so that they can be lerped between.
final Set<MaterialState> activeStates = states..add(MaterialState.selected); final Set<MaterialState> activeStates = _states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected); final Set<MaterialState> inactiveStates = _states..remove(MaterialState.selected);
final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates) final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates)
?? _widgetFillColor.resolve(activeStates) ?? _widgetFillColor.resolve(activeStates)
?? themeData.radioTheme.fillColor?.resolve(activeStates) ?? themeData.radioTheme.fillColor?.resolve(activeStates)
...@@ -460,13 +475,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg ...@@ -460,13 +475,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
?? themeData.radioTheme.fillColor?.resolve(inactiveStates) ?? themeData.radioTheme.fillColor?.resolve(inactiveStates)
?? _defaultFillColor.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) final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor ?? widget.focusColor
?? themeData.radioTheme.overlayColor?.resolve(focusedStates) ?? themeData.radioTheme.overlayColor?.resolve(focusedStates)
?? themeData.focusColor; ?? 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) final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor ?? widget.hoverColor
?? themeData.radioTheme.overlayColor?.resolve(hoveredStates) ?? themeData.radioTheme.overlayColor?.resolve(hoveredStates)
...@@ -482,40 +497,156 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg ...@@ -482,40 +497,156 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
?? themeData.radioTheme.overlayColor?.resolve(inactivePressedStates) ?? themeData.radioTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveColor.withAlpha(kRadialReactionAlpha); ?? effectiveActiveColor.withAlpha(kRadialReactionAlpha);
return Semantics(
inMutuallyExclusiveGroup: true, return FocusableActionDetector(
checked: widget._selected, actions: _actionMap,
child: buildToggleable( focusNode: widget.focusNode,
focusNode: widget.focusNode, autofocus: widget.autofocus,
autofocus: widget.autofocus, mouseCursor: effectiveMouseCursor,
mouseCursor: effectiveMouseCursor, enabled: enabled,
size: size, onShowFocusHighlight: _handleHighlightChanged,
painter: _painter onShowHoverHighlight: _handleHoverChanged,
..position = position child: Builder(
..reaction = reaction builder: (BuildContext context) {
..reactionFocusFade = reactionFocusFade return _RadioRenderObjectWidget(
..reactionHoverFade = reactionHoverFade selected: _selected,
..inactiveReactionColor = effectiveInactivePressedOverlayColor activeColor: effectiveActiveColor,
..reactionColor = effectiveActivePressedOverlayColor inactiveColor: effectiveInactiveColor,
..hoverColor = effectiveHoverOverlayColor focusColor: effectiveFocusOverlayColor,
..focusColor = effectiveFocusOverlayColor hoverColor: effectiveHoverOverlayColor,
..splashRadius = widget.splashRadius ?? themeData.radioTheme.splashRadius ?? kRadialReactionRadius reactionColor: effectiveActivePressedOverlayColor,
..downPosition = downPosition inactiveReactionColor: effectiveInactivePressedOverlayColor,
..isFocused = states.contains(MaterialState.focused) splashRadius: widget.splashRadius ?? themeData.radioTheme.splashRadius ?? kRadialReactionRadius,
..isHovered = states.contains(MaterialState.hovered) onChanged: enabled ? _handleChanged : null,
..activeColor = effectiveActiveColor toggleable: widget.toggleable,
..inactiveColor = effectiveInactiveColor additionalConstraints: additionalConstraints,
vsync: this,
hasFocus: _focused,
hovering: _hovering,
);
},
), ),
); );
} }
} }
class _RadioPainter extends ToggleablePainter { 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 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 @override
void paint(Canvas canvas, Size size) { _RenderRadio createRenderObject(BuildContext context) => _RenderRadio(
paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero)); 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,
);
final Offset center = (Offset.zero & size).center; @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 // Outer circle
final Paint paint = Paint() final Paint paint = Paint()
...@@ -530,4 +661,12 @@ class _RadioPainter extends ToggleablePainter { ...@@ -530,4 +661,12 @@ class _RadioPainter extends ToggleablePainter {
canvas.drawCircle(center, _kInnerRadius * position.value, paint); canvas.drawCircle(center, _kInnerRadius * position.value, paint);
} }
} }
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isInMutuallyExclusiveGroup = true
..isChecked = value == true;
}
} }
...@@ -51,7 +51,7 @@ enum _SwitchType { material, adaptive } ...@@ -51,7 +51,7 @@ enum _SwitchType { material, adaptive }
/// * [Radio], for selecting among a set of explicit values. /// * [Radio], for selecting among a set of explicit values.
/// * [Slider], for selecting a value in a range. /// * [Slider], for selecting a value in a range.
/// * <https://material.io/design/components/selection-controls.html#switches> /// * <https://material.io/design/components/selection-controls.html#switches>
class Switch extends StatelessWidget { class Switch extends StatefulWidget {
/// Creates a material design switch. /// Creates a material design switch.
/// ///
/// The switch itself does not maintain any state. Instead, when the state of /// The switch itself does not maintain any state. Instead, when the state of
...@@ -359,87 +359,8 @@ class Switch extends StatelessWidget { ...@@ -359,87 +359,8 @@ class Switch extends StatelessWidget {
/// {@macro flutter.widgets.Focus.autofocus} /// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus; final bool autofocus;
Size _getSwitchSize(ThemeData theme) {
final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
?? theme.switchTheme.materialTapTargetSize
?? theme.materialTapTargetSize;
switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded:
return const Size(_kSwitchWidth, _kSwitchHeight);
case MaterialTapTargetSize.shrinkWrap:
return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
}
}
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
),
),
);
}
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 @override
Widget build(BuildContext context) { _SwitchState createState() => _SwitchState();
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 @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
...@@ -449,95 +370,65 @@ class Switch extends StatelessWidget { ...@@ -449,95 +370,65 @@ class Switch extends StatelessWidget {
} }
} }
class _MaterialSwitch extends StatefulWidget { class _SwitchState extends State<Switch> with TickerProviderStateMixin {
const _MaterialSwitch({ late Map<Type, Action<Intent>> _actionMap;
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 @override
State<StatefulWidget> createState() => _MaterialSwitchState(); 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());
}
class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderStateMixin, ToggleableStateMixin { bool _focused = false;
final _SwitchPainter _painter = _SwitchPainter(); void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
setState(() { _focused = focused; });
}
}
@override bool _hovering = false;
void didUpdateWidget(_MaterialSwitch oldWidget) { void _handleHoverChanged(bool hovering) {
super.didUpdateWidget(oldWidget); if (hovering != _hovering) {
if (oldWidget.value != widget.value) { setState(() { _hovering = hovering; });
// 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 Size getSwitchSize(ThemeData theme) {
void dispose() { final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize
_painter.dispose(); ?? theme.switchTheme.materialTapTargetSize
super.dispose(); ?? theme.materialTapTargetSize;
switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded:
return const Size(_kSwitchWidth, _kSwitchHeight);
case MaterialTapTargetSize.shrinkWrap:
return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
}
} }
@override bool get enabled => widget.onChanged != null;
ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
@override void _didFinishDragging() {
bool get tristate => false; // The user has finished dragging the thumb of this switch. Rebuild the switch
// to update the animation.
setState(() {});
}
@override Set<MaterialState> get _states => <MaterialState>{
bool? get value => widget.value; if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.value) MaterialState.selected,
};
MaterialStateProperty<Color?> get _widgetThumbColor { MaterialStateProperty<Color?> get _widgetThumbColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
...@@ -596,68 +487,14 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -596,68 +487,14 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
}); });
} }
double get _trackInnerLength => widget.size.width - _kSwitchMinSize; Widget buildMaterialSwitch(BuildContext context) {
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) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
if (_needsPositionAnimation) {
_needsPositionAnimation = false;
animateToValue();
}
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
// Colors need to be resolved in selected and non selected states separately // Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between. // so that they can be lerped between.
final Set<MaterialState> activeStates = states..add(MaterialState.selected); final Set<MaterialState> activeStates = _states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected); final Set<MaterialState> inactiveStates = _states..remove(MaterialState.selected);
final Color effectiveActiveThumbColor = widget.thumbColor?.resolve(activeStates) final Color effectiveActiveThumbColor = widget.thumbColor?.resolve(activeStates)
?? _widgetThumbColor.resolve(activeStates) ?? _widgetThumbColor.resolve(activeStates)
?? theme.switchTheme.thumbColor?.resolve(activeStates) ?? theme.switchTheme.thumbColor?.resolve(activeStates)
...@@ -675,13 +512,14 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -675,13 +512,14 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
?? theme.switchTheme.trackColor?.resolve(inactiveStates) ?? theme.switchTheme.trackColor?.resolve(inactiveStates)
?? _defaultTrackColor.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) final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor ?? widget.focusColor
?? theme.switchTheme.overlayColor?.resolve(focusedStates) ?? theme.switchTheme.overlayColor?.resolve(focusedStates)
?? theme.focusColor; ?? 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) final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor ?? widget.hoverColor
?? theme.switchTheme.overlayColor?.resolve(hoveredStates) ?? theme.switchTheme.overlayColor?.resolve(hoveredStates)
...@@ -697,65 +535,277 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -697,65 +535,277 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
?? theme.switchTheme.overlayColor?.resolve(inactivePressedStates) ?? theme.switchTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha); ?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) { final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states)
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ?? theme.switchTheme.mouseCursor?.resolve(_states)
?? theme.switchTheme.mouseCursor?.resolve(states) ?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states);
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
}); 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,
);
},
),
);
}
return Semantics( Widget buildCupertinoSwitch(BuildContext context) {
toggled: widget.value, final Size size = getSwitchSize(Theme.of(context));
child: GestureDetector( return Focus(
excludeFromSemantics: true, focusNode: widget.focusNode,
onHorizontalDragStart: _handleDragStart, autofocus: widget.autofocus,
onHorizontalDragUpdate: _handleDragUpdate, child: Container(
onHorizontalDragEnd: _handleDragEnd, width: size.width, // Same size as the Material switch.
dragStartBehavior: widget.dragStartBehavior, height: size.height,
child: buildToggleable( alignment: Alignment.center,
mouseCursor: effectiveMouseCursor, child: CupertinoSwitch(
focusNode: widget.focusNode, dragStartBehavior: widget.dragStartBehavior,
autofocus: widget.autofocus, value: widget.value,
size: widget.size, onChanged: widget.onChanged,
painter: _painter activeColor: widget.activeColor,
..position = position trackColor: widget.inactiveTrackColor
..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
), ),
), ),
); );
} }
@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 _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 _SwitchPainter extends ToggleablePainter { 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? get activeThumbImage => _activeThumbImage;
ImageProvider? _activeThumbImage; ImageProvider? _activeThumbImage;
set activeThumbImage(ImageProvider? value) { set activeThumbImage(ImageProvider? value) {
if (value == _activeThumbImage) if (value == _activeThumbImage)
return; return;
_activeThumbImage = value; _activeThumbImage = value;
notifyListeners(); markNeedsPaint();
} }
ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError; ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError;
...@@ -765,7 +815,7 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -765,7 +815,7 @@ class _SwitchPainter extends ToggleablePainter {
return; return;
} }
_onActiveThumbImageError = value; _onActiveThumbImageError = value;
notifyListeners(); markNeedsPaint();
} }
ImageProvider? get inactiveThumbImage => _inactiveThumbImage; ImageProvider? get inactiveThumbImage => _inactiveThumbImage;
...@@ -774,7 +824,7 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -774,7 +824,7 @@ class _SwitchPainter extends ToggleablePainter {
if (value == _inactiveThumbImage) if (value == _inactiveThumbImage)
return; return;
_inactiveThumbImage = value; _inactiveThumbImage = value;
notifyListeners(); markNeedsPaint();
} }
ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError; ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError;
...@@ -784,77 +834,132 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -784,77 +834,132 @@ class _SwitchPainter extends ToggleablePainter {
return; return;
} }
_onInactiveThumbImageError = value; _onInactiveThumbImageError = value;
notifyListeners(); markNeedsPaint();
} }
Color get activeTrackColor => _activeTrackColor!; Color get activeTrackColor => _activeTrackColor;
Color? _activeTrackColor; Color _activeTrackColor;
set activeTrackColor(Color value) { set activeTrackColor(Color value) {
assert(value != null); assert(value != null);
if (value == _activeTrackColor) if (value == _activeTrackColor)
return; return;
_activeTrackColor = value; _activeTrackColor = value;
notifyListeners(); markNeedsPaint();
} }
Color get inactiveTrackColor => _inactiveTrackColor!; Color get inactiveTrackColor => _inactiveTrackColor;
Color? _inactiveTrackColor; Color _inactiveTrackColor;
set inactiveTrackColor(Color value) { set inactiveTrackColor(Color value) {
assert(value != null); assert(value != null);
if (value == _inactiveTrackColor) if (value == _inactiveTrackColor)
return; return;
_inactiveTrackColor = value; _inactiveTrackColor = value;
notifyListeners(); markNeedsPaint();
} }
ImageConfiguration get configuration => _configuration!; ImageConfiguration get configuration => _configuration;
ImageConfiguration? _configuration; ImageConfiguration _configuration;
set configuration(ImageConfiguration value) { set configuration(ImageConfiguration value) {
assert(value != null); assert(value != null);
if (value == _configuration) if (value == _configuration)
return; return;
_configuration = value; _configuration = value;
notifyListeners(); markNeedsPaint();
} }
TextDirection get textDirection => _textDirection!; TextDirection get textDirection => _textDirection;
TextDirection? _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
assert(value != null); assert(value != null);
if (_textDirection == value) if (_textDirection == value)
return; return;
_textDirection = value; _textDirection = value;
notifyListeners(); markNeedsPaint();
} }
Color get surfaceColor => _surfaceColor!; DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
Color? _surfaceColor; 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) { set surfaceColor(Color value) {
assert(value != null); assert(value != null);
if (value == _surfaceColor) if (value == _surfaceColor)
return; return;
_surfaceColor = value; _surfaceColor = value;
notifyListeners(); markNeedsPaint();
} }
bool get isInteractive => _isInteractive!; _SwitchState state;
bool? _isInteractive;
set isInteractive(bool value) { @override
if (value == _isInteractive) { set value(bool? newValue) {
return; 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!; @override
double? _trackInnerLength; void detach() {
set trackInnerLength(double value) { _cachedThumbPainter?.dispose();
if (value == _trackInnerLength) { _cachedThumbPainter = null;
return; 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; Color? _cachedThumbColor;
...@@ -879,12 +984,19 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -879,12 +984,19 @@ class _SwitchPainter extends ToggleablePainter {
// are already in the middle of painting. (In fact, doing so would trigger // are already in the middle of painting. (In fact, doing so would trigger
// an assert). // an assert).
if (!_isPainting) if (!_isPainting)
notifyListeners(); markNeedsPaint();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isToggled = value == true;
} }
@override @override
void paint(Canvas canvas, Size size) { void paint(PaintingContext context, Offset offset) {
final bool isEnabled = isInteractive; final Canvas canvas = context.canvas;
final bool isEnabled = onChanged != null;
final double currentValue = position.value; final double currentValue = position.value;
final double visualPosition; final double visualPosition;
...@@ -917,8 +1029,8 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -917,8 +1029,8 @@ class _SwitchPainter extends ToggleablePainter {
..color = trackColor; ..color = trackColor;
const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius; const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
final Rect trackRect = Rect.fromLTWH( final Rect trackRect = Rect.fromLTWH(
trackHorizontalPadding, offset.dx + trackHorizontalPadding,
(size.height - _kTrackHeight) / 2.0, offset.dy + (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding, size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight, _kTrackHeight,
); );
...@@ -926,11 +1038,11 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -926,11 +1038,11 @@ class _SwitchPainter extends ToggleablePainter {
canvas.drawRRect(trackRRect, paint); canvas.drawRRect(trackRRect, paint);
final Offset thumbPosition = Offset( final Offset thumbPosition = Offset(
kRadialReactionRadius + visualPosition * trackInnerLength, kRadialReactionRadius + visualPosition * _trackInnerLength,
size.height / 2.0, size.height / 2.0,
); );
paintRadialReaction(canvas: canvas, origin: thumbPosition); paintRadialReaction(canvas, offset, thumbPosition);
try { try {
_isPainting = true; _isPainting = true;
...@@ -947,7 +1059,7 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -947,7 +1059,7 @@ class _SwitchPainter extends ToggleablePainter {
final double radius = _kThumbRadius - inset; final double radius = _kThumbRadius - inset;
thumbPainter.paint( thumbPainter.paint(
canvas, canvas,
thumbPosition - Offset(radius, radius), thumbPosition + offset - Offset(radius, radius),
configuration.copyWith(size: Size.fromRadius(radius)), configuration.copyWith(size: Size.fromRadius(radius)),
); );
} finally { } finally {
......
...@@ -6,10 +6,9 @@ import 'package:flutter/animation.dart'; ...@@ -6,10 +6,9 @@ import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/scheduler.dart';
import 'constants.dart'; import 'constants.dart';
import 'material_state.dart';
// Duration of the animation that moves the toggle from one state to another. // Duration of the animation that moves the toggle from one state to another.
const Duration _kToggleDuration = Duration(milliseconds: 200); const Duration _kToggleDuration = Duration(milliseconds: 200);
...@@ -17,21 +16,92 @@ 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. // Duration of the fade animation for the reaction when focus and hover occur.
const Duration _kReactionFadeDuration = Duration(milliseconds: 50); const Duration _kReactionFadeDuration = Duration(milliseconds: 50);
/// A mixin for [StatefulWidget]s that implement material-themed toggleable /// A base class for material style toggleable controls with toggle animations.
/// controls with toggle animations (e.g. [Switch]es, [Checkbox]es, and
/// [Radio]s).
/// ///
/// The mixin implements the logic for toggling the control (e.g. when tapped) /// This class handles storing the current value, dispatching ValueChanged on a
/// and provides a series of animation controllers to transition the control /// tap gesture and driving a changed animation. Subclasses are responsible for
/// from one state to another. It does not have any opinion about the visual /// painting.
/// representation of the toggleable widget. The visuals are defined by a abstract class RenderToggleable extends RenderConstrainedBox {
/// [CustomPainter] passed to the [buildToggleable]. [State] objects using this /// Creates a toggleable render object.
/// mixin should call that method from their [build] method. ///
/// /// The [activeColor], and [inactiveColor] arguments must not be
/// This mixin is used to implement the material components for [Switch], /// null. The [value] can only be null if tristate is true.
/// [Checkbox], and [Radio] controls. RenderToggleable({
@optionalTypeArgs required bool? value,
mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin<S> { 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. /// Used by subclasses to manipulate the visual value of the control.
/// ///
/// Some controls respond to user input by updating their visual value. For /// Some controls respond to user input by updating their visual value. For
...@@ -39,15 +109,16 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin ...@@ -39,15 +109,16 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
/// dragged. These controls manipulate this animation controller to update /// dragged. These controls manipulate this animation controller to update
/// their [position] and eventually trigger an [onChanged] callback when the /// their [position] and eventually trigger an [onChanged] callback when the
/// animation reaches either 0.0 or 1.0. /// animation reaches either 0.0 or 1.0.
@protected
AnimationController get positionController => _positionController; AnimationController get positionController => _positionController;
late AnimationController _positionController; late AnimationController _positionController;
/// The visual value of the control. /// The visual value of the control.
/// ///
/// When the control is inactive, the [value] is false and this animation has /// 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 /// the value 0.0. When the control is active, the value either true or tristate
/// tristate is true and the value is null. When the control is active the /// is true and the value is null. When the control is active the animation
/// animation has a value of 1.0. When the control is changing from inactive /// 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 /// to active (or vice versa), [value] is the target value and this animation
/// gradually updates from 0.0 to 1.0 (or vice versa). /// gradually updates from 0.0 to 1.0 (or vice versa).
CurvedAnimation get position => _position; CurvedAnimation get position => _position;
...@@ -58,66 +129,84 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin ...@@ -58,66 +129,84 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
/// Some controls have a radial ink reaction to user input. This animation /// Some controls have a radial ink reaction to user input. This animation
/// controller can be used to start or stop these ink reactions. /// controller can be used to start or stop these ink reactions.
/// ///
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] /// Subclasses should call [paintRadialReaction] to actually paint the radial
/// may be used. /// reaction.
@protected
AnimationController get reactionController => _reactionController; AnimationController get reactionController => _reactionController;
late AnimationController _reactionController; late AnimationController _reactionController;
late Animation<double> _reaction;
/// The visual value of the radial reaction animation. /// Used by subclasses to control the radial reaction's opacity animation for
/// [hasFocus] changes.
/// ///
/// Some controls have a radial ink reaction to user input. This animation /// Some controls have a radial ink reaction to focus. This animation
/// controls the progress of these ink reactions. /// controller can be used to start or stop these ink reaction fade-ins and
/// fade-outs.
/// ///
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] /// Subclasses should call [paintRadialReaction] to actually paint the radial
/// may be used. /// reaction.
Animation<double> get reaction => _reaction; @protected
late Animation<double> _reaction; AnimationController get reactionFocusFadeController => _reactionFocusFadeController;
late AnimationController _reactionFocusFadeController;
late Animation<double> _reactionFocusFade;
/// Controls the radial reaction's opacity animation for hover changes. /// Used by subclasses to control the radial reaction's opacity animation for
/// [hovering] changes.
/// ///
/// Some controls have a radial ink reaction to pointer hover. This animation /// Some controls have a radial ink reaction to pointer hover. This animation
/// controls these ink reaction fade-ins and /// controller can be used to start or stop these ink reaction fade-ins and
/// fade-outs. /// fade-outs.
/// ///
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] /// Subclasses should call [paintRadialReaction] to actually paint the radial
/// may be used. /// reaction.
Animation<double> get reactionHoverFade => _reactionHoverFade; @protected
late Animation<double> _reactionHoverFade; AnimationController get reactionHoverFadeController => _reactionHoverFadeController;
late AnimationController _reactionHoverFadeController; late AnimationController _reactionHoverFadeController;
late Animation<double> _reactionHoverFade;
/// Controls the radial reaction's opacity animation for focus changes. /// True if this toggleable has the input focus.
/// bool get hasFocus => _hasFocus;
/// Some controls have a radial ink reaction to focus. This animation bool _hasFocus;
/// controls these ink reaction fade-ins and fade-outs. set hasFocus(bool value) {
/// assert(value != null);
/// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] if (value == _hasFocus)
/// may be used. return;
Animation<double> get reactionFocusFade => _reactionFocusFade; _hasFocus = value;
late Animation<double> _reactionFocusFade; if (_hasFocus) {
late AnimationController _reactionFocusFadeController; _reactionFocusFadeController.forward();
} else {
/// Whether [value] of this control can be changed by user interaction. _reactionFocusFadeController.reverse();
/// }
/// The control is considered interactive if the [onChanged] callback is markNeedsPaint();
/// 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 final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{ /// True if this toggleable is being hovered over by a pointer.
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap), 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();
}
/// Called when the control changes value. /// The [TickerProvider] for the [AnimationController]s that run the animations.
/// TickerProvider get vsync => _vsync;
/// If the control is tapped, [onChanged] is called immediately with the new TickerProvider _vsync;
/// value. set vsync(TickerProvider value) {
/// assert(value != null);
/// The control is considered interactive (see [isInteractive]) if this if (value == _vsync)
/// callback is non-null. If the callback is null, then the control is return;
/// disabled, and non-interactive. A disabled checkbox, for example, is _vsync = value;
/// displayed using a grey color and its value cannot be changed. positionController.resync(vsync);
ValueChanged<bool?>? get onChanged; reactionController.resync(vsync);
}
/// False if this control is "inactive" (not checked, off, or unselected). /// False if this control is "inactive" (not checked, off, or unselected).
/// ///
...@@ -128,62 +217,17 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin ...@@ -128,62 +217,17 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
/// When the value changes, this object starts the [positionController] and /// When the value changes, this object starts the [positionController] and
/// [position] animations to animate the visual appearance of the control to /// [position] animations to animate the visual appearance of the control to
/// the new value. /// the new value.
bool? get value; bool? get value => _value;
bool? _value;
/// If true, [value] can be true, false, or null, otherwise [value] must set value(bool? value) {
/// be true or false. assert(tristate || value != null);
/// if (value == _value)
/// When [tristate] is true and [value] is null, then the control is return;
/// considered to be in its third or "indeterminate" state. _value = value;
bool get tristate; markNeedsSemanticsUpdate();
_position
@override ..curve = Curves.easeIn
void initState() { ..reverseCurve = Curves.easeOut;
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() {
if (tristate) { if (tristate) {
if (value == null) if (value == null)
_positionController.value = 0.0; _positionController.value = 0.0;
...@@ -199,235 +243,94 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin ...@@ -199,235 +243,94 @@ mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin
} }
} }
@override /// If true, [value] can be true, false, or null, otherwise [value] must
void dispose() { /// be true or false.
_positionController.dispose();
_reactionController.dispose();
_reactionHoverFadeController.dispose();
_reactionFocusFadeController.dispose();
super.dispose();
}
/// The most recent [Offset] at which a pointer touched the Toggleable.
/// ///
/// This is null if currently no pointer is touching the Toggleable or if /// When [tristate] is true and [value] is null, then the control is
/// [isInteractive] is false. /// considered to be in its third or "indeterminate" state.
Offset? get downPosition => _downPosition; bool get tristate => _tristate;
Offset? _downPosition; bool _tristate;
set tristate(bool value) {
void _handleTapDown(TapDownDetails details) { assert(tristate != null);
if (isInteractive) { if (value == _tristate)
setState(() {
_downPosition = details.localPosition;
});
_reactionController.forward();
}
}
void _handleTap([Intent? _]) {
if (!isInteractive)
return; return;
switch (value) { _tristate = value;
case false: markNeedsSemanticsUpdate();
onChanged!(true);
break;
case true:
onChanged!(tristate ? null : false);
break;
case null:
onChanged!(false);
break;
}
context.findRenderObject()!.sendSemanticsEvent(const TapSemanticEvent());
}
void _handleTapEnd([TapUpDetails? _]) {
if (_downPosition != null) {
setState(() { _downPosition = null; });
}
_reactionController.reverse();
}
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
setState(() { _focused = focused; });
if (focused) {
_reactionFocusFadeController.forward();
} else {
_reactionFocusFadeController.reverse();
}
}
}
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
if (hovering) {
_reactionHoverFadeController.forward();
} else {
_reactionHoverFadeController.reverse();
}
}
}
/// 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 /// The color that should be used in the active state (i.e., when [value] is true).
/// [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]. /// For example, a checkbox should use this color when checked.
Animation<double> get position => _position!; Color get activeColor => _activeColor;
Animation<double>? _position; Color _activeColor;
set position(Animation<double> value) { set activeColor(Color value) {
if (value == _position) { assert(value != null);
if (value == _activeColor)
return; return;
} _activeColor = value;
_position?.removeListener(notifyListeners); markNeedsPaint();
value.addListener(notifyListeners);
_position = value;
notifyListeners();
} }
/// The visual value of the radial reaction animation. /// The color that should be used in the inactive state (i.e., when [value] is false).
/// ///
/// Usually set to [ToggleableStateMixin.reaction]. /// For example, a checkbox should use this color when unchecked.
Animation<double> get reaction => _reaction!; Color get inactiveColor => _inactiveColor;
Animation<double>? _reaction; Color _inactiveColor;
set reaction(Animation<double> value) { set inactiveColor(Color value) {
if (value == _reaction) { assert(value != null);
if (value == _inactiveColor)
return; return;
} _inactiveColor = value;
_reaction?.removeListener(notifyListeners); markNeedsPaint();
value.addListener(notifyListeners);
_reaction = value;
notifyListeners();
} }
/// Controls the radial reaction's opacity animation for focus changes. /// The color that should be used for the reaction when [hovering] is true.
/// ///
/// Usually set to [ToggleableStateMixin.reactionFocusFade]. /// Used when the toggleable needs to change the reaction color/transparency,
Animation<double> get reactionFocusFade => _reactionFocusFade!; /// when it is being hovered over.
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]. /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Animation<double> get reactionHoverFade => _reactionHoverFade!; Color get hoverColor => _hoverColor;
Animation<double>? _reactionHoverFade; Color _hoverColor;
set reactionHoverFade(Animation<double> value) { set hoverColor(Color value) {
if (value == _reactionHoverFade) { assert(value != null);
if (value == _hoverColor)
return; return;
} _hoverColor = value;
_reactionHoverFade?.removeListener(notifyListeners); markNeedsPaint();
value.addListener(notifyListeners);
_reactionHoverFade = value;
notifyListeners();
} }
/// The color that should be used in the active state (i.e., when /// The color that should be used for the reaction when [hasFocus] is true.
/// [ToggleableStateMixin.value] is true).
/// ///
/// For example, a checkbox should use this color when checked. /// Used when the toggleable needs to change the reaction color/transparency,
Color get activeColor => _activeColor!; /// when it has focus.
Color? _activeColor; ///
set activeColor(Color value) { /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
if (_activeColor == value) { Color get focusColor => _focusColor;
Color _focusColor;
set focusColor(Color value) {
assert(value != null);
if (value == _focusColor)
return; return;
} _focusColor = value;
_activeColor = value; markNeedsPaint();
notifyListeners();
} }
/// The color that should be used in the inactive state (i.e., when /// The color that should be used for the reaction when the toggleable is
/// [ToggleableStateMixin.value] is false). /// active.
/// ///
/// For example, a checkbox should use this color when unchecked. /// Used when the toggleable needs to change the reaction color/transparency
Color get inactiveColor => _inactiveColor!; /// that is displayed when the toggleable is active and tapped.
Color? _inactiveColor; ///
set inactiveColor(Color value) { /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
if (_inactiveColor == value) { Color? get reactionColor => _reactionColor;
Color? _reactionColor;
set reactionColor(Color? value) {
assert(value != null);
if (value == _reactionColor)
return; return;
} _reactionColor = value;
_inactiveColor = value; markNeedsPaint();
notifyListeners();
} }
/// The color that should be used for the reaction when the toggleable is /// The color that should be used for the reaction when the toggleable is
...@@ -435,161 +338,184 @@ abstract class ToggleablePainter extends ChangeNotifier implements CustomPainter ...@@ -435,161 +338,184 @@ abstract class ToggleablePainter extends ChangeNotifier implements CustomPainter
/// ///
/// Used when the toggleable needs to change the reaction color/transparency /// Used when the toggleable needs to change the reaction color/transparency
/// that is displayed when the toggleable is inactive and tapped. /// that is displayed when the toggleable is inactive and tapped.
Color get inactiveReactionColor => _inactiveReactionColor!; ///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
Color? get inactiveReactionColor => _inactiveReactionColor;
Color? _inactiveReactionColor; Color? _inactiveReactionColor;
set inactiveReactionColor(Color value) { set inactiveReactionColor(Color? value) {
if (value == _inactiveReactionColor) { assert(value != null);
if (value == _inactiveReactionColor)
return; return;
}
_inactiveReactionColor = value; _inactiveReactionColor = value;
notifyListeners(); markNeedsPaint();
} }
/// The color that should be used for the reaction when the toggleable is /// The splash radius for the radial reaction.
/// active. double get splashRadius => _splashRadius;
/// double _splashRadius;
/// Used when the toggleable needs to change the reaction color/transparency set splashRadius(double value) {
/// that is displayed when the toggleable is active and tapped. if (value == _splashRadius)
Color get reactionColor => _reactionColor!;
Color? _reactionColor;
set reactionColor(Color value) {
if (value == _reactionColor) {
return; return;
} _splashRadius = value;
_reactionColor = value; markNeedsPaint();
notifyListeners();
} }
/// The color that should be used for the reaction when [isHovered] is true. /// Called when the control changes value.
/// ///
/// Used when the toggleable needs to change the reaction color/transparency, /// If the control is tapped, [onChanged] is called immediately with the new
/// when it is being hovered over. /// value.
Color get hoverColor => _hoverColor!; ///
Color? _hoverColor; /// The control is considered interactive (see [isInteractive]) if this
set hoverColor(Color value) { /// callback is non-null. If the callback is null, then the control is
if (value == _hoverColor) { /// 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; return;
final bool wasInteractive = isInteractive;
_onChanged = value;
if (wasInteractive != isInteractive) {
markNeedsPaint();
markNeedsSemanticsUpdate();
} }
_hoverColor = value;
notifyListeners();
} }
/// The color that should be used for the reaction when [isFocused] is true. /// Whether [value] of this control can be changed by user interaction.
/// ///
/// Used when the toggleable needs to change the reaction color/transparency, /// The control is considered interactive if the [onChanged] callback is
/// when it has focus. /// non-null. If the callback is null, then the control is disabled, and
Color get focusColor => _focusColor!; /// non-interactive. A disabled checkbox, for example, is displayed using a
Color? _focusColor; /// grey color and its value cannot be changed.
set focusColor(Color value) { bool get isInteractive => onChanged != null;
if (value == _focusColor) {
return; 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;
}
} }
_focusColor = value;
notifyListeners();
} }
/// The splash radius for the radial reaction. @override
double get splashRadius => _splashRadius!; void detach() {
double? _splashRadius; _positionController.stop();
set splashRadius(double value) { _reactionController.stop();
if (value == _splashRadius) { _reactionHoverFadeController.stop();
return; _reactionFocusFadeController.stop();
} super.detach();
_splashRadius = value;
notifyListeners();
} }
/// The [Offset] within the Toggleable at which a pointer touched the Toggleable. void _handleTapDown(TapDownDetails details) {
/// if (isInteractive) {
/// This is null if currently no pointer is touching the Toggleable. _downPosition = globalToLocal(details.globalPosition);
/// _reactionController.forward();
/// 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. void _handleTap() {
bool get isFocused => _isFocused!; if (!isInteractive)
bool? _isFocused;
set isFocused(bool? value) {
if (value == _isFocused) {
return; return;
switch (value) {
case false:
onChanged!(true);
break;
case true:
onChanged!(tristate ? null : false);
break;
case null:
onChanged!(false);
break;
} }
_isFocused = value; sendSemanticsEvent(const TapSemanticEvent());
notifyListeners();
} }
/// True if this toggleable is being hovered over by a pointer. void _handleTapUp(TapUpDetails details) {
bool get isHovered => _isHovered!; _downPosition = null;
bool? _isHovered; if (isInteractive)
set isHovered(bool? value) { _reactionController.reverse();
if (value == _isHovered) { }
return;
} void _handleTapCancel() {
_isHovered = value; _downPosition = null;
notifyListeners(); if (isInteractive)
_reactionController.reverse();
}
@override
bool hitTestSelf(Offset position) => true;
@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. /// 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 /// 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 /// origin is the center point of the reaction (usually distinct from the
/// [downPosition] at which the user interacted with the control). /// point at which the user interacted with the control, which is handled
void paintRadialReaction({ /// automatically).
required Canvas canvas, void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
Offset offset = Offset.zero, if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) {
required Offset origin,
}) {
if (!reaction.isDismissed || !reactionFocusFade.isDismissed || !reactionHoverFade.isDismissed) {
final Paint reactionPaint = Paint() final Paint reactionPaint = Paint()
..color = Color.lerp( ..color = Color.lerp(
Color.lerp( Color.lerp(
Color.lerp(inactiveReactionColor, reactionColor, position.value), Color.lerp(inactiveReactionColor, reactionColor, _position.value),
hoverColor, hoverColor,
reactionHoverFade.value, _reactionHoverFade.value,
), ),
focusColor, 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>( final Animatable<double> radialReactionRadiusTween = Tween<double>(
begin: 0.0, begin: 0.0,
end: splashRadius, end: splashRadius,
); );
final double reactionRadius = isFocused || isHovered final double reactionRadius = hasFocus || hovering
? splashRadius ? splashRadius
: radialReactionRadiusTween.evaluate(reaction); : radialReactionRadiusTween.evaluate(_reaction);
if (reactionRadius > 0.0) { if (reactionRadius > 0.0) {
canvas.drawCircle(center + offset, reactionRadius, reactionPaint); canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
} }
} }
} }
@override @override
void dispose() { void describeSemanticsConfiguration(SemanticsConfiguration config) {
_position?.removeListener(notifyListeners); super.describeSemanticsConfiguration(config);
_reaction?.removeListener(notifyListeners);
_reactionFocusFade?.removeListener(notifyListeners);
_reactionHoverFade?.removeListener(notifyListeners);
super.dispose();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
@override
bool? hitTest(Offset position) => null;
@override config.isEnabled = isInteractive;
SemanticsBuilderCallback? get semanticsBuilder => null; if (isInteractive)
config.onTap = _handleTap;
}
@override @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));
}
} }
...@@ -99,7 +99,7 @@ void main() { ...@@ -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, hasCheckedState: true,
hasEnabledState: true, hasEnabledState: true,
// isFocusable is delayed by 1 frame. // isFocusable is delayed by 1 frame.
...@@ -108,7 +108,7 @@ void main() { ...@@ -108,7 +108,7 @@ void main() {
await tester.pump(); await tester.pump();
// isFocusable should be false now after the 1 frame delay. // 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, hasCheckedState: true,
hasEnabledState: true, hasEnabledState: true,
)); ));
...@@ -120,7 +120,7 @@ void main() { ...@@ -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, hasCheckedState: true,
hasEnabledState: true, hasEnabledState: true,
isChecked: true, isChecked: true,
...@@ -290,7 +290,7 @@ void main() { ...@@ -290,7 +290,7 @@ void main() {
); );
await tester.tap(find.byType(Checkbox)); 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(checkboxValue, true);
expect(semanticEvent, <String, dynamic>{ expect(semanticEvent, <String, dynamic>{
...@@ -319,8 +319,10 @@ void main() { ...@@ -319,8 +319,10 @@ void main() {
); );
} }
RenderBox getCheckboxRenderer() { RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox)); return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame(false)); await tester.pumpWidget(buildFrame(false));
...@@ -375,8 +377,10 @@ void main() { ...@@ -375,8 +377,10 @@ void main() {
); );
} }
RenderBox getCheckboxRenderer() { RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox)); return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame(checkColor: checkColor)); await tester.pumpWidget(buildFrame(checkColor: checkColor));
...@@ -449,10 +453,11 @@ void main() { ...@@ -449,10 +453,11 @@ void main() {
paints paints
..circle(color: Colors.orange[500]) ..circle(color: Colors.orange[500])
..drrect( ..drrect(
color: const Color(0x8a000000), color: const Color(0x8a000000),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)), outer: RRect.fromLTRBR(
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, const Radius.circular(-1.0)), 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. // Check what happens when disabled.
...@@ -464,10 +469,11 @@ void main() { ...@@ -464,10 +469,11 @@ void main() {
Material.of(tester.element(find.byType(Checkbox))), Material.of(tester.element(find.byType(Checkbox))),
paints paints
..drrect( ..drrect(
color: const Color(0x61000000), color: const Color(0x61000000),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)), outer: RRect.fromLTRBR(
inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, const Radius.circular(-1.0)), 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() { ...@@ -819,8 +825,10 @@ void main() {
); );
} }
RenderBox getCheckboxRenderer() { RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox)); return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame(enabled: true)); await tester.pumpWidget(buildFrame(enabled: true));
...@@ -870,8 +878,10 @@ void main() { ...@@ -870,8 +878,10 @@ void main() {
); );
} }
RenderBox getCheckboxRenderer() { RenderToggleable getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox)); return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame()); await tester.pumpWidget(buildFrame());
...@@ -927,9 +937,11 @@ void main() { ...@@ -927,9 +937,11 @@ void main() {
paints paints
..drrect( ..drrect(
color: const Color(0xfff44336), color: const Color(0xfff44336),
outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(5)), outer: RRect.fromLTRBR(
inner: RRect.fromLTRBR(19.0, 19.0, 29.0, 29.0, const Radius.circular(1)), 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() { ...@@ -1172,29 +1184,6 @@ void main() {
await gesture.up(); 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 { class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
......
...@@ -308,7 +308,7 @@ void main() { ...@@ -308,7 +308,7 @@ void main() {
)); ));
await tester.tap(find.byKey(key)); 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(radioValue, 1);
expect(semanticEvent, <String, dynamic>{ expect(semanticEvent, <String, dynamic>{
...@@ -1078,29 +1078,4 @@ void main() { ...@@ -1078,29 +1078,4 @@ void main() {
reason: 'Hovered Radio should use overlay color $hoverOverlayColor over $hoverColor', 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();
});
} }
...@@ -37,7 +37,7 @@ void main() { ...@@ -37,7 +37,7 @@ void main() {
expect(log, equals(<dynamic>[false, '-', false])); 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); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(wrap( await tester.pumpWidget(wrap(
child: Column( child: Column(
......
...@@ -301,7 +301,8 @@ void main() { ...@@ -301,7 +301,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x52000000), // Black with 32% opacity 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -316,7 +317,8 @@ void main() { ...@@ -316,7 +317,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.blue[600]!.withAlpha(0x80), 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -349,7 +351,8 @@ void main() { ...@@ -349,7 +351,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -380,7 +383,8 @@ void main() { ...@@ -380,7 +383,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -423,7 +427,8 @@ void main() { ...@@ -423,7 +427,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.blue[500], 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -437,7 +442,8 @@ void main() { ...@@ -437,7 +442,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.green[500], 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -522,19 +528,19 @@ void main() { ...@@ -522,19 +528,19 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(value, isFalse); expect(value, isFalse);
final ToggleableStateMixin state = tester.state<ToggleableStateMixin>( final RenderToggleable renderObject = tester.renderObject<RenderToggleable>(
find.descendant( find.descendant(
of: find.byType(Switch), of: find.byType(Switch),
matching: find.byWidgetPredicate( 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.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, isFalse); expect(value, isFalse);
expect(state.position.value, 0); expect(renderObject.position.value, 0);
// Move past the middle. // Move past the middle.
gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center); gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
...@@ -543,12 +549,12 @@ void main() { ...@@ -543,12 +549,12 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(value, isTrue); expect(value, isTrue);
expect(state.position.value, greaterThan(0.5)); expect(renderObject.position.value, greaterThan(0.5));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, isTrue); 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. // Now move back to the left, the revert animation should play.
gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center); gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
...@@ -557,12 +563,12 @@ void main() { ...@@ -557,12 +563,12 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(value, isTrue); expect(value, isTrue);
expect(state.position.value, lessThan(0.5)); expect(renderObject.position.value, lessThan(0.5));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, isTrue); expect(value, isTrue);
expect(state.position.value, 1.0); expect(renderObject.position.value, 1.0);
}); });
testWidgets('switch has semantic events', (WidgetTester tester) async { testWidgets('switch has semantic events', (WidgetTester tester) async {
...@@ -595,7 +601,7 @@ void main() { ...@@ -595,7 +601,7 @@ void main() {
), ),
); );
await tester.tap(find.byType(Switch)); 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(value, true);
expect(semanticEvent, <String, dynamic>{ expect(semanticEvent, <String, dynamic>{
...@@ -744,7 +750,8 @@ void main() { ...@@ -744,7 +750,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), 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: Colors.orange[500])
..circle(color: const Color(0x33000000)) ..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
...@@ -762,7 +769,8 @@ void main() { ...@@ -762,7 +769,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x52000000), 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: Colors.orange[500])
..circle(color: const Color(0x33000000)) ..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
...@@ -780,7 +788,8 @@ void main() { ...@@ -780,7 +788,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x1f000000), 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -845,7 +854,8 @@ void main() { ...@@ -845,7 +854,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -865,7 +875,8 @@ void main() { ...@@ -865,7 +875,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), 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: Colors.orange[500])
..circle(color: const Color(0x33000000)) ..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
...@@ -881,7 +892,8 @@ void main() { ...@@ -881,7 +892,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x1f000000), 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -1058,7 +1070,8 @@ void main() { ...@@ -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; }); stateSetter(() { value = false; });
await tester.pump(); await tester.pump();
...@@ -1066,12 +1079,14 @@ void main() { ...@@ -1066,12 +1079,14 @@ void main() {
stateSetter(() { enabled = false; }); stateSetter(() { enabled = false; });
await tester.pump(); 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(updatedSwitchRenderObject.isInteractive, false);
expect(updatedSwitchState.position.isCompleted, false); expect(updatedSwitchRenderObject, oldSwitchRenderObject);
expect(updatedSwitchState.position.isDismissed, false); expect(updatedSwitchRenderObject.position.isCompleted, false);
expect(updatedSwitchRenderObject.position.isDismissed, false);
}); });
testWidgets('Switch thumb color resolves in active/enabled states', (WidgetTester tester) async { testWidgets('Switch thumb color resolves in active/enabled states', (WidgetTester tester) async {
...@@ -1122,7 +1137,8 @@ void main() { ...@@ -1122,7 +1137,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -1138,7 +1154,8 @@ void main() { ...@@ -1138,7 +1154,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -1154,7 +1171,8 @@ void main() { ...@@ -1154,7 +1171,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x52000000), // Black with 32% opacity, 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -1170,7 +1188,8 @@ void main() { ...@@ -1170,7 +1188,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -1227,7 +1246,8 @@ void main() { ...@@ -1227,7 +1246,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), 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(0x1f000000))
..circle(color: const Color(0x33000000)) ..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
...@@ -1248,7 +1268,8 @@ void main() { ...@@ -1248,7 +1268,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), 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(0x1f000000))
..circle(color: const Color(0x33000000)) ..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
...@@ -1306,7 +1327,8 @@ void main() { ...@@ -1306,7 +1327,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: inactiveDisabledTrackColor, 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', reason: 'Inactive disabled switch track should use this value',
); );
...@@ -1318,7 +1340,8 @@ void main() { ...@@ -1318,7 +1340,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: activeDisabledTrackColor, 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', reason: 'Active disabled switch should match these colors',
); );
...@@ -1330,7 +1353,8 @@ void main() { ...@@ -1330,7 +1353,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: inactiveEnabledTrackColor, 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', reason: 'Inactive enabled switch should match these colors',
); );
...@@ -1342,7 +1366,8 @@ void main() { ...@@ -1342,7 +1366,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: inactiveDisabledTrackColor, 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', reason: 'Inactive disabled switch should match these colors',
); );
}); });
...@@ -1395,7 +1420,8 @@ void main() { ...@@ -1395,7 +1420,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: focusedTrackColor, 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', reason: 'Inactive enabled switch should match these colors',
); );
...@@ -1411,7 +1437,8 @@ void main() { ...@@ -1411,7 +1437,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: hoveredTrackColor, 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', reason: 'Inactive enabled switch should match these colors',
); );
}); });
...@@ -1461,7 +1488,8 @@ void main() { ...@@ -1461,7 +1488,8 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, 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(0x33000000))
..circle(color: const Color(0x24000000)) ..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000)) ..circle(color: const Color(0x1f000000))
...@@ -1610,27 +1638,4 @@ void main() { ...@@ -1610,27 +1638,4 @@ void main() {
reason: 'Hovered Switch should use overlay color $hoverOverlayColor over $hoverColor', 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();
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment