Skip to content

Commit a8b3d1b

Browse files
authored
Add toggleable attribute to Radio (flutter#53846)
This adds a new toggleable attribute to the Radio widget. This allows a radio group to be set back to an indeterminate state if the selected radio button is selected again. Fixes flutter#53791
1 parent e97c385 commit a8b3d1b

File tree

7 files changed

+891
-296
lines changed

7 files changed

+891
-296
lines changed

dev/integration_tests/flutter_gallery/test/demo/material/expansion_panels_demo_test.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ bool _isRadioSelected(int index) =>
4040
List<Radio<Location>> get _radios => List<Radio<Location>>.from(
4141
_radioFinder.evaluate().map<Widget>((Element e) => e.widget));
4242

43-
// [find.byType] and [find.widgetWithText] do not match subclasses; `Radio` is not sufficient to find a `Radio<_Location>`.
44-
// Another approach is to grab the `runtimeType` of a dummy instance; see packages/flutter/test/material/control_list_tile_test.dart.
43+
// [find.byType] and [find.widgetWithText] do not match subclasses; `Radio` is
44+
// not sufficient to find a `Radio<_Location>`. Another approach is to grab the
45+
// `runtimeType` of a dummy instance; see
46+
// packages/flutter/test/material/radio_list_tile_test.dart.
4547
Finder get _radioFinder =>
4648
find.byWidgetPredicate((Widget w) => w is Radio<Location>);
4749

packages/flutter/lib/src/material/radio.dart

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class Radio<T> extends StatefulWidget {
108108
@required this.value,
109109
@required this.groupValue,
110110
@required this.onChanged,
111+
this.toggleable = false,
111112
this.activeColor,
112113
this.focusColor,
113114
this.hoverColor,
@@ -116,6 +117,7 @@ class Radio<T> extends StatefulWidget {
116117
this.focusNode,
117118
this.autofocus = false,
118119
}) : assert(autofocus != null),
120+
assert(toggleable != null),
119121
super(key: key);
120122

121123
/// The value represented by this radio button.
@@ -155,6 +157,69 @@ class Radio<T> extends StatefulWidget {
155157
/// ```
156158
final ValueChanged<T> onChanged;
157159

160+
/// Set to true if this radio button is allowed to be returned to an
161+
/// indeterminate state by selecting it again when selected.
162+
///
163+
/// To indicate returning to an indeterminate state, [onChanged] will be
164+
/// called with null.
165+
///
166+
/// If true, [onChanged] can be called with [value] when selected while
167+
/// [groupValue] != [value], or with null when selected again while
168+
/// [groupValue] == [value].
169+
///
170+
/// If false, [onChanged] will be called with [value] when it is selected
171+
/// while [groupValue] != [value], and only by selecting another radio button
172+
/// in the group (i.e. changing the value of [groupValue]) can this radio
173+
/// button be unselected.
174+
///
175+
/// The default is false.
176+
///
177+
/// {@tool dartpad --template=stateful_widget_scaffold}
178+
/// This example shows how to enable deselecting a radio button by setting the
179+
/// [toggleable] attribute.
180+
///
181+
/// ```dart
182+
/// int groupValue;
183+
/// static const List<String> selections = <String>[
184+
/// 'Hercules Mulligan',
185+
/// 'Eliza Hamilton',
186+
/// 'Philip Schuyler',
187+
/// 'Maria Reynolds',
188+
/// 'Samuel Seabury',
189+
/// ];
190+
///
191+
/// @override
192+
/// Widget build(BuildContext context) {
193+
/// return Scaffold(
194+
/// body: ListView.builder(
195+
/// itemBuilder: (context, index) {
196+
/// return Row(
197+
/// mainAxisSize: MainAxisSize.min,
198+
/// crossAxisAlignment: CrossAxisAlignment.center,
199+
/// children: <Widget>[
200+
/// Radio<int>(
201+
/// value: index,
202+
/// groupValue: groupValue,
203+
/// // TRY THIS: Try setting the toggleable value to false and
204+
/// // see how that changes the behavior of the widget.
205+
/// toggleable: true,
206+
/// onChanged: (int value) {
207+
/// setState(() {
208+
/// groupValue = value;
209+
/// });
210+
/// }),
211+
/// Text(selections[index]),
212+
/// ],
213+
/// );
214+
/// },
215+
/// itemCount: selections.length,
216+
/// ),
217+
/// );
218+
/// }
219+
/// ```
220+
/// {@end-tool}
221+
final bool toggleable;
222+
158223
/// The color to use when this radio button is selected.
159224
///
160225
/// Defaults to [ThemeData.toggleableActiveColor].
@@ -207,7 +272,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
207272
};
208273
}
209274

210-
void _actionHandler(FocusNode node, Intent intent){
275+
void _actionHandler(FocusNode node, Intent intent) {
211276
if (widget.onChanged != null) {
212277
widget.onChanged(widget.value);
213278
}
@@ -241,8 +306,13 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
241306
}
242307

243308
void _handleChanged(bool selected) {
244-
if (selected)
309+
if (selected == null) {
310+
widget.onChanged(null);
311+
return;
312+
}
313+
if (selected) {
245314
widget.onChanged(widget.value);
315+
}
246316
}
247317

248318
@override
@@ -276,6 +346,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
276346
focusColor: widget.focusColor ?? themeData.focusColor,
277347
hoverColor: widget.hoverColor ?? themeData.hoverColor,
278348
onChanged: enabled ? _handleChanged : null,
349+
toggleable: widget.toggleable,
279350
additionalConstraints: additionalConstraints,
280351
vsync: this,
281352
hasFocus: _focused,
@@ -297,13 +368,15 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
297368
@required this.hoverColor,
298369
@required this.additionalConstraints,
299370
this.onChanged,
371+
@required this.toggleable,
300372
@required this.vsync,
301373
@required this.hasFocus,
302374
@required this.hovering,
303375
}) : assert(selected != null),
304376
assert(activeColor != null),
305377
assert(inactiveColor != null),
306378
assert(vsync != null),
379+
assert(toggleable != null),
307380
super(key: key);
308381

309382
final bool selected;
@@ -314,6 +387,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
314387
final Color focusColor;
315388
final Color hoverColor;
316389
final ValueChanged<bool> onChanged;
390+
final bool toggleable;
317391
final TickerProvider vsync;
318392
final BoxConstraints additionalConstraints;
319393

@@ -325,6 +399,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
325399
focusColor: focusColor,
326400
hoverColor: hoverColor,
327401
onChanged: onChanged,
402+
tristate: toggleable,
328403
vsync: vsync,
329404
additionalConstraints: additionalConstraints,
330405
hasFocus: hasFocus,
@@ -340,6 +415,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
340415
..focusColor = focusColor
341416
..hoverColor = hoverColor
342417
..onChanged = onChanged
418+
..tristate = toggleable
343419
..additionalConstraints = additionalConstraints
344420
..vsync = vsync
345421
..hasFocus = hasFocus
@@ -355,18 +431,19 @@ class _RenderRadio extends RenderToggleable {
355431
Color focusColor,
356432
Color hoverColor,
357433
ValueChanged<bool> onChanged,
434+
bool tristate,
358435
BoxConstraints additionalConstraints,
359436
@required TickerProvider vsync,
360437
bool hasFocus,
361438
bool hovering,
362439
}) : super(
363440
value: value,
364-
tristate: false,
365441
activeColor: activeColor,
366442
inactiveColor: inactiveColor,
367443
focusColor: focusColor,
368444
hoverColor: hoverColor,
369445
onChanged: onChanged,
446+
tristate: tristate,
370447
additionalConstraints: additionalConstraints,
371448
vsync: vsync,
372449
hasFocus: hasFocus,

packages/flutter/lib/src/material/radio_list_tile.dart

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ class RadioListTile<T> extends StatelessWidget {
309309
@required this.value,
310310
@required this.groupValue,
311311
@required this.onChanged,
312+
this.toggleable = false,
312313
this.activeColor,
313314
this.title,
314315
this.subtitle,
@@ -317,7 +318,9 @@ class RadioListTile<T> extends StatelessWidget {
317318
this.secondary,
318319
this.selected = false,
319320
this.controlAffinity = ListTileControlAffinity.platform,
320-
}) : assert(isThreeLine != null),
321+
322+
}) : assert(toggleable != null),
323+
assert(isThreeLine != null),
321324
assert(!isThreeLine || subtitle != null),
322325
assert(selected != null),
323326
assert(controlAffinity != null),
@@ -361,6 +364,62 @@ class RadioListTile<T> extends StatelessWidget {
361364
/// ```
362365
final ValueChanged<T> onChanged;
363366

367+
/// Set to true if this radio list tile is allowed to be returned to an
368+
/// indeterminate state by selecting it again when selected.
369+
///
370+
/// To indicate returning to an indeterminate state, [onChanged] will be
371+
/// called with null.
372+
///
373+
/// If true, [onChanged] can be called with [value] when selected while
374+
/// [groupValue] != [value], or with null when selected again while
375+
/// [groupValue] == [value].
376+
///
377+
/// If false, [onChanged] will be called with [value] when it is selected
378+
/// while [groupValue] != [value], and only by selecting another radio button
379+
/// in the group (i.e. changing the value of [groupValue]) can this radio
380+
/// list tile be unselected.
381+
///
382+
/// The default is false.
383+
///
384+
/// {@tool dartpad --template=stateful_widget_scaffold}
385+
/// This example shows how to enable deselecting a radio button by setting the
386+
/// [toggleable] attribute.
387+
///
388+
/// ```dart
389+
/// int groupValue;
390+
/// static const List<String> selections = <String>[
391+
/// 'Hercules Mulligan',
392+
/// 'Eliza Hamilton',
393+
/// 'Philip Schuyler',
394+
/// 'Maria Reynolds',
395+
/// 'Samuel Seabury',
396+
/// ];
397+
///
398+
/// @override
399+
/// Widget build(BuildContext context) {
400+
/// return Scaffold(
401+
/// body: ListView.builder(
402+
/// itemBuilder: (context, index) {
403+
/// return RadioListTile<int>(
404+
/// value: index,
405+
/// groupValue: groupValue,
406+
/// toggleable: true,
407+
/// title: Text(selections[index]),
408+
/// onChanged: (int value) {
409+
/// setState(() {
410+
/// groupValue = value;
411+
/// });
412+
/// },
413+
/// );
414+
/// },
415+
/// itemCount: selections.length,
416+
/// ),
417+
/// );
418+
/// }
419+
/// ```
420+
/// {@end-tool}
421+
final bool toggleable;
422+
364423
/// The color to use when this radio button is selected.
365424
///
366425
/// Defaults to accent color of the current [Theme].
@@ -416,6 +475,7 @@ class RadioListTile<T> extends StatelessWidget {
416475
value: value,
417476
groupValue: groupValue,
418477
onChanged: onChanged,
478+
toggleable: toggleable,
419479
activeColor: activeColor,
420480
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
421481
);
@@ -442,7 +502,15 @@ class RadioListTile<T> extends StatelessWidget {
442502
isThreeLine: isThreeLine,
443503
dense: dense,
444504
enabled: onChanged != null,
445-
onTap: onChanged != null && !checked ? () { onChanged(value); } : null,
505+
onTap: onChanged != null ? () {
506+
if (toggleable && checked) {
507+
onChanged(null);
508+
return;
509+
}
510+
if (!checked) {
511+
onChanged(value);
512+
}
513+
} : null,
446514
selected: selected,
447515
),
448516
),

0 commit comments

Comments
 (0)