Unverified Commit 2aa9bb2b authored by Hans Muller's avatar Hans Muller Committed by GitHub

Tri-state Checkbox (#14611)

parent 8507b72a
...@@ -21,6 +21,10 @@ import 'toggleable.dart'; ...@@ -21,6 +21,10 @@ import 'toggleable.dart';
/// rebuild the checkbox with a new [value] to update the visual appearance of /// rebuild the checkbox with a new [value] to update the visual appearance of
/// the checkbox. /// the checkbox.
/// ///
/// The checkbox can optionally display three values - true, false, and null -
/// if [tristate] is true. When [value] is null a dash is displayed. By default
/// [tristate] is false and the checkbox's [value] must be true or false.
///
/// Requires one of its ancestors to be a [Material] widget. /// Requires one of its ancestors to be a [Material] widget.
/// ///
/// See also: /// See also:
...@@ -43,16 +47,20 @@ class Checkbox extends StatefulWidget { ...@@ -43,16 +47,20 @@ class Checkbox extends StatefulWidget {
/// ///
/// The following arguments are required: /// The following arguments are required:
/// ///
/// * [value], which determines whether the checkbox is checked, and must not /// * [value], which determines whether the checkbox is checked. The [value]
/// be null. /// can only be be null if [tristate] is true.
/// * [onChanged], which is called when the value of the checkbox should /// * [onChanged], which is called when the value of the checkbox should
/// change. It can be set to null to disable the checkbox. /// change. It can be set to null to disable the checkbox.
///
/// The value of [tristate] must not be null.
const Checkbox({ const Checkbox({
Key key, Key key,
@required this.value, @required this.value,
this.tristate: false,
@required this.onChanged, @required this.onChanged,
this.activeColor, this.activeColor,
}) : assert(value != null), }) : assert(tristate != null),
assert(tristate || value != null),
super(key: key); super(key: key);
/// Whether this checkbox is checked. /// Whether this checkbox is checked.
...@@ -66,7 +74,12 @@ class Checkbox extends StatefulWidget { ...@@ -66,7 +74,12 @@ class Checkbox extends StatefulWidget {
/// change state until the parent widget rebuilds the checkbox with the new /// change state until the parent widget rebuilds the checkbox with the new
/// value. /// value.
/// ///
/// If null, the checkbox will be displayed as disabled. /// If this callback is null, the checkbox will be displayed as disabled
/// and will not respond to input gestures.
///
/// When the checkbox is tapped, if [tristate] is false (the default) then
/// the [onChanged] callback will be applied to `!value`. If [tristate] is
/// true this callback cycle from false to true to null.
/// ///
/// The callback provided to [onChanged] should update the state of the parent /// The callback provided to [onChanged] should update the state of the parent
/// [StatefulWidget] using the [State.setState] method, so that the parent /// [StatefulWidget] using the [State.setState] method, so that the parent
...@@ -89,6 +102,18 @@ class Checkbox extends StatefulWidget { ...@@ -89,6 +102,18 @@ class Checkbox extends StatefulWidget {
/// Defaults to accent color of the current [Theme]. /// Defaults to accent color of the current [Theme].
final Color activeColor; final Color activeColor;
/// If true the checkbox's [value] can be true, false, or null.
///
/// Checkbox displays a dash when its value is null.
///
/// When a tri-state checkbox is tapped its [onChanged] callback will be
/// applied to true if the current value is null or false, false otherwise.
/// Typically tri-state checkboxes are disabled (the onChanged callback is
/// null) so they don't respond to taps.
///
/// If tristate is false (the default), [value] must not be null.
final bool tristate;
/// The width of a checkbox widget. /// The width of a checkbox widget.
static const double width = 18.0; static const double width = 18.0;
...@@ -103,6 +128,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -103,6 +128,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
return new _CheckboxRenderObjectWidget( return new _CheckboxRenderObjectWidget(
value: widget.value, value: widget.value,
tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.accentColor, activeColor: widget.activeColor ?? themeData.accentColor,
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor, inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
onChanged: widget.onChanged, onChanged: widget.onChanged,
...@@ -115,17 +141,20 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -115,17 +141,20 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
const _CheckboxRenderObjectWidget({ const _CheckboxRenderObjectWidget({
Key key, Key key,
@required this.value, @required this.value,
@required this.tristate,
@required this.activeColor, @required this.activeColor,
@required this.inactiveColor, @required this.inactiveColor,
@required this.onChanged, @required this.onChanged,
@required this.vsync, @required this.vsync,
}) : assert(value != null), }) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null), assert(activeColor != null),
assert(inactiveColor != null), assert(inactiveColor != null),
assert(vsync != null), assert(vsync != null),
super(key: key); super(key: key);
final bool value; final bool value;
final bool tristate;
final Color activeColor; final Color activeColor;
final Color inactiveColor; final Color inactiveColor;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
...@@ -134,6 +163,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -134,6 +163,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
@override @override
_RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox( _RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox(
value: value, value: value,
tristate: tristate,
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
...@@ -144,6 +174,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -144,6 +174,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) { void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject renderObject
..value = value ..value = value
..tristate = tristate
..activeColor = activeColor ..activeColor = activeColor
..inactiveColor = inactiveColor ..inactiveColor = inactiveColor
..onChanged = onChanged ..onChanged = onChanged
...@@ -158,67 +189,151 @@ const double _kStrokeWidth = 2.0; ...@@ -158,67 +189,151 @@ const double _kStrokeWidth = 2.0;
class _RenderCheckbox extends RenderToggleable { class _RenderCheckbox extends RenderToggleable {
_RenderCheckbox({ _RenderCheckbox({
bool value, bool value,
bool tristate,
Color activeColor, Color activeColor,
Color inactiveColor, Color inactiveColor,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
@required TickerProvider vsync, @required TickerProvider vsync,
}): super( }): _oldValue = value,
value: value, super(
activeColor: activeColor, value: value,
inactiveColor: inactiveColor, tristate: tristate,
onChanged: onChanged, activeColor: activeColor,
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), inactiveColor: inactiveColor,
vsync: vsync, onChanged: onChanged,
); size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
vsync: vsync,
);
bool _oldValue;
@override @override
void paint(PaintingContext context, Offset offset) { set value(bool newValue) {
if (newValue == value)
return;
_oldValue = value;
super.value = newValue;
}
final Canvas canvas = context.canvas; // The square outer bounds of the checkbox at t, with the specified origin.
// At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
// At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
// At t == 1.0, .. is _kEdgeSize
RRect _outerRectAt(Offset origin, double t) {
final double inset = 1.0 - (t - 0.5).abs() * 2.0;
final double size = _kEdgeSize - inset * _kStrokeWidth;
final Rect rect = new Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size);
return new RRect.fromRectAndRadius(rect, _kEdgeRadius);
}
final double offsetX = offset.dx + (size.width - _kEdgeSize) / 2.0; // The checkbox's border color if value == false, or its fill color when
final double offsetY = offset.dy + (size.height - _kEdgeSize) / 2.0; // value == true or null.
Color _colorAt(double t) {
// As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
return onChanged == null
? inactiveColor
: (t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0));
}
// White stroke used to paint the check and dash.
void _initStrokePaint(Paint paint) {
paint
..color = const Color(0xFFFFFFFF)
..style = PaintingStyle.stroke
..strokeWidth = _kStrokeWidth;
}
void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) {
assert(t >= 0.0 && t <= 0.5);
final double size = outer.width;
// As t goes from 0.0 to 1.0, gradually fill the outer RRect.
final RRect inner = outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t));
canvas.drawDRRect(outer, inner, paint);
}
void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
assert(t >= 0.0 && t <= 1.0);
// As t goes from 0.0 to 1.0, animate the two checkmark strokes from the
// mid point outwards.
final Path path = new Path();
const Offset start = const Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45);
const Offset mid = const Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7);
const Offset end = const Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25);
final Offset drawStart = Offset.lerp(start, mid, 1.0 - t);
final Offset drawEnd = Offset.lerp(mid, end, t);
path.moveTo(origin.dx + drawStart.dx, origin.dy + drawStart.dy);
path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
canvas.drawPath(path, paint);
}
void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) {
assert(t >= 0.0 && t <= 1.0);
// As t goes from 0.0 to 1.0, animate the horizontal line from the
// mid point outwards.
const Offset start = const Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5);
const Offset mid = const Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5);
const Offset end = const Offset(_kEdgeSize * 0.8, _kEdgeSize * 0.5);
final Offset drawStart = Offset.lerp(start, mid, 1.0 - t);
final Offset drawEnd = Offset.lerp(mid, end, t);
canvas.drawLine(origin + drawStart, origin + drawEnd, paint);
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
paintRadialReaction(canvas, offset, size.center(Offset.zero)); paintRadialReaction(canvas, offset, size.center(Offset.zero));
final double t = position.value; final Offset origin = offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0);
final AnimationStatus status = position.status;
final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed
? position.value
: 1.0 - position.value;
Color borderColor = inactiveColor; // Four cases: false to null, false to true, null to false, true to false
if (onChanged != null) if (_oldValue == false || value == false) {
borderColor = t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0); final double t = value == false ? 1.0 - tNormalized : tNormalized;
final RRect outer = _outerRectAt(origin, t);
final Paint paint = new Paint()..color = _colorAt(t);
final Paint paint = new Paint() if (t <= 0.5) {
..color = borderColor; _drawBorder(canvas, outer, t, paint);
} else {
canvas.drawRRect(outer, paint);
final double inset = 1.0 - (t - 0.5).abs() * 2.0; _initStrokePaint(paint);
final double rectSize = _kEdgeSize - inset * _kStrokeWidth; final double tShrink = (t - 0.5) * 2.0;
final Rect rect = new Rect.fromLTWH(offsetX + inset, offsetY + inset, rectSize, rectSize); if (_oldValue == null)
_drawDash(canvas, origin, tShrink, paint);
final RRect outer = new RRect.fromRectAndRadius(rect, _kEdgeRadius); else
if (t <= 0.5) { _drawCheck(canvas, origin, tShrink, paint);
// Outline }
final RRect inner = outer.deflate(math.min(rectSize / 2.0, _kStrokeWidth + rectSize * t)); } else { // Two cases: null to true, true to null
canvas.drawDRRect(outer, inner, paint); final RRect outer = _outerRectAt(origin, 1.0);
} else { final Paint paint = new Paint() ..color = _colorAt(1.0);
// Background
canvas.drawRRect(outer, paint); canvas.drawRRect(outer, paint);
// White inner check _initStrokePaint(paint);
final double value = (t - 0.5) * 2.0; if (tNormalized <= 0.5) {
paint final double tShrink = 1.0 - tNormalized * 2.0;
..color = const Color(0xFFFFFFFF) if (_oldValue == true)
..style = PaintingStyle.stroke _drawCheck(canvas, origin, tShrink, paint);
..strokeWidth = _kStrokeWidth; else
final Path path = new Path(); _drawDash(canvas, origin, tShrink, paint);
const Offset start = const Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45); } else {
const Offset mid = const Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7); final double tExpand = (tNormalized - 0.5) * 2.0;
const Offset end = const Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25); if (value == true)
final Offset drawStart = Offset.lerp(start, mid, 1.0 - value); _drawCheck(canvas, origin, tExpand, paint);
final Offset drawEnd = Offset.lerp(mid, end, value); else
path.moveTo(offsetX + drawStart.dx, offsetY + drawStart.dy); _drawDash(canvas, origin, tExpand, paint);
path.lineTo(offsetX + mid.dx, offsetY + mid.dy); }
path.lineTo(offsetX + drawEnd.dx, offsetY + drawEnd.dy);
canvas.drawPath(path, paint);
} }
} }
// TODO(hmuller): smooth segues for cases where the value changes
// in the middle of position's animation cycle.
// https://github.com/flutter/flutter/issues/14674
// TODO(hmuller): accessibility support for tristate checkboxes.
// https://github.com/flutter/flutter/issues/14677
} }
...@@ -176,6 +176,7 @@ class _RenderRadio extends RenderToggleable { ...@@ -176,6 +176,7 @@ class _RenderRadio extends RenderToggleable {
@required TickerProvider vsync, @required TickerProvider vsync,
}): super( }): super(
value: value, value: value,
tristate: false,
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
......
...@@ -247,6 +247,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -247,6 +247,7 @@ class _RenderSwitch extends RenderToggleable {
_textDirection = textDirection, _textDirection = textDirection,
super( super(
value: value, value: value,
tristate: false,
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
......
...@@ -21,20 +21,23 @@ final Tween<double> _kRadialReactionRadiusTween = new Tween<double>(begin: 0.0, ...@@ -21,20 +21,23 @@ final Tween<double> _kRadialReactionRadiusTween = new Tween<double>(begin: 0.0,
abstract class RenderToggleable extends RenderConstrainedBox { abstract class RenderToggleable extends RenderConstrainedBox {
/// Creates a toggleable render object. /// Creates a toggleable render object.
/// ///
/// The [value], [activeColor], and [inactiveColor] arguments must not be /// The [activeColor], and [inactiveColor] arguments must not be
/// null. /// null. The [value] can only be null if tristate is true.
RenderToggleable({ RenderToggleable({
@required bool value, @required bool value,
bool tristate: false,
Size size, Size size,
@required Color activeColor, @required Color activeColor,
@required Color inactiveColor, @required Color inactiveColor,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
@required TickerProvider vsync, @required TickerProvider vsync,
}) : assert(value != null), }) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null), assert(activeColor != null),
assert(inactiveColor != null), assert(inactiveColor != null),
assert(vsync != null), assert(vsync != null),
_value = value, _value = value,
_tristate = tristate,
_activeColor = activeColor, _activeColor = activeColor,
_inactiveColor = inactiveColor, _inactiveColor = inactiveColor,
_onChanged = onChanged, _onChanged = onChanged,
...@@ -47,7 +50,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -47,7 +50,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
..onTapCancel = _handleTapCancel; ..onTapCancel = _handleTapCancel;
_positionController = new AnimationController( _positionController = new AnimationController(
duration: _kToggleDuration, duration: _kToggleDuration,
value: value ? 1.0 : 0.0, value: value == false ? 0.0 : 1.0,
vsync: vsync, vsync: vsync,
); );
_position = new CurvedAnimation( _position = new CurvedAnimation(
...@@ -79,8 +82,9 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -79,8 +82,9 @@ abstract class RenderToggleable extends RenderConstrainedBox {
/// The visual value of the control. /// The visual value of the control.
/// ///
/// When the control is inactive, the [value] is false and this animation has /// When the control is inactive, the [value] is false and this animation has
/// the value 0.0. When the control is active, the value is true and this /// the value 0.0. When the control is active, the value either true or tristate
/// animation has the value 1.0. When the control is changing from inactive /// 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 /// to active (or vice versa), [value] is the target value and this animation
/// gradually updates from 0.0 to 1.0 (or vice versa). /// gradually updates from 0.0 to 1.0 (or vice versa).
CurvedAnimation get position => _position; CurvedAnimation get position => _position;
...@@ -110,7 +114,11 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -110,7 +114,11 @@ abstract class RenderToggleable extends RenderConstrainedBox {
reactionController.resync(vsync); reactionController.resync(vsync);
} }
/// Whether this control is current "active" (checked, on, selected) or "inactive" (unchecked, off, not selected). /// False if this control is "inactive" (not checked, off, or unselected).
///
/// If value is true then the control "active" (checked, on, or selected). If
/// tristate is true and value is null, then the control is considered to be
/// in its third or "indeterminate" state.
/// ///
/// When the value changes, this object starts the [positionController] and /// When the value changes, this object starts the [positionController] and
/// [position] animations to animate the visual appearance of the control to /// [position] animations to animate the visual appearance of the control to
...@@ -118,7 +126,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -118,7 +126,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
bool get value => _value; bool get value => _value;
bool _value; bool _value;
set value(bool value) { set value(bool value) {
assert(value != null); assert(tristate || value != null);
if (value == _value) if (value == _value)
return; return;
_value = value; _value = value;
...@@ -126,10 +134,29 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -126,10 +134,29 @@ abstract class RenderToggleable extends RenderConstrainedBox {
_position _position
..curve = Curves.easeIn ..curve = Curves.easeIn
..reverseCurve = Curves.easeOut; ..reverseCurve = Curves.easeOut;
if (value) switch (_positionController.status) {
_positionController.forward(); case AnimationStatus.forward:
else case AnimationStatus.completed:
_positionController.reverse(); _positionController.reverse();
break;
default:
_positionController.forward();
}
}
/// 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 color that should be used in the active state (i.e., when [value] is true). /// The color that should be used in the active state (i.e., when [value] is true).
...@@ -196,10 +223,10 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -196,10 +223,10 @@ abstract class RenderToggleable extends RenderConstrainedBox {
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
if (value) if (value == false)
_positionController.forward();
else
_positionController.reverse(); _positionController.reverse();
else
_positionController.forward();
if (isInteractive) { if (isInteractive) {
switch (_reactionController.status) { switch (_reactionController.status) {
case AnimationStatus.forward: case AnimationStatus.forward:
...@@ -223,12 +250,17 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -223,12 +250,17 @@ abstract class RenderToggleable extends RenderConstrainedBox {
super.detach(); super.detach();
} }
// Handle the case where the _positionController's value changes because
// the user dragged the toggleable: we may reach 0.0 or 1.0 without
// seeing a tap. The Switch does this.
void _handlePositionStateChanged(AnimationStatus status) { void _handlePositionStateChanged(AnimationStatus status) {
if (isInteractive) { if (isInteractive && !tristate) {
if (status == AnimationStatus.completed && !_value) if (status == AnimationStatus.completed && _value == false) {
onChanged(true); onChanged(true);
else if (status == AnimationStatus.dismissed && _value) }
else if (status == AnimationStatus.dismissed && _value != false) {
onChanged(false); onChanged(false);
}
} }
} }
...@@ -240,8 +272,19 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -240,8 +272,19 @@ abstract class RenderToggleable extends RenderConstrainedBox {
} }
void _handleTap() { void _handleTap() {
if (isInteractive) if (!isInteractive)
onChanged(!_value); return;
switch (value) {
case false:
onChanged(true);
break;
case true:
onChanged(tristate ? null : false);
break;
default: // case null:
onChanged(false);
break;
}
} }
void _handleTapUp(TapUpDetails details) { void _handleTapUp(TapUpDetails details) {
...@@ -290,7 +333,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -290,7 +333,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
config.isEnabled = isInteractive; config.isEnabled = isInteractive;
if (isInteractive) if (isInteractive)
config.onTap = _handleTap; config.onTap = _handleTap;
config.isChecked = _value; config.isChecked = _value != false;
} }
@override @override
......
...@@ -102,4 +102,48 @@ void main() { ...@@ -102,4 +102,48 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('CheckBox tristate: true', (WidgetTester tester) async {
bool checkBoxValue;
await tester.pumpWidget(
new Material(
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Checkbox(
tristate: true,
value: checkBoxValue,
onChanged: (bool value) {
setState(() {
checkBoxValue = value;
});
},
);
},
),
),
);
expect(tester.widget<Checkbox>(find.byType(Checkbox)).value, null);
await tester.tap(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(checkBoxValue, false);
await tester.tap(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(checkBoxValue, true);
await tester.tap(find.byType(Checkbox));
await tester.pumpAndSettle();
expect(checkBoxValue, null);
checkBoxValue = true;
await tester.pumpAndSettle();
expect(checkBoxValue, true);
checkBoxValue = null;
await tester.pumpAndSettle();
expect(checkBoxValue, null);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment