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