@@ -21,6 +21,10 @@ import 'toggleable.dart';
21
21
/// rebuild the checkbox with a new [value] to update the visual appearance of
22
22
/// the checkbox.
23
23
///
24
+ /// The checkbox can optionally display three values - true, false, and null -
25
+ /// if [tristate] is true. When [value] is null a dash is displayed. By default
26
+ /// [tristate] is false and the checkbox's [value] must be true or false.
27
+ ///
24
28
/// Requires one of its ancestors to be a [Material] widget.
25
29
///
26
30
/// See also:
@@ -43,16 +47,20 @@ class Checkbox extends StatefulWidget {
43
47
///
44
48
/// The following arguments are required:
45
49
///
46
- /// * [value] , which determines whether the checkbox is checked, and must not
47
- /// be null.
50
+ /// * [value] , which determines whether the checkbox is checked. The [value]
51
+ /// can only be be null if [tristate] is true .
48
52
/// * [onChanged] , which is called when the value of the checkbox should
49
53
/// change. It can be set to null to disable the checkbox.
54
+ ///
55
+ /// The value of [tristate] must not be null.
50
56
const Checkbox ({
51
57
Key key,
52
58
@required this .value,
59
+ this .tristate: false ,
53
60
@required this .onChanged,
54
61
this .activeColor,
55
- }) : assert (value != null ),
62
+ }) : assert (tristate != null ),
63
+ assert (tristate || value != null ),
56
64
super (key: key);
57
65
58
66
/// Whether this checkbox is checked.
@@ -66,7 +74,12 @@ class Checkbox extends StatefulWidget {
66
74
/// change state until the parent widget rebuilds the checkbox with the new
67
75
/// value.
68
76
///
69
- /// If null, the checkbox will be displayed as disabled.
77
+ /// If this callback is null, the checkbox will be displayed as disabled
78
+ /// and will not respond to input gestures.
79
+ ///
80
+ /// When the checkbox is tapped, if [tristate] is false (the default) then
81
+ /// the [onChanged] callback will be applied to `!value` . If [tristate] is
82
+ /// true this callback cycle from false to true to null.
70
83
///
71
84
/// The callback provided to [onChanged] should update the state of the parent
72
85
/// [StatefulWidget] using the [State.setState] method, so that the parent
@@ -89,6 +102,18 @@ class Checkbox extends StatefulWidget {
89
102
/// Defaults to accent color of the current [Theme] .
90
103
final Color activeColor;
91
104
105
+ /// If true the checkbox's [value] can be true, false, or null.
106
+ ///
107
+ /// Checkbox displays a dash when its value is null.
108
+ ///
109
+ /// When a tri-state checkbox is tapped its [onChanged] callback will be
110
+ /// applied to true if the current value is null or false, false otherwise.
111
+ /// Typically tri-state checkboxes are disabled (the onChanged callback is
112
+ /// null) so they don't respond to taps.
113
+ ///
114
+ /// If tristate is false (the default), [value] must not be null.
115
+ final bool tristate;
116
+
92
117
/// The width of a checkbox widget.
93
118
static const double width = 18.0 ;
94
119
@@ -103,6 +128,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
103
128
final ThemeData themeData = Theme .of (context);
104
129
return new _CheckboxRenderObjectWidget (
105
130
value: widget.value,
131
+ tristate: widget.tristate,
106
132
activeColor: widget.activeColor ?? themeData.accentColor,
107
133
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
108
134
onChanged: widget.onChanged,
@@ -115,17 +141,20 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
115
141
const _CheckboxRenderObjectWidget ({
116
142
Key key,
117
143
@required this .value,
144
+ @required this .tristate,
118
145
@required this .activeColor,
119
146
@required this .inactiveColor,
120
147
@required this .onChanged,
121
148
@required this .vsync,
122
- }) : assert (value != null ),
149
+ }) : assert (tristate != null ),
150
+ assert (tristate || value != null ),
123
151
assert (activeColor != null ),
124
152
assert (inactiveColor != null ),
125
153
assert (vsync != null ),
126
154
super (key: key);
127
155
128
156
final bool value;
157
+ final bool tristate;
129
158
final Color activeColor;
130
159
final Color inactiveColor;
131
160
final ValueChanged <bool > onChanged;
@@ -134,6 +163,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
134
163
@override
135
164
_RenderCheckbox createRenderObject (BuildContext context) => new _RenderCheckbox (
136
165
value: value,
166
+ tristate: tristate,
137
167
activeColor: activeColor,
138
168
inactiveColor: inactiveColor,
139
169
onChanged: onChanged,
@@ -144,6 +174,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
144
174
void updateRenderObject (BuildContext context, _RenderCheckbox renderObject) {
145
175
renderObject
146
176
..value = value
177
+ ..tristate = tristate
147
178
..activeColor = activeColor
148
179
..inactiveColor = inactiveColor
149
180
..onChanged = onChanged
@@ -158,67 +189,151 @@ const double _kStrokeWidth = 2.0;
158
189
class _RenderCheckbox extends RenderToggleable {
159
190
_RenderCheckbox ({
160
191
bool value,
192
+ bool tristate,
161
193
Color activeColor,
162
194
Color inactiveColor,
163
195
ValueChanged <bool > onChanged,
164
196
@required TickerProvider vsync,
165
- }): super (
166
- value: value,
167
- activeColor: activeColor,
168
- inactiveColor: inactiveColor,
169
- onChanged: onChanged,
170
- size: const Size (2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
171
- vsync: vsync,
172
- );
197
+ }): _oldValue = value,
198
+ super (
199
+ value: value,
200
+ tristate: tristate,
201
+ activeColor: activeColor,
202
+ inactiveColor: inactiveColor,
203
+ onChanged: onChanged,
204
+ size: const Size (2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
205
+ vsync: vsync,
206
+ );
207
+
208
+ bool _oldValue;
173
209
174
210
@override
175
- void paint (PaintingContext context, Offset offset) {
211
+ set value (bool newValue) {
212
+ if (newValue == value)
213
+ return ;
214
+ _oldValue = value;
215
+ super .value = newValue;
216
+ }
176
217
177
- final Canvas canvas = context.canvas;
218
+ // The square outer bounds of the checkbox at t, with the specified origin.
219
+ // At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
220
+ // At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
221
+ // At t == 1.0, .. is _kEdgeSize
222
+ RRect _outerRectAt (Offset origin, double t) {
223
+ final double inset = 1.0 - (t - 0.5 ).abs () * 2.0 ;
224
+ final double size = _kEdgeSize - inset * _kStrokeWidth;
225
+ final Rect rect = new Rect .fromLTWH (origin.dx + inset, origin.dy + inset, size, size);
226
+ return new RRect .fromRectAndRadius (rect, _kEdgeRadius);
227
+ }
178
228
179
- final double offsetX = offset.dx + (size.width - _kEdgeSize) / 2.0 ;
180
- final double offsetY = offset.dy + (size.height - _kEdgeSize) / 2.0 ;
229
+ // The checkbox's border color if value == false, or its fill color when
230
+ // value == true or null.
231
+ Color _colorAt (double t) {
232
+ // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
233
+ return onChanged == null
234
+ ? inactiveColor
235
+ : (t >= 0.25 ? activeColor : Color .lerp (inactiveColor, activeColor, t * 4.0 ));
236
+ }
237
+
238
+ // White stroke used to paint the check and dash.
239
+ void _initStrokePaint (Paint paint) {
240
+ paint
241
+ ..color = const Color (0xFFFFFFFF )
242
+ ..style = PaintingStyle .stroke
243
+ ..strokeWidth = _kStrokeWidth;
244
+ }
245
+
246
+ void _drawBorder (Canvas canvas, RRect outer, double t, Paint paint) {
247
+ assert (t >= 0.0 && t <= 0.5 );
248
+ final double size = outer.width;
249
+ // As t goes from 0.0 to 1.0, gradually fill the outer RRect.
250
+ final RRect inner = outer.deflate (math.min (size / 2.0 , _kStrokeWidth + size * t));
251
+ canvas.drawDRRect (outer, inner, paint);
252
+ }
253
+
254
+ void _drawCheck (Canvas canvas, Offset origin, double t, Paint paint) {
255
+ assert (t >= 0.0 && t <= 1.0 );
256
+ // As t goes from 0.0 to 1.0, animate the two checkmark strokes from the
257
+ // mid point outwards.
258
+ final Path path = new Path ();
259
+ const Offset start = const Offset (_kEdgeSize * 0.15 , _kEdgeSize * 0.45 );
260
+ const Offset mid = const Offset (_kEdgeSize * 0.4 , _kEdgeSize * 0.7 );
261
+ const Offset end = const Offset (_kEdgeSize * 0.85 , _kEdgeSize * 0.25 );
262
+ final Offset drawStart = Offset .lerp (start, mid, 1.0 - t);
263
+ final Offset drawEnd = Offset .lerp (mid, end, t);
264
+ path.moveTo (origin.dx + drawStart.dx, origin.dy + drawStart.dy);
265
+ path.lineTo (origin.dx + mid.dx, origin.dy + mid.dy);
266
+ path.lineTo (origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
267
+ canvas.drawPath (path, paint);
268
+ }
181
269
270
+ void _drawDash (Canvas canvas, Offset origin, double t, Paint paint) {
271
+ assert (t >= 0.0 && t <= 1.0 );
272
+ // As t goes from 0.0 to 1.0, animate the horizontal line from the
273
+ // mid point outwards.
274
+ const Offset start = const Offset (_kEdgeSize * 0.2 , _kEdgeSize * 0.5 );
275
+ const Offset mid = const Offset (_kEdgeSize * 0.5 , _kEdgeSize * 0.5 );
276
+ const Offset end = const Offset (_kEdgeSize * 0.8 , _kEdgeSize * 0.5 );
277
+ final Offset drawStart = Offset .lerp (start, mid, 1.0 - t);
278
+ final Offset drawEnd = Offset .lerp (mid, end, t);
279
+ canvas.drawLine (origin + drawStart, origin + drawEnd, paint);
280
+ }
281
+
282
+ @override
283
+ void paint (PaintingContext context, Offset offset) {
284
+ final Canvas canvas = context.canvas;
182
285
paintRadialReaction (canvas, offset, size.center (Offset .zero));
183
286
184
- final double t = position.value;
287
+ final Offset origin = offset + (size / 2.0 - const Size .square (_kEdgeSize) / 2.0 );
288
+ final AnimationStatus status = position.status;
289
+ final double tNormalized = status == AnimationStatus .forward || status == AnimationStatus .completed
290
+ ? position.value
291
+ : 1.0 - position.value;
185
292
186
- Color borderColor = inactiveColor;
187
- if (onChanged != null )
188
- borderColor = t >= 0.25 ? activeColor : Color .lerp (inactiveColor, activeColor, t * 4.0 );
293
+ // Four cases: false to null, false to true, null to false, true to false
294
+ if (_oldValue == false || value == false ) {
295
+ final double t = value == false ? 1.0 - tNormalized : tNormalized;
296
+ final RRect outer = _outerRectAt (origin, t);
297
+ final Paint paint = new Paint ()..color = _colorAt (t);
189
298
190
- final Paint paint = new Paint ()
191
- ..color = borderColor;
299
+ if (t <= 0.5 ) {
300
+ _drawBorder (canvas, outer, t, paint);
301
+ } else {
302
+ canvas.drawRRect (outer, paint);
192
303
193
- final double inset = 1.0 - (t - 0.5 ).abs () * 2.0 ;
194
- final double rectSize = _kEdgeSize - inset * _kStrokeWidth;
195
- final Rect rect = new Rect .fromLTWH (offsetX + inset, offsetY + inset, rectSize, rectSize);
196
-
197
- final RRect outer = new RRect .fromRectAndRadius (rect, _kEdgeRadius);
198
- if (t <= 0.5 ) {
199
- // Outline
200
- final RRect inner = outer.deflate (math.min (rectSize / 2.0 , _kStrokeWidth + rectSize * t));
201
- canvas.drawDRRect (outer, inner, paint);
202
- } else {
203
- // Background
304
+ _initStrokePaint (paint);
305
+ final double tShrink = (t - 0.5 ) * 2.0 ;
306
+ if (_oldValue == null )
307
+ _drawDash (canvas, origin, tShrink, paint);
308
+ else
309
+ _drawCheck (canvas, origin, tShrink, paint);
310
+ }
311
+ } else { // Two cases: null to true, true to null
312
+ final RRect outer = _outerRectAt (origin, 1.0 );
313
+ final Paint paint = new Paint () ..color = _colorAt (1.0 );
204
314
canvas.drawRRect (outer, paint);
205
315
206
- // White inner check
207
- final double value = (t - 0.5 ) * 2.0 ;
208
- paint
209
- ..color = const Color (0xFFFFFFFF )
210
- ..style = PaintingStyle .stroke
211
- ..strokeWidth = _kStrokeWidth;
212
- final Path path = new Path ();
213
- const Offset start = const Offset (_kEdgeSize * 0.15 , _kEdgeSize * 0.45 );
214
- const Offset mid = const Offset (_kEdgeSize * 0.4 , _kEdgeSize * 0.7 );
215
- const Offset end = const Offset (_kEdgeSize * 0.85 , _kEdgeSize * 0.25 );
216
- final Offset drawStart = Offset .lerp (start, mid, 1.0 - value);
217
- final Offset drawEnd = Offset .lerp (mid, end, value);
218
- path.moveTo (offsetX + drawStart.dx, offsetY + drawStart.dy);
219
- path.lineTo (offsetX + mid.dx, offsetY + mid.dy);
220
- path.lineTo (offsetX + drawEnd.dx, offsetY + drawEnd.dy);
221
- canvas.drawPath (path, paint);
316
+ _initStrokePaint (paint);
317
+ if (tNormalized <= 0.5 ) {
318
+ final double tShrink = 1.0 - tNormalized * 2.0 ;
319
+ if (_oldValue == true )
320
+ _drawCheck (canvas, origin, tShrink, paint);
321
+ else
322
+ _drawDash (canvas, origin, tShrink, paint);
323
+ } else {
324
+ final double tExpand = (tNormalized - 0.5 ) * 2.0 ;
325
+ if (value == true )
326
+ _drawCheck (canvas, origin, tExpand, paint);
327
+ else
328
+ _drawDash (canvas, origin, tExpand, paint);
329
+ }
222
330
}
223
331
}
332
+
333
+ // TODO(hmuller): smooth segues for cases where the value changes
334
+ // in the middle of position's animation cycle.
335
+ // https://github.com/flutter/flutter/issues/14674
336
+
337
+ // TODO(hmuller): accessibility support for tristate checkboxes.
338
+ // https://github.com/flutter/flutter/issues/14677
224
339
}
0 commit comments