Unverified Commit 67edb63d authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Re-land "Toggable Refactor (#76745)" (#77263)

parent 90f353f9
...@@ -238,12 +238,7 @@ void main() { ...@@ -238,12 +238,7 @@ void main() {
test('Checkbox has correct Android semantics', () async { test('Checkbox has correct Android semantics', () async {
Future<AndroidSemanticsNode> getCheckboxSemantics(String key) async { Future<AndroidSemanticsNode> getCheckboxSemantics(String key) async {
return getSemantics( return getSemantics(find.byValueKey(key));
find.descendant(
of: find.byValueKey(key),
matching: find.byType('_CheckboxRenderObjectWidget'),
),
);
} }
expect( expect(
await getCheckboxSemantics(checkboxKeyValue), await getCheckboxSemantics(checkboxKeyValue),
...@@ -290,12 +285,7 @@ void main() { ...@@ -290,12 +285,7 @@ void main() {
}); });
test('Radio has correct Android semantics', () async { test('Radio has correct Android semantics', () async {
Future<AndroidSemanticsNode> getRadioSemantics(String key) async { Future<AndroidSemanticsNode> getRadioSemantics(String key) async {
return getSemantics( return getSemantics(find.byValueKey(key));
find.descendant(
of: find.byValueKey(key),
matching: find.byType('_RadioRenderObjectWidget'),
),
);
} }
expect( expect(
await getRadioSemantics(radio2KeyValue), await getRadioSemantics(radio2KeyValue),
...@@ -331,12 +321,7 @@ void main() { ...@@ -331,12 +321,7 @@ void main() {
}); });
test('Switch has correct Android semantics', () async { test('Switch has correct Android semantics', () async {
Future<AndroidSemanticsNode> getSwitchSemantics(String key) async { Future<AndroidSemanticsNode> getSwitchSemantics(String key) async {
return getSemantics( return getSemantics(find.byValueKey(key));
find.descendant(
of: find.byValueKey(key),
matching: find.byType('_SwitchRenderObjectWidget'),
),
);
} }
expect( expect(
await getSwitchSemantics(switchKeyValue), await getSwitchSemantics(switchKeyValue),
...@@ -374,12 +359,7 @@ void main() { ...@@ -374,12 +359,7 @@ void main() {
// Regression test for https://github.com/flutter/flutter/issues/20820. // Regression test for https://github.com/flutter/flutter/issues/20820.
test('Switch can be labeled', () async { test('Switch can be labeled', () async {
Future<AndroidSemanticsNode> getSwitchSemantics(String key) async { Future<AndroidSemanticsNode> getSwitchSemantics(String key) async {
return getSemantics( return getSemantics(find.byValueKey(key));
find.descendant(
of: find.byValueKey(key),
matching: find.byType('_SwitchRenderObjectWidget'),
),
);
} }
expect( expect(
await getSwitchSemantics(labeledSwitchKeyValue), await getSwitchSemantics(labeledSwitchKeyValue),
......
...@@ -290,56 +290,39 @@ class Checkbox extends StatefulWidget { ...@@ -290,56 +290,39 @@ class Checkbox extends StatefulWidget {
_CheckboxState createState() => _CheckboxState(); _CheckboxState createState() => _CheckboxState();
} }
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, ToggleableStateMixin {
bool get enabled => widget.onChanged != null; final _CheckboxPainter _painter = _CheckboxPainter();
late Map<Type, Action<Intent>> _actionMap; bool? _previousValue;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_actionMap = <Type, Action<Intent>>{ _previousValue = widget.value;
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
};
} }
void _actionHandler(ActivateIntent intent) { @override
if (widget.onChanged != null) { void didUpdateWidget(Checkbox oldWidget) {
switch (widget.value) { super.didUpdateWidget(oldWidget);
case false: if (oldWidget.value != widget.value) {
widget.onChanged!(true); _previousValue = oldWidget.value;
break; animateToValue();
case true:
widget.onChanged!(widget.tristate ? null : false);
break;
case null:
widget.onChanged!(false);
break;
}
} }
final RenderObject renderObject = context.findRenderObject()!;
renderObject.sendSemanticsEvent(const TapSemanticEvent());
} }
bool _focused = false; @override
void _handleFocusHighlightChanged(bool focused) { void dispose() {
if (focused != _focused) { _painter.dispose();
setState(() { _focused = focused; }); super.dispose();
}
} }
bool _hovering = false; @override
void _handleHoverChanged(bool hovering) { ValueChanged<bool?>? get onChanged => widget.onChanged;
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
}
}
Set<MaterialState> get _states => <MaterialState>{ @override
if (!enabled) MaterialState.disabled, bool get tristate => widget.tristate;
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused, @override
if (widget.value == null || widget.value!) MaterialState.selected, bool? get value => widget.value;
};
MaterialStateProperty<Color?> get _widgetFillColor { MaterialStateProperty<Color?> get _widgetFillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
...@@ -386,14 +369,17 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -386,14 +369,17 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
break; break;
} }
size += effectiveVisualDensity.baseSizeAdjustment; size += effectiveVisualDensity.baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states) final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
?? themeData.checkboxTheme.mouseCursor?.resolve(_states) return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states); ?? themeData.checkboxTheme.mouseCursor?.resolve(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)
...@@ -403,13 +389,13 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -403,13 +389,13 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
?? 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)
...@@ -426,194 +412,95 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -426,194 +412,95 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
?? 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 FocusableActionDetector( return Semantics(
actions: _actionMap, checked: widget.value == true,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
enabled: enabled, size: size,
onShowFocusHighlight: _handleFocusHighlightChanged, painter: _painter
onShowHoverHighlight: _handleHoverChanged, ..position = position
mouseCursor: effectiveMouseCursor, ..reaction = reaction
child: Builder( ..reactionFocusFade = reactionFocusFade
builder: (BuildContext context) { ..reactionHoverFade = reactionHoverFade
return _CheckboxRenderObjectWidget( ..inactiveReactionColor = effectiveInactivePressedOverlayColor
value: widget.value, ..reactionColor = effectiveActivePressedOverlayColor
tristate: widget.tristate, ..hoverColor = effectiveHoverOverlayColor
activeColor: effectiveActiveColor, ..focusColor = effectiveFocusOverlayColor
checkColor: effectiveCheckColor, ..splashRadius = widget.splashRadius ?? themeData.checkboxTheme.splashRadius ?? kRadialReactionRadius
inactiveColor: effectiveInactiveColor, ..downPosition = downPosition
focusColor: effectiveFocusOverlayColor, ..isFocused = states.contains(MaterialState.focused)
hoverColor: effectiveHoverOverlayColor, ..isHovered = states.contains(MaterialState.hovered)
reactionColor: effectiveActivePressedOverlayColor, ..activeColor = effectiveActiveColor
inactiveReactionColor: effectiveInactivePressedOverlayColor, ..inactiveColor = effectiveInactiveColor
splashRadius: widget.splashRadius ?? themeData.checkboxTheme.splashRadius ?? kRadialReactionRadius, ..checkColor = effectiveCheckColor
onChanged: widget.onChanged, ..value = value
additionalConstraints: additionalConstraints, ..previousValue = _previousValue
vsync: this, ..shape = widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder(
hasFocus: _focused, borderRadius: BorderRadius.all(Radius.circular(1.0))
hovering: _hovering, )
side: widget.side ?? themeData.checkboxTheme.side, ..side = widget.side ?? themeData.checkboxTheme.side,
shape: widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(1.0)),
),
);
},
), ),
); );
} }
} }
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
const _CheckboxRenderObjectWidget({
Key? key,
required this.value,
required this.tristate,
required this.activeColor,
required this.checkColor,
required this.inactiveColor,
required this.focusColor,
required this.hoverColor,
required this.reactionColor,
required this.inactiveReactionColor,
required this.splashRadius,
required this.onChanged,
required this.vsync,
required this.additionalConstraints,
required this.hasFocus,
required this.hovering,
required this.shape,
required this.side,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null),
assert(inactiveColor != null),
assert(vsync != null),
super(key: key);
final bool? value;
final bool tristate;
final bool hasFocus;
final bool hovering;
final Color activeColor;
final Color checkColor;
final Color inactiveColor;
final Color focusColor;
final Color hoverColor;
final Color reactionColor;
final Color inactiveReactionColor;
final double splashRadius;
final ValueChanged<bool?>? onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
final OutlinedBorder shape;
final BorderSide? side;
@override
_RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox(
value: value,
tristate: tristate,
activeColor: activeColor,
checkColor: checkColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
vsync: vsync,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
shape: shape,
side: side,
);
@override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject
// The `tristate` must be changed before `value` due to the assertion at
// the beginning of `set value`.
..tristate = tristate
..value = value
..activeColor = activeColor
..checkColor = checkColor
..inactiveColor = inactiveColor
..focusColor = focusColor
..hoverColor = hoverColor
..reactionColor = reactionColor
..inactiveReactionColor = inactiveReactionColor
..splashRadius = splashRadius
..onChanged = onChanged
..additionalConstraints = additionalConstraints
..vsync = vsync
..hasFocus = hasFocus
..hovering = hovering
..shape = shape
..side = side;
}
}
const double _kEdgeSize = Checkbox.width; const double _kEdgeSize = Checkbox.width;
const double _kStrokeWidth = 2.0; const double _kStrokeWidth = 2.0;
class _RenderCheckbox extends RenderToggleable { class _CheckboxPainter extends ToggleablePainter {
_RenderCheckbox({ Color get checkColor => _checkColor!;
bool? value, Color? _checkColor;
required bool tristate, set checkColor(Color value) {
required Color activeColor, if (_checkColor == value) {
required this.checkColor, return;
required Color inactiveColor, }
Color? focusColor, _checkColor = value;
Color? hoverColor, notifyListeners();
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; bool? get value => _value;
Color checkColor; bool? _value;
OutlinedBorder shape; set value(bool? value) {
BorderSide? side; if (_value == value) {
return;
}
_value = value;
notifyListeners();
}
@override bool? get previousValue => _previousValue;
set value(bool? newValue) { bool? _previousValue;
if (newValue == value) set previousValue(bool? value) {
if (_previousValue == value) {
return;
}
_previousValue = value;
notifyListeners();
}
OutlinedBorder get shape => _shape!;
OutlinedBorder? _shape;
set shape(OutlinedBorder value) {
if (_shape == value) {
return; return;
_oldValue = value; }
super.value = newValue; _shape = value;
notifyListeners();
} }
@override BorderSide? get side => _side;
void describeSemanticsConfiguration(SemanticsConfiguration config) { BorderSide? _side;
super.describeSemanticsConfiguration(config); set side(BorderSide? value) {
config.isChecked = value == true; if (_side == value) {
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.
...@@ -644,10 +531,11 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -644,10 +531,11 @@ class _RenderCheckbox extends RenderToggleable {
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) {
shape = shape.copyWith(side: BorderSide(width: 2, color: paint.color)); resolvedShape = resolvedShape.copyWith(side: BorderSide(width: 2, color: paint.color));
} }
shape.copyWith(side: side).paint(canvas, outer); resolvedShape.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) {
...@@ -686,19 +574,18 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -686,19 +574,18 @@ class _RenderCheckbox extends RenderToggleable {
} }
@override @override
void paint(PaintingContext context, Offset offset) { void paint(Canvas canvas, Size size) {
final Canvas canvas = context.canvas; paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));
paintRadialReaction(canvas, offset, size.center(Offset.zero));
final Paint strokePaint = _createStrokePaint(); final Paint strokePaint = _createStrokePaint();
final Offset origin = offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0 as Offset); final Offset origin = 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 (_oldValue == false || value == false) { if (previousValue == 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);
...@@ -710,7 +597,7 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -710,7 +597,7 @@ class _RenderCheckbox extends RenderToggleable {
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 (_oldValue == null || value == null) if (previousValue == 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);
...@@ -722,7 +609,7 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -722,7 +609,7 @@ class _RenderCheckbox extends RenderToggleable {
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 (_oldValue == true) if (previousValue == true)
_drawCheck(canvas, origin, tShrink, strokePaint); _drawCheck(canvas, origin, tShrink, strokePaint);
else else
_drawDash(canvas, origin, tShrink, strokePaint); _drawDash(canvas, origin, tShrink, strokePaint);
......
...@@ -353,64 +353,47 @@ class Radio<T> extends StatefulWidget { ...@@ -353,64 +353,47 @@ 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 { class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, ToggleableStateMixin {
bool get enabled => widget.onChanged != null; final _RadioPainter _painter = _RadioPainter();
late Map<Type, Action<Intent>> _actionMap;
@override void _handleChanged(bool? selected) {
void initState() { if (selected == null) {
super.initState(); widget.onChanged!(null);
_actionMap = <Type, Action<Intent>>{ return;
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: _actionHandler,
),
};
} }
if (selected) {
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());
} }
bool _focused = false; @override
void _handleHighlightChanged(bool focused) { void didUpdateWidget(Radio<T> oldWidget) {
if (_focused != focused) { super.didUpdateWidget(oldWidget);
setState(() { _focused = focused; }); if (widget._selected != oldWidget._selected) {
animateToValue();
} }
} }
bool _hovering = false; @override
void _handleHoverChanged(bool hovering) { void dispose() {
if (_hovering != hovering) { _painter.dispose();
setState(() { _hovering = hovering; }); super.dispose();
}
} }
void _handleChanged(bool? selected) { @override
if (selected == null) { ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
widget.onChanged!(null);
return;
}
if (selected) {
widget.onChanged!(widget.value);
}
}
bool get _selected => widget.value == widget.groupValue; @override
bool get tristate => widget.toggleable;
Set<MaterialState> get _states => <MaterialState>{ @override
if (!enabled) MaterialState.disabled, bool? get value => widget._selected;
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) {
...@@ -457,15 +440,17 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -457,15 +440,17 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
break; break;
} }
size += effectiveVisualDensity.baseSizeAdjustment; size += effectiveVisualDensity.baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states) final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
?? themeData.radioTheme.mouseCursor?.resolve(_states) return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states); ?? themeData.radioTheme.mouseCursor?.resolve(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)
...@@ -475,13 +460,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -475,13 +460,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
?? 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)
...@@ -497,156 +482,40 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -497,156 +482,40 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
?? themeData.radioTheme.overlayColor?.resolve(inactivePressedStates) ?? themeData.radioTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveColor.withAlpha(kRadialReactionAlpha); ?? effectiveActiveColor.withAlpha(kRadialReactionAlpha);
return Semantics(
return FocusableActionDetector( inMutuallyExclusiveGroup: true,
actions: _actionMap, checked: widget._selected,
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 _RadioRenderObjectWidget extends LeafRenderObjectWidget { class _RadioPainter extends ToggleablePainter {
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
_RenderRadio createRenderObject(BuildContext context) => _RenderRadio( void paint(Canvas canvas, Size size) {
value: selected, paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));
activeColor: activeColor,
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
tristate: toggleable,
vsync: vsync,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
);
@override final Offset center = (Offset.zero & size).center;
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()
...@@ -661,12 +530,4 @@ class _RenderRadio extends RenderToggleable { ...@@ -661,12 +530,4 @@ class _RenderRadio extends RenderToggleable {
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 StatefulWidget { class Switch extends StatelessWidget {
/// 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,8 +359,87 @@ class Switch extends StatefulWidget { ...@@ -359,8 +359,87 @@ class Switch extends StatefulWidget {
/// {@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
_SwitchState createState() => _SwitchState(); Widget build(BuildContext context) {
switch (_switchType) {
case _SwitchType.material:
return _buildMaterialSwitch(context);
case _SwitchType.adaptive: {
final ThemeData theme = Theme.of(context);
assert(theme.platform != null);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _buildMaterialSwitch(context);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _buildCupertinoSwitch(context);
}
}
}
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
...@@ -370,65 +449,95 @@ class Switch extends StatefulWidget { ...@@ -370,65 +449,95 @@ class Switch extends StatefulWidget {
} }
} }
class _SwitchState extends State<Switch> with TickerProviderStateMixin { class _MaterialSwitch extends StatefulWidget {
late Map<Type, Action<Intent>> _actionMap; const _MaterialSwitch({
Key? key,
required this.value,
required this.onChanged,
required this.size,
this.activeColor,
this.activeTrackColor,
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.onActiveThumbImageError,
this.inactiveThumbImage,
this.onInactiveThumbImageError,
this.thumbColor,
this.trackColor,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.focusNode,
this.autofocus = false,
}) : assert(dragStartBehavior != null),
assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
super(key: key);
final bool value;
final ValueChanged<bool>? onChanged;
final Color? activeColor;
final Color? activeTrackColor;
final Color? inactiveThumbColor;
final Color? inactiveTrackColor;
final ImageProvider? activeThumbImage;
final ImageErrorListener? onActiveThumbImageError;
final ImageProvider? inactiveThumbImage;
final ImageErrorListener? onInactiveThumbImageError;
final MaterialStateProperty<Color?>? thumbColor;
final MaterialStateProperty<Color?>? trackColor;
final MaterialTapTargetSize? materialTapTargetSize;
final DragStartBehavior dragStartBehavior;
final MouseCursor? mouseCursor;
final Color? focusColor;
final Color? hoverColor;
final MaterialStateProperty<Color?>? overlayColor;
final double? splashRadius;
final FocusNode? focusNode;
final bool autofocus;
final Size size;
@override @override
void initState() { State<StatefulWidget> createState() => _MaterialSwitchState();
super.initState(); }
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
};
}
void _actionHandler(ActivateIntent intent) { class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderStateMixin, ToggleableStateMixin {
if (widget.onChanged != null) { final _SwitchPainter _painter = _SwitchPainter();
widget.onChanged!(!widget.value);
}
final RenderObject renderObject = context.findRenderObject()!;
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
bool _focused = false; @override
void _handleFocusHighlightChanged(bool focused) { void didUpdateWidget(_MaterialSwitch oldWidget) {
if (focused != _focused) { super.didUpdateWidget(oldWidget);
setState(() { _focused = focused; }); if (oldWidget.value != widget.value) {
} // During a drag we may have modified the curve, reset it if its possible
// to do without visual discontinuation.
if (position.value == 0.0 || position.value == 1.0) {
position
..curve = Curves.easeIn
..reverseCurve = Curves.easeOut;
} }
animateToValue();
bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
} }
} }
Size getSwitchSize(ThemeData theme) { @override
final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize void dispose() {
?? theme.switchTheme.materialTapTargetSize _painter.dispose();
?? theme.materialTapTargetSize; super.dispose();
switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded:
return const Size(_kSwitchWidth, _kSwitchHeight);
case MaterialTapTargetSize.shrinkWrap:
return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
}
} }
bool get enabled => widget.onChanged != null; @override
ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
void _didFinishDragging() { @override
// The user has finished dragging the thumb of this switch. Rebuild the switch bool get tristate => false;
// to update the animation.
setState(() {});
}
Set<MaterialState> get _states => <MaterialState>{ @override
if (!enabled) MaterialState.disabled, bool? get value => widget.value;
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) {
...@@ -487,14 +596,68 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -487,14 +596,68 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
}); });
} }
Widget buildMaterialSwitch(BuildContext context) { double get _trackInnerLength => widget.size.width - _kSwitchMinSize;
void _handleDragStart(DragStartDetails details) {
if (isInteractive)
reactionController.forward();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
position
..curve = Curves.linear
..reverseCurve = null;
final double delta = details.primaryDelta! / _trackInnerLength;
switch (Directionality.of(context)) {
case TextDirection.rtl:
positionController.value -= delta;
break;
case TextDirection.ltr:
positionController.value += delta;
break;
}
}
}
bool _needsPositionAnimation = false;
void _handleDragEnd(DragEndDetails details) {
if (position.value >= 0.5 != widget.value) {
widget.onChanged!(!widget.value);
// Wait with finishing the animation until widget.value has changed to
// !widget.value as part of the widget.onChanged call above.
setState(() {
_needsPositionAnimation = true;
});
} else {
animateToValue();
}
reactionController.reverse();
}
void _handleChanged(bool? value) {
assert(value != null);
assert(widget.onChanged != null);
widget.onChanged!(value!);
}
@override
Widget build(BuildContext context) {
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)
...@@ -512,14 +675,13 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -512,14 +675,13 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
?? 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)
...@@ -535,277 +697,65 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -535,277 +697,65 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
?? theme.switchTheme.overlayColor?.resolve(inactivePressedStates) ?? theme.switchTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha); ?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, _states) final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
?? theme.switchTheme.mouseCursor?.resolve(_states) return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, _states); ?? theme.switchTheme.mouseCursor?.resolve(states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
});
return FocusableActionDetector( return Semantics(
actions: _actionMap, toggled: widget.value,
focusNode: widget.focusNode, child: GestureDetector(
autofocus: widget.autofocus, excludeFromSemantics: true,
enabled: enabled, onHorizontalDragStart: _handleDragStart,
onShowFocusHighlight: _handleFocusHighlightChanged, onHorizontalDragUpdate: _handleDragUpdate,
onShowHoverHighlight: _handleHoverChanged, onHorizontalDragEnd: _handleDragEnd,
mouseCursor: effectiveMouseCursor,
child: Builder(
builder: (BuildContext context) {
return _SwitchRenderObjectWidget(
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
value: widget.value, child: buildToggleable(
activeColor: effectiveActiveThumbColor, mouseCursor: effectiveMouseCursor,
inactiveColor: effectiveInactiveThumbColor,
surfaceColor: theme.colorScheme.surface,
focusColor: effectiveFocusOverlayColor,
hoverColor: effectiveHoverOverlayColor,
reactionColor: effectiveActivePressedOverlayColor,
inactiveReactionColor: effectiveInactivePressedOverlayColor,
splashRadius: widget.splashRadius ?? theme.switchTheme.splashRadius ?? kRadialReactionRadius,
activeThumbImage: widget.activeThumbImage,
onActiveThumbImageError: widget.onActiveThumbImageError,
inactiveThumbImage: widget.inactiveThumbImage,
onInactiveThumbImageError: widget.onInactiveThumbImageError,
activeTrackColor: effectiveActiveTrackColor,
inactiveTrackColor: effectiveInactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged,
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
hasFocus: _focused,
hovering: _hovering,
state: this,
);
},
),
);
}
Widget buildCupertinoSwitch(BuildContext context) {
final Size size = getSwitchSize(Theme.of(context));
return Focus(
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
child: Container( size: widget.size,
width: size.width, // Same size as the Material switch. painter: _painter
height: size.height, ..position = position
alignment: Alignment.center, ..reaction = reaction
child: CupertinoSwitch( ..reactionFocusFade = reactionFocusFade
dragStartBehavior: widget.dragStartBehavior, ..reactionHoverFade = reactionHoverFade
value: widget.value, ..inactiveReactionColor = effectiveInactivePressedOverlayColor
onChanged: widget.onChanged, ..reactionColor = effectiveActivePressedOverlayColor
activeColor: widget.activeColor, ..hoverColor = effectiveHoverOverlayColor
trackColor: widget.inactiveTrackColor ..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 { class _SwitchPainter extends ToggleablePainter {
const _SwitchRenderObjectWidget({
Key? key,
required this.value,
required this.activeColor,
required this.inactiveColor,
required this.hoverColor,
required this.focusColor,
required this.reactionColor,
required this.inactiveReactionColor,
required this.splashRadius,
required this.activeThumbImage,
required this.onActiveThumbImageError,
required this.inactiveThumbImage,
required this.onInactiveThumbImageError,
required this.activeTrackColor,
required this.inactiveTrackColor,
required this.configuration,
required this.onChanged,
required this.additionalConstraints,
required this.dragStartBehavior,
required this.hasFocus,
required this.hovering,
required this.state,
required this.surfaceColor,
}) : super(key: key);
final bool value;
final Color activeColor;
final Color inactiveColor;
final Color hoverColor;
final Color focusColor;
final Color reactionColor;
final Color inactiveReactionColor;
final double splashRadius;
final ImageProvider? activeThumbImage;
final ImageErrorListener? onActiveThumbImageError;
final ImageProvider? inactiveThumbImage;
final ImageErrorListener? onInactiveThumbImageError;
final Color activeTrackColor;
final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool>? onChanged;
final BoxConstraints additionalConstraints;
final DragStartBehavior dragStartBehavior;
final bool hasFocus;
final bool hovering;
final _SwitchState state;
final Color surfaceColor;
@override
_RenderSwitch createRenderObject(BuildContext context) {
return _RenderSwitch(
dragStartBehavior: dragStartBehavior,
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
hoverColor: hoverColor,
focusColor: focusColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
activeThumbImage: activeThumbImage,
onActiveThumbImageError: onActiveThumbImageError,
inactiveThumbImage: inactiveThumbImage,
onInactiveThumbImageError: onInactiveThumbImageError,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
onChanged: onChanged != null ? _handleValueChanged : null,
textDirection: Directionality.of(context),
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
state: state,
surfaceColor: surfaceColor,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
renderObject
..value = value
..activeColor = activeColor
..inactiveColor = inactiveColor
..hoverColor = hoverColor
..focusColor = focusColor
..reactionColor = reactionColor
..inactiveReactionColor = inactiveReactionColor
..splashRadius = splashRadius
..activeThumbImage = activeThumbImage
..onActiveThumbImageError = onActiveThumbImageError
..inactiveThumbImage = inactiveThumbImage
..onInactiveThumbImageError = onInactiveThumbImageError
..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
..onChanged = onChanged != null ? _handleValueChanged : null
..textDirection = Directionality.of(context)
..additionalConstraints = additionalConstraints
..dragStartBehavior = dragStartBehavior
..hasFocus = hasFocus
..hovering = hovering
..vsync = state
..surfaceColor = surfaceColor;
}
void _handleValueChanged(bool? value) {
// Wrap the onChanged callback because the RenderToggleable supports tri-state
// values (i.e. value can be null), but the Switch doesn't. We pass false
// for the tristate param to RenderToggleable, so value should never
// be null.
assert(value != null);
if (onChanged != null) {
onChanged!(value!);
}
}
}
class _RenderSwitch extends RenderToggleable {
_RenderSwitch({
required bool value,
required Color activeColor,
required Color inactiveColor,
required Color hoverColor,
required Color focusColor,
required Color reactionColor,
required Color inactiveReactionColor,
required double splashRadius,
required ImageProvider? activeThumbImage,
required ImageErrorListener? onActiveThumbImageError,
required ImageProvider? inactiveThumbImage,
required ImageErrorListener? onInactiveThumbImageError,
required Color activeTrackColor,
required Color inactiveTrackColor,
required ImageConfiguration configuration,
required BoxConstraints additionalConstraints,
required TextDirection textDirection,
required ValueChanged<bool?>? onChanged,
required DragStartBehavior dragStartBehavior,
required bool hasFocus,
required bool hovering,
required this.state,
required Color surfaceColor,
}) : assert(textDirection != null),
_activeThumbImage = activeThumbImage,
_onActiveThumbImageError = onActiveThumbImageError,
_inactiveThumbImage = inactiveThumbImage,
_onInactiveThumbImageError = onInactiveThumbImageError,
_activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor,
_configuration = configuration,
_textDirection = textDirection,
_surfaceColor = surfaceColor,
super(
value: value,
tristate: false,
activeColor: activeColor,
inactiveColor: inactiveColor,
hoverColor: hoverColor,
focusColor: focusColor,
reactionColor: reactionColor,
inactiveReactionColor: inactiveReactionColor,
splashRadius: splashRadius,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
vsync: state,
) {
_drag = HorizontalDragGestureRecognizer()
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..dragStartBehavior = dragStartBehavior;
}
ImageProvider? get activeThumbImage => _activeThumbImage; ImageProvider? 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;
markNeedsPaint(); notifyListeners();
} }
ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError; ImageErrorListener? get onActiveThumbImageError => _onActiveThumbImageError;
...@@ -815,7 +765,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -815,7 +765,7 @@ class _RenderSwitch extends RenderToggleable {
return; return;
} }
_onActiveThumbImageError = value; _onActiveThumbImageError = value;
markNeedsPaint(); notifyListeners();
} }
ImageProvider? get inactiveThumbImage => _inactiveThumbImage; ImageProvider? get inactiveThumbImage => _inactiveThumbImage;
...@@ -824,7 +774,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -824,7 +774,7 @@ class _RenderSwitch extends RenderToggleable {
if (value == _inactiveThumbImage) if (value == _inactiveThumbImage)
return; return;
_inactiveThumbImage = value; _inactiveThumbImage = value;
markNeedsPaint(); notifyListeners();
} }
ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError; ImageErrorListener? get onInactiveThumbImageError => _onInactiveThumbImageError;
...@@ -834,132 +784,77 @@ class _RenderSwitch extends RenderToggleable { ...@@ -834,132 +784,77 @@ class _RenderSwitch extends RenderToggleable {
return; return;
} }
_onInactiveThumbImageError = value; _onInactiveThumbImageError = value;
markNeedsPaint(); notifyListeners();
} }
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;
markNeedsPaint(); notifyListeners();
} }
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;
markNeedsPaint(); notifyListeners();
} }
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;
markNeedsPaint(); notifyListeners();
} }
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;
markNeedsPaint(); notifyListeners();
}
DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
set dragStartBehavior(DragStartBehavior value) {
assert(value != null);
if (_drag.dragStartBehavior == value)
return;
_drag.dragStartBehavior = value;
} }
Color get surfaceColor => _surfaceColor; Color get surfaceColor => _surfaceColor!;
Color _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;
markNeedsPaint(); notifyListeners();
} }
_SwitchState state; bool get isInteractive => _isInteractive!;
bool? _isInteractive;
@override set isInteractive(bool value) {
set value(bool? newValue) { if (value == _isInteractive) {
assert(value != null); return;
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();
}
}
@override
void detach() {
_cachedThumbPainter?.dispose();
_cachedThumbPainter = null;
super.detach();
}
double get _trackInnerLength => size.width - _kSwitchMinSize;
late HorizontalDragGestureRecognizer _drag;
bool _needsPositionAnimation = false;
void _handleDragStart(DragStartDetails details) {
if (isInteractive)
reactionController.forward();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
position.reverseCurve = null;
final double delta = details.primaryDelta! / _trackInnerLength;
switch (textDirection) {
case TextDirection.rtl:
positionController.value -= delta;
break;
case TextDirection.ltr:
positionController.value += delta;
break;
}
} }
_isInteractive = value;
notifyListeners();
} }
void _handleDragEnd(DragEndDetails details) { double get trackInnerLength => _trackInnerLength!;
_needsPositionAnimation = true; double? _trackInnerLength;
set trackInnerLength(double value) {
if (position.value >= 0.5 != value) if (value == _trackInnerLength) {
onChanged!(!value!); return;
reactionController.reverse();
state._didFinishDragging();
} }
_trackInnerLength = value;
@override notifyListeners();
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;
...@@ -984,19 +879,12 @@ class _RenderSwitch extends RenderToggleable { ...@@ -984,19 +879,12 @@ class _RenderSwitch extends RenderToggleable {
// 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)
markNeedsPaint(); notifyListeners();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isToggled = value == true;
} }
@override @override
void paint(PaintingContext context, Offset offset) { void paint(Canvas canvas, Size size) {
final Canvas canvas = context.canvas; final bool isEnabled = isInteractive;
final bool isEnabled = onChanged != null;
final double currentValue = position.value; final double currentValue = position.value;
final double visualPosition; final double visualPosition;
...@@ -1029,8 +917,8 @@ class _RenderSwitch extends RenderToggleable { ...@@ -1029,8 +917,8 @@ class _RenderSwitch extends RenderToggleable {
..color = trackColor; ..color = trackColor;
const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius; const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
final Rect trackRect = Rect.fromLTWH( final Rect trackRect = Rect.fromLTWH(
offset.dx + trackHorizontalPadding, trackHorizontalPadding,
offset.dy + (size.height - _kTrackHeight) / 2.0, (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding, size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight, _kTrackHeight,
); );
...@@ -1038,11 +926,11 @@ class _RenderSwitch extends RenderToggleable { ...@@ -1038,11 +926,11 @@ class _RenderSwitch extends RenderToggleable {
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, offset, thumbPosition); paintRadialReaction(canvas: canvas, origin: thumbPosition);
try { try {
_isPainting = true; _isPainting = true;
...@@ -1059,7 +947,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -1059,7 +947,7 @@ class _RenderSwitch extends RenderToggleable {
final double radius = _kThumbRadius - inset; final double radius = _kThumbRadius - inset;
thumbPainter.paint( thumbPainter.paint(
canvas, canvas,
thumbPosition + offset - Offset(radius, radius), thumbPosition - Offset(radius, radius),
configuration.copyWith(size: Size.fromRadius(radius)), configuration.copyWith(size: Size.fromRadius(radius)),
); );
} finally { } finally {
......
...@@ -6,9 +6,10 @@ import 'package:flutter/animation.dart'; ...@@ -6,9 +6,10 @@ 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/scheduler.dart'; import 'package:flutter/widgets.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);
...@@ -16,92 +17,21 @@ const Duration _kToggleDuration = Duration(milliseconds: 200); ...@@ -16,92 +17,21 @@ 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 base class for material style toggleable controls with toggle animations. /// A mixin for [StatefulWidget]s that implement material-themed toggleable
/// controls with toggle animations (e.g. [Switch]es, [Checkbox]es, and
/// [Radio]s).
/// ///
/// This class handles storing the current value, dispatching ValueChanged on a /// The mixin implements the logic for toggling the control (e.g. when tapped)
/// tap gesture and driving a changed animation. Subclasses are responsible for /// and provides a series of animation controllers to transition the control
/// painting. /// from one state to another. It does not have any opinion about the visual
abstract class RenderToggleable extends RenderConstrainedBox { /// representation of the toggleable widget. The visuals are defined by a
/// Creates a toggleable render object. /// [CustomPainter] passed to the [buildToggleable]. [State] objects using this
/// /// mixin should call that method from their [build] method.
/// The [activeColor], and [inactiveColor] arguments must not be ///
/// null. The [value] can only be null if tristate is true. /// This mixin is used to implement the material components for [Switch],
RenderToggleable({ /// [Checkbox], and [Radio] controls.
required bool? value, @optionalTypeArgs
bool tristate = false, mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin<S> {
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
...@@ -109,16 +39,15 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -109,16 +39,15 @@ abstract class RenderToggleable extends RenderConstrainedBox {
/// 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 either true or tristate /// the value 0.0. When the control is active, the value is either true or
/// is true and the value is null. When the control is active the animation /// tristate is true and the value is null. When the control is active the
/// has a value of 1.0. When the control is changing from inactive /// animation has a value of 1.0. When the control is changing from inactive
/// to active (or vice versa), [value] is the target value and this animation /// 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;
...@@ -129,84 +58,66 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -129,84 +58,66 @@ abstract class RenderToggleable extends RenderConstrainedBox {
/// 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.
/// ///
/// Subclasses should call [paintRadialReaction] to actually paint the radial /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
/// reaction. /// may be used.
@protected
AnimationController get reactionController => _reactionController; AnimationController get reactionController => _reactionController;
late AnimationController _reactionController; late AnimationController _reactionController;
late Animation<double> _reaction;
/// Used by subclasses to control the radial reaction's opacity animation for /// The visual value of the radial reaction animation.
/// [hasFocus] changes.
/// ///
/// Some controls have a radial ink reaction to focus. This animation /// Some controls have a radial ink reaction to user input. This animation
/// controller can be used to start or stop these ink reaction fade-ins and /// controls the progress of these ink reactions.
/// fade-outs.
/// ///
/// Subclasses should call [paintRadialReaction] to actually paint the radial /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
/// reaction. /// may be used.
@protected Animation<double> get reaction => _reaction;
AnimationController get reactionFocusFadeController => _reactionFocusFadeController; late Animation<double> _reaction;
late AnimationController _reactionFocusFadeController;
late Animation<double> _reactionFocusFade;
/// Used by subclasses to control the radial reaction's opacity animation for /// Controls the radial reaction's opacity animation for hover changes.
/// [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
/// controller can be used to start or stop these ink reaction fade-ins and /// controls these ink reaction fade-ins and
/// fade-outs. /// fade-outs.
/// ///
/// Subclasses should call [paintRadialReaction] to actually paint the radial /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
/// reaction. /// may be used.
@protected Animation<double> get reactionHoverFade => _reactionHoverFade;
AnimationController get reactionHoverFadeController => _reactionHoverFadeController;
late AnimationController _reactionHoverFadeController;
late Animation<double> _reactionHoverFade; late Animation<double> _reactionHoverFade;
late AnimationController _reactionHoverFadeController;
/// True if this toggleable has the input focus. /// Controls the radial reaction's opacity animation for focus changes.
bool get hasFocus => _hasFocus; ///
bool _hasFocus; /// Some controls have a radial ink reaction to focus. This animation
set hasFocus(bool value) { /// controls these ink reaction fade-ins and fade-outs.
assert(value != null); ///
if (value == _hasFocus) /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction]
return; /// may be used.
_hasFocus = value; Animation<double> get reactionFocusFade => _reactionFocusFade;
if (_hasFocus) { late Animation<double> _reactionFocusFade;
_reactionFocusFadeController.forward(); late AnimationController _reactionFocusFadeController;
} else {
_reactionFocusFadeController.reverse();
}
markNeedsPaint();
}
/// True if this toggleable is being hovered over by a pointer. /// Whether [value] of this control can be changed by user interaction.
bool get hovering => _hovering; ///
bool _hovering; /// The control is considered interactive if the [onChanged] callback is
set hovering(bool value) { /// non-null. If the callback is null, then the control is disabled, and
assert(value != null); /// non-interactive. A disabled checkbox, for example, is displayed using a
if (value == _hovering) /// grey color and its value cannot be changed.
return; bool get isInteractive => onChanged != null;
_hovering = value;
if (_hovering) {
_reactionHoverFadeController.forward();
} else {
_reactionHoverFadeController.reverse();
}
markNeedsPaint();
}
/// The [TickerProvider] for the [AnimationController]s that run the animations. late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
TickerProvider get vsync => _vsync; ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap),
TickerProvider _vsync; };
set vsync(TickerProvider value) {
assert(value != null); /// Called when the control changes value.
if (value == _vsync) ///
return; /// If the control is tapped, [onChanged] is called immediately with the new
_vsync = value; /// value.
positionController.resync(vsync); ///
reactionController.resync(vsync); /// The control is considered interactive (see [isInteractive]) if this
} /// callback is non-null. If the callback is null, then the control is
/// disabled, and non-interactive. A disabled checkbox, for example, is
/// displayed using a grey color and its value cannot be changed.
ValueChanged<bool?>? get onChanged;
/// False if this control is "inactive" (not checked, off, or unselected). /// False if this control is "inactive" (not checked, off, or unselected).
/// ///
...@@ -217,17 +128,62 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -217,17 +128,62 @@ abstract class RenderToggleable extends RenderConstrainedBox {
/// 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 => _value; bool? get value;
bool? _value;
set value(bool? value) { /// If true, [value] can be true, false, or null, otherwise [value] must
assert(tristate || value != null); /// be true or false.
if (value == _value) ///
return; /// When [tristate] is true and [value] is null, then the control is
_value = value; /// considered to be in its third or "indeterminate" state.
markNeedsSemanticsUpdate(); bool get tristate;
_position
..curve = Curves.easeIn @override
..reverseCurve = Curves.easeOut; void initState() {
super.initState();
_positionController = AnimationController(
duration: _kToggleDuration,
value: value == false ? 0.0 : 1.0,
vsync: this,
);
_position = CurvedAnimation(
parent: _positionController,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
);
_reactionController = AnimationController(
duration: kRadialReactionDuration,
vsync: this,
);
_reaction = CurvedAnimation(
parent: _reactionController,
curve: Curves.fastOutSlowIn,
);
_reactionHoverFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: _hovering || _focused ? 1.0 : 0.0,
vsync: this,
);
_reactionHoverFade = CurvedAnimation(
parent: _reactionHoverFadeController,
curve: Curves.fastOutSlowIn,
);
_reactionFocusFadeController = AnimationController(
duration: _kReactionFadeDuration,
value: _hovering || _focused ? 1.0 : 0.0,
vsync: this,
);
_reactionFocusFade = CurvedAnimation(
parent: _reactionFocusFadeController,
curve: Curves.fastOutSlowIn,
);
}
/// Runs the [position] animation to transition the Toggleable's appearance
/// to match [value].
///
/// This method must be called whenever [value] changes to ensure that the
/// visual representation of the Toggleable matches the current [value].
void animateToValue() {
if (tristate) { if (tristate) {
if (value == null) if (value == null)
_positionController.value = 0.0; _positionController.value = 0.0;
...@@ -243,94 +199,235 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -243,94 +199,235 @@ abstract class RenderToggleable extends RenderConstrainedBox {
} }
} }
/// If true, [value] can be true, false, or null, otherwise [value] must @override
/// be true or false. void dispose() {
_positionController.dispose();
_reactionController.dispose();
_reactionHoverFadeController.dispose();
_reactionFocusFadeController.dispose();
super.dispose();
}
/// The most recent [Offset] at which a pointer touched the Toggleable.
/// ///
/// When [tristate] is true and [value] is null, then the control is /// This is null if currently no pointer is touching the Toggleable or if
/// considered to be in its third or "indeterminate" state. /// [isInteractive] is false.
bool get tristate => _tristate; Offset? get downPosition => _downPosition;
bool _tristate; Offset? _downPosition;
set tristate(bool value) {
assert(tristate != null); void _handleTapDown(TapDownDetails details) {
if (value == _tristate) if (isInteractive) {
setState(() {
_downPosition = details.localPosition;
});
_reactionController.forward();
}
}
void _handleTap([Intent? _]) {
if (!isInteractive)
return; return;
_tristate = value; switch (value) {
markNeedsSemanticsUpdate(); case false:
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();
}
}
} }
/// The color that should be used in the active state (i.e., when [value] is true). 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.
/// ///
/// For example, a checkbox should use this color when checked. /// The returned set will include:
Color get activeColor => _activeColor; ///
Color _activeColor; /// * [MaterialState.disabled], if [isInteractive] is false
set activeColor(Color value) { /// * [MaterialState.hovered], if a pointer is hovering over the Toggleable
assert(value != null); /// * [MaterialState.focused], if the Toggleable has input focus
if (value == _activeColor) /// * [MaterialState.selected], if [value] is true or null
return; Set<MaterialState> get states => <MaterialState>{
_activeColor = value; if (!isInteractive) MaterialState.disabled,
markNeedsPaint(); 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,
),
),
),
);
} }
}
/// The color that should be used in the inactive state (i.e., when [value] is false). /// A base class for a [CustomPainter] that may be passed to
/// [ToggleableStateMixin.buildToggleable] to draw the visual representation of
/// a Toggleable.
///
/// Subclasses must implement the [paint] method to draw the actual visuals of
/// the Toggleable. In their [paint] method subclasses may call
/// [paintRadialReaction] to draw a radial ink reaction for this control.
abstract class ToggleablePainter extends ChangeNotifier implements CustomPainter {
/// The visual value of the control.
/// ///
/// For example, a checkbox should use this color when unchecked. /// Usually set to [ToggleableStateMixin.position].
Color get inactiveColor => _inactiveColor; Animation<double> get position => _position!;
Color _inactiveColor; Animation<double>? _position;
set inactiveColor(Color value) { set position(Animation<double> value) {
assert(value != null); if (value == _position) {
if (value == _inactiveColor)
return; return;
_inactiveColor = value; }
markNeedsPaint(); _position?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_position = value;
notifyListeners();
} }
/// The color that should be used for the reaction when [hovering] is true. /// The visual value of the radial reaction animation.
/// ///
/// Used when the toggleable needs to change the reaction color/transparency, /// Usually set to [ToggleableStateMixin.reaction].
/// when it is being hovered over. Animation<double> get reaction => _reaction!;
/// Animation<double>? _reaction;
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha]. set reaction(Animation<double> value) {
Color get hoverColor => _hoverColor; if (value == _reaction) {
Color _hoverColor;
set hoverColor(Color value) {
assert(value != null);
if (value == _hoverColor)
return; return;
_hoverColor = value; }
markNeedsPaint(); _reaction?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_reaction = value;
notifyListeners();
} }
/// The color that should be used for the reaction when [hasFocus] is true. /// Controls the radial reaction's opacity animation for focus changes.
/// ///
/// Used when the toggleable needs to change the reaction color/transparency, /// Usually set to [ToggleableStateMixin.reactionFocusFade].
/// when it has focus. Animation<double> get reactionFocusFade => _reactionFocusFade!;
Animation<double>? _reactionFocusFade;
set reactionFocusFade(Animation<double> value) {
if (value == _reactionFocusFade) {
return;
}
_reactionFocusFade?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_reactionFocusFade = value;
notifyListeners();
}
/// Controls the radial reaction's opacity animation for hover changes.
/// ///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha]. /// Usually set to [ToggleableStateMixin.reactionHoverFade].
Color get focusColor => _focusColor; Animation<double> get reactionHoverFade => _reactionHoverFade!;
Color _focusColor; Animation<double>? _reactionHoverFade;
set focusColor(Color value) { set reactionHoverFade(Animation<double> value) {
assert(value != null); if (value == _reactionHoverFade) {
if (value == _focusColor)
return; return;
_focusColor = value; }
markNeedsPaint(); _reactionHoverFade?.removeListener(notifyListeners);
value.addListener(notifyListeners);
_reactionHoverFade = value;
notifyListeners();
} }
/// The color that should be used for the reaction when the toggleable is /// The color that should be used in the active state (i.e., when
/// active. /// [ToggleableStateMixin.value] is true).
/// ///
/// Used when the toggleable needs to change the reaction color/transparency /// For example, a checkbox should use this color when checked.
/// that is displayed when the toggleable is active and tapped. Color get activeColor => _activeColor!;
Color? _activeColor;
set activeColor(Color value) {
if (_activeColor == value) {
return;
}
_activeColor = value;
notifyListeners();
}
/// The color that should be used in the inactive state (i.e., when
/// [ToggleableStateMixin.value] is false).
/// ///
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha]. /// For example, a checkbox should use this color when unchecked.
Color? get reactionColor => _reactionColor; Color get inactiveColor => _inactiveColor!;
Color? _reactionColor; Color? _inactiveColor;
set reactionColor(Color? value) { set inactiveColor(Color value) {
assert(value != null); if (_inactiveColor == value) {
if (value == _reactionColor)
return; return;
_reactionColor = value; }
markNeedsPaint(); _inactiveColor = value;
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
...@@ -338,184 +435,161 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -338,184 +435,161 @@ abstract class RenderToggleable extends RenderConstrainedBox {
/// ///
/// 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) {
assert(value != null); if (value == _inactiveReactionColor) {
if (value == _inactiveReactionColor)
return; return;
}
_inactiveReactionColor = value; _inactiveReactionColor = value;
markNeedsPaint(); notifyListeners();
} }
/// The splash radius for the radial reaction. /// The color that should be used for the reaction when the toggleable is
double get splashRadius => _splashRadius; /// active.
double _splashRadius; ///
set splashRadius(double value) { /// Used when the toggleable needs to change the reaction color/transparency
if (value == _splashRadius) /// that is displayed when the toggleable is active and tapped.
Color get reactionColor => _reactionColor!;
Color? _reactionColor;
set reactionColor(Color value) {
if (value == _reactionColor) {
return; return;
_splashRadius = value; }
markNeedsPaint(); _reactionColor = value;
notifyListeners();
} }
/// Called when the control changes value. /// The color that should be used for the reaction when [isHovered] is true.
/// ///
/// If the control is tapped, [onChanged] is called immediately with the new /// Used when the toggleable needs to change the reaction color/transparency,
/// value. /// when it is being hovered over.
/// Color get hoverColor => _hoverColor!;
/// The control is considered interactive (see [isInteractive]) if this Color? _hoverColor;
/// callback is non-null. If the callback is null, then the control is set hoverColor(Color value) {
/// disabled, and non-interactive. A disabled checkbox, for example, is if (value == _hoverColor) {
/// 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();
} }
/// Whether [value] of this control can be changed by user interaction. /// The color that should be used for the reaction when [isFocused] is true.
/// ///
/// The control is considered interactive if the [onChanged] callback is /// Used when the toggleable needs to change the reaction color/transparency,
/// non-null. If the callback is null, then the control is disabled, and /// when it has focus.
/// non-interactive. A disabled checkbox, for example, is displayed using a Color get focusColor => _focusColor!;
/// grey color and its value cannot be changed. Color? _focusColor;
bool get isInteractive => onChanged != null; set focusColor(Color value) {
if (value == _focusColor) {
late TapGestureRecognizer _tap; return;
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;
@override notifyListeners();
void detach() {
_positionController.stop();
_reactionController.stop();
_reactionHoverFadeController.stop();
_reactionFocusFadeController.stop();
super.detach();
} }
void _handleTapDown(TapDownDetails details) { /// The splash radius for the radial reaction.
if (isInteractive) { double get splashRadius => _splashRadius!;
_downPosition = globalToLocal(details.globalPosition); double? _splashRadius;
_reactionController.forward(); set splashRadius(double value) {
if (value == _splashRadius) {
return;
} }
_splashRadius = value;
notifyListeners();
} }
void _handleTap() { /// The [Offset] within the Toggleable at which a pointer touched the Toggleable.
if (!isInteractive) ///
/// This is null if currently no pointer is touching the Toggleable.
///
/// Usually set to [ToggleableStateMixin.downPosition].
Offset? get downPosition => _downPosition;
Offset? _downPosition;
set downPosition(Offset? value) {
if (value == _downPosition) {
return; return;
switch (value) {
case false:
onChanged!(true);
break;
case true:
onChanged!(tristate ? null : false);
break;
case null:
onChanged!(false);
break;
} }
sendSemanticsEvent(const TapSemanticEvent()); _downPosition = value;
notifyListeners();
} }
void _handleTapUp(TapUpDetails details) { /// True if this toggleable has the input focus.
_downPosition = null; bool get isFocused => _isFocused!;
if (isInteractive) bool? _isFocused;
_reactionController.reverse(); set isFocused(bool? value) {
if (value == _isFocused) {
return;
} }
_isFocused = value;
void _handleTapCancel() { notifyListeners();
_downPosition = null;
if (isInteractive)
_reactionController.reverse();
} }
@override /// True if this toggleable is being hovered over by a pointer.
bool hitTestSelf(Offset position) => true; bool get isHovered => _isHovered!;
bool? _isHovered;
@override set isHovered(bool? value) {
void handleEvent(PointerEvent event, BoxHitTestEntry entry) { if (value == _isHovered) {
assert(debugHandleEvent(event, entry)); return;
if (event is PointerDownEvent && isInteractive) }
_tap.addPointer(event); _isHovered = value;
notifyListeners();
} }
/// 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
/// point at which the user interacted with the control, which is handled /// [downPosition] at which the user interacted with the control).
/// automatically). void paintRadialReaction({
void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) { required Canvas canvas,
if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) { Offset offset = Offset.zero,
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 = hasFocus || hovering final double reactionRadius = isFocused || isHovered
? 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
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isEnabled = isInteractive; @override
if (isInteractive) void dispose() {
config.onTap = _handleTap; _position?.removeListener(notifyListeners);
_reaction?.removeListener(notifyListeners);
_reactionFocusFade?.removeListener(notifyListeners);
_reactionHoverFade?.removeListener(notifyListeners);
super.dispose();
} }
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
super.debugFillProperties(properties);
properties.add(FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true)); @override
properties.add(FlagProperty('isInteractive', value: isInteractive, ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true)); bool? hitTest(Offset position) => null;
}
@override
SemanticsBuilderCallback? get semanticsBuilder => null;
@override
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => false;
} }
...@@ -99,7 +99,7 @@ void main() { ...@@ -99,7 +99,7 @@ void main() {
), ),
)); ));
expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics( expect(tester.getSemantics(find.byType(Checkbox)), 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.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics( expect(tester.getSemantics(find.byType(Checkbox)), 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.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics( expect(tester.getSemantics(find.byType(Checkbox)), 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(Focus)); final RenderObject object = tester.firstRenderObject(find.byType(Checkbox));
expect(checkboxValue, true); expect(checkboxValue, true);
expect(semanticEvent, <String, dynamic>{ expect(semanticEvent, <String, dynamic>{
...@@ -319,10 +319,8 @@ void main() { ...@@ -319,10 +319,8 @@ void main() {
); );
} }
RenderToggleable getCheckboxRenderer() { RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) { return tester.renderObject<RenderBox>(find.byType(Checkbox));
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame(false)); await tester.pumpWidget(buildFrame(false));
...@@ -377,10 +375,8 @@ void main() { ...@@ -377,10 +375,8 @@ void main() {
); );
} }
RenderToggleable getCheckboxRenderer() { RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) { return tester.renderObject<RenderBox>(find.byType(Checkbox));
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame(checkColor: checkColor)); await tester.pumpWidget(buildFrame(checkColor: checkColor));
...@@ -454,10 +450,9 @@ void main() { ...@@ -454,10 +450,9 @@ void main() {
..circle(color: Colors.orange[500]) ..circle(color: Colors.orange[500])
..drrect( ..drrect(
color: const Color(0x8a000000), color: const Color(0x8a000000),
outer: RRect.fromLTRBR( outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)),
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)), inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.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.
...@@ -470,10 +465,9 @@ void main() { ...@@ -470,10 +465,9 @@ void main() {
paints paints
..drrect( ..drrect(
color: const Color(0x61000000), color: const Color(0x61000000),
outer: RRect.fromLTRBR( outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(1.0)),
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)), inner: RRect.fromLTRBR(17.0, 17.0, 31.0, 31.0, const Radius.circular(-1.0)),
inner: RRect.fromLTRBR(393.0, ),
293.0, 407.0, 307.0, const Radius.circular(-1.0))),
); );
}); });
...@@ -825,10 +819,8 @@ void main() { ...@@ -825,10 +819,8 @@ void main() {
); );
} }
RenderToggleable getCheckboxRenderer() { RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) { return tester.renderObject<RenderBox>(find.byType(Checkbox));
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame(enabled: true)); await tester.pumpWidget(buildFrame(enabled: true));
...@@ -878,10 +870,8 @@ void main() { ...@@ -878,10 +870,8 @@ void main() {
); );
} }
RenderToggleable getCheckboxRenderer() { RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) { return tester.renderObject<RenderBox>(find.byType(Checkbox));
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
}));
} }
await tester.pumpWidget(buildFrame()); await tester.pumpWidget(buildFrame());
...@@ -937,11 +927,9 @@ void main() { ...@@ -937,11 +927,9 @@ void main() {
paints paints
..drrect( ..drrect(
color: const Color(0xfff44336), color: const Color(0xfff44336),
outer: RRect.fromLTRBR( outer: RRect.fromLTRBR(15.0, 15.0, 33.0, 33.0, const Radius.circular(5)),
391.0, 291.0, 409.0, 309.0, const Radius.circular(5)), inner: RRect.fromLTRBR(19.0, 19.0, 29.0, 29.0, const Radius.circular(1)),
inner: RRect.fromLTRBR( ),
395.0, 295.0, 405.0, 305.0, const Radius.circular(1)))
,
); );
}); });
...@@ -1184,6 +1172,29 @@ void main() { ...@@ -1184,6 +1172,29 @@ 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.byType(Focus)); final RenderObject object = tester.firstRenderObject(find.byKey(key));
expect(radioValue, 1); expect(radioValue, 1);
expect(semanticEvent, <String, dynamic>{ expect(semanticEvent, <String, dynamic>{
...@@ -1078,4 +1078,29 @@ void main() { ...@@ -1078,4 +1078,29 @@ 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 control test', (WidgetTester tester) async { testWidgets('SwitchListTile semantics 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,8 +301,7 @@ void main() { ...@@ -301,8 +301,7 @@ 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( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -317,8 +316,7 @@ void main() { ...@@ -317,8 +316,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.blue[600]!.withAlpha(0x80), color: Colors.blue[600]!.withAlpha(0x80),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -351,8 +349,7 @@ void main() { ...@@ -351,8 +349,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, color: Colors.black12,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -383,8 +380,7 @@ void main() { ...@@ -383,8 +380,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, color: Colors.black12,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -427,8 +423,7 @@ void main() { ...@@ -427,8 +423,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.blue[500], color: Colors.blue[500],
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -442,8 +437,7 @@ void main() { ...@@ -442,8 +437,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.green[500], color: Colors.green[500],
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -528,19 +522,19 @@ void main() { ...@@ -528,19 +522,19 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(value, isFalse); expect(value, isFalse);
final RenderToggleable renderObject = tester.renderObject<RenderToggleable>( final ToggleableStateMixin state = tester.state<ToggleableStateMixin>(
find.descendant( find.descendant(
of: find.byType(Switch), of: find.byType(Switch),
matching: find.byWidgetPredicate( matching: find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_SwitchRenderObjectWidget', (Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch',
), ),
), ),
); );
expect(renderObject.position.value, lessThan(0.5)); expect(state.position.value, lessThan(0.5));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, isFalse); expect(value, isFalse);
expect(renderObject.position.value, 0); expect(state.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);
...@@ -549,12 +543,12 @@ void main() { ...@@ -549,12 +543,12 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(value, isTrue); expect(value, isTrue);
expect(renderObject.position.value, greaterThan(0.5)); expect(state.position.value, greaterThan(0.5));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, isTrue); expect(value, isTrue);
expect(renderObject.position.value, 1.0); expect(state.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);
...@@ -563,12 +557,12 @@ void main() { ...@@ -563,12 +557,12 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(value, isTrue); expect(value, isTrue);
expect(renderObject.position.value, lessThan(0.5)); expect(state.position.value, lessThan(0.5));
await tester.pump(); await tester.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, isTrue); expect(value, isTrue);
expect(renderObject.position.value, 1.0); expect(state.position.value, 1.0);
}); });
testWidgets('switch has semantic events', (WidgetTester tester) async { testWidgets('switch has semantic events', (WidgetTester tester) async {
...@@ -601,7 +595,7 @@ void main() { ...@@ -601,7 +595,7 @@ void main() {
), ),
); );
await tester.tap(find.byType(Switch)); await tester.tap(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Focus)); final RenderObject object = tester.firstRenderObject(find.byType(Switch));
expect(value, true); expect(value, true);
expect(semanticEvent, <String, dynamic>{ expect(semanticEvent, <String, dynamic>{
...@@ -750,8 +744,7 @@ void main() { ...@@ -750,8 +744,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -769,8 +762,7 @@ void main() { ...@@ -769,8 +762,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x52000000), color: const Color(0x52000000),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -788,8 +780,7 @@ void main() { ...@@ -788,8 +780,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x1f000000), color: const Color(0x1f000000),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -854,8 +845,7 @@ void main() { ...@@ -854,8 +845,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -875,8 +865,7 @@ void main() { ...@@ -875,8 +865,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -892,8 +881,7 @@ void main() { ...@@ -892,8 +881,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x1f000000), color: const Color(0x1f000000),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -1070,8 +1058,7 @@ void main() { ...@@ -1070,8 +1058,7 @@ void main() {
), ),
); );
final RenderToggleable oldSwitchRenderObject = tester final ToggleableStateMixin oldSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));
.renderObject(find.byWidgetPredicate((Widget widget) => widget is LeafRenderObjectWidget));
stateSetter(() { value = false; }); stateSetter(() { value = false; });
await tester.pump(); await tester.pump();
...@@ -1079,14 +1066,12 @@ void main() { ...@@ -1079,14 +1066,12 @@ void main() {
stateSetter(() { enabled = false; }); stateSetter(() { enabled = false; });
await tester.pump(); await tester.pump();
final RenderToggleable updatedSwitchRenderObject = tester final ToggleableStateMixin updatedSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));
.renderObject(find.byWidgetPredicate((Widget widget) => widget is LeafRenderObjectWidget));
expect(updatedSwitchState.isInteractive, false);
expect(updatedSwitchRenderObject.isInteractive, false); expect(updatedSwitchState, oldSwitchState);
expect(updatedSwitchRenderObject, oldSwitchRenderObject); expect(updatedSwitchState.position.isCompleted, false);
expect(updatedSwitchRenderObject.position.isCompleted, false); expect(updatedSwitchState.position.isDismissed, 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 {
...@@ -1137,8 +1122,7 @@ void main() { ...@@ -1137,8 +1122,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, color: Colors.black12,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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,8 +1138,7 @@ void main() { ...@@ -1154,8 +1138,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, color: Colors.black12,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -1171,8 +1154,7 @@ void main() { ...@@ -1171,8 +1154,7 @@ 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( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -1188,8 +1170,7 @@ void main() { ...@@ -1188,8 +1170,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, color: Colors.black12,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -1246,8 +1227,7 @@ void main() { ...@@ -1246,8 +1227,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -1268,8 +1248,7 @@ void main() { ...@@ -1268,8 +1248,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: const Color(0x801e88e5), color: const Color(0x801e88e5),
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -1327,8 +1306,7 @@ void main() { ...@@ -1327,8 +1306,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: inactiveDisabledTrackColor, color: inactiveDisabledTrackColor,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
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',
); );
...@@ -1340,8 +1318,7 @@ void main() { ...@@ -1340,8 +1318,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: activeDisabledTrackColor, color: activeDisabledTrackColor,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
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',
); );
...@@ -1353,8 +1330,7 @@ void main() { ...@@ -1353,8 +1330,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: inactiveEnabledTrackColor, color: inactiveEnabledTrackColor,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
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',
); );
...@@ -1366,8 +1342,7 @@ void main() { ...@@ -1366,8 +1342,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: inactiveDisabledTrackColor, color: inactiveDisabledTrackColor,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
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',
); );
}); });
...@@ -1420,8 +1395,7 @@ void main() { ...@@ -1420,8 +1395,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: focusedTrackColor, color: focusedTrackColor,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
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',
); );
...@@ -1437,8 +1411,7 @@ void main() { ...@@ -1437,8 +1411,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: hoveredTrackColor, color: hoveredTrackColor,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0))),
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',
); );
}); });
...@@ -1488,8 +1461,7 @@ void main() { ...@@ -1488,8 +1461,7 @@ void main() {
paints paints
..rrect( ..rrect(
color: Colors.black12, color: Colors.black12,
rrect: RRect.fromLTRBR( rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)))
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))
...@@ -1638,4 +1610,27 @@ void main() { ...@@ -1638,4 +1610,27 @@ 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