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