mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Adds radio group widget r2 (#168161)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->
previous https://github.com/flutter/flutter/pull/167363
I have to factor out abstract class for RadioGroupRegistry and
RadioClient which are subclassed by RadioGroupState and RawRadio
respectively.
I have to do this because the RadioListTile that has 2 focusnode one for
listTile and one for the radio it builds. The issue is that RadioGroup's
keyboard shortcut has to tightly couples with the focus node of each
radio, but the radioListtile has to mute the radio's focusnode so it can
act as one control under keyboard shortcut
d582b35809/packages/flutter/lib/src/material/radio_list_tile.dart (L484)
Therefore i abstract the out the logic of RadioGroup so that another
widget can participate by implementing the interface.
fixes https://github.com/flutter/flutter/issues/113562
migration guide: https://github.com/flutter/website/pull/12080/files
## Pre-launch Checklist
- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
88bc2536d3
commit
d0058ec361
@ -870,7 +870,6 @@ class _RadiosState extends State<Radios> {
|
||||
title: const Text('Option 3'),
|
||||
value: Options.option3,
|
||||
groupValue: _selectedOption,
|
||||
onChanged: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -155,9 +155,9 @@ class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
|
||||
const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Radio<int>(value: 0, groupValue: 0, onChanged: null),
|
||||
Radio<int>(value: 1, groupValue: 0, onChanged: null),
|
||||
Radio<int>(value: 2, groupValue: 0, onChanged: null),
|
||||
Radio<int>(value: 0, groupValue: 0),
|
||||
Radio<int>(value: 1, groupValue: 0),
|
||||
Radio<int>(value: 2, groupValue: 0),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -179,7 +179,7 @@ class SelectionControls {
|
||||
);
|
||||
|
||||
// Creates a disabled radio button.
|
||||
const Radio<int>(value: 0, groupValue: 0, onChanged: null);
|
||||
const Radio<int>(value: 0, groupValue: 0);
|
||||
// END
|
||||
|
||||
// START selectioncontrols_switch
|
||||
|
@ -38,14 +38,15 @@ Widget get _radioPanelExpandIcon => _expandIcons.evaluate().toList()[1].widget;
|
||||
|
||||
bool _isRadioSelected(int index) => _radios[index].value == _radios[index].groupValue;
|
||||
|
||||
List<Radio<Location>> get _radios =>
|
||||
List<Radio<Location>>.from(_radioFinder.evaluate().map<Widget>((Element e) => e.widget));
|
||||
List<RadioListTile<Location>> get _radios => List<RadioListTile<Location>>.from(
|
||||
_radioFinder.evaluate().map<Widget>((Element e) => e.widget),
|
||||
);
|
||||
|
||||
// [find.byType] and [find.widgetWithText] do not match subclasses; `Radio` is
|
||||
// not sufficient to find a `Radio<_Location>`. Another approach is to grab the
|
||||
// `runtimeType` of a dummy instance; see
|
||||
// packages/flutter/test/material/radio_list_tile_test.dart.
|
||||
Finder get _radioFinder => find.byWidgetPredicate((Widget w) => w is Radio<Location>);
|
||||
Finder get _radioFinder => find.byWidgetPredicate((Widget w) => w is RadioListTile<Location>);
|
||||
|
||||
List<RadioListTile<Location>> get _radioListTiles => List<RadioListTile<Location>>.from(
|
||||
_radioListTilesFinder.evaluate().map<Widget>((Element e) => e.widget),
|
||||
|
@ -167,7 +167,7 @@ class _RadioDemoState extends State<_RadioDemo> with RestorationMixin {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
for (int index = 0; index < 2; ++index)
|
||||
Radio<int>(value: index, groupValue: radioValue.value, onChanged: null),
|
||||
Radio<int>(value: index, groupValue: radioValue.value),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -37,33 +37,25 @@ class _CupertinoRadioExampleState extends State<CupertinoRadioExample> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoListSection(
|
||||
children: <Widget>[
|
||||
CupertinoListTile(
|
||||
title: const Text('Lafayette'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.lafayette,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
return RadioGroup<SingingCharacter>(
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(
|
||||
title: Text('Lafayette'),
|
||||
leading: CupertinoRadio<SingingCharacter>(value: SingingCharacter.lafayette),
|
||||
),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: const Text('Thomas Jefferson'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.jefferson,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
CupertinoListTile(
|
||||
title: Text('Thomas Jefferson'),
|
||||
leading: CupertinoRadio<SingingCharacter>(value: SingingCharacter.jefferson),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -36,37 +36,33 @@ class _CupertinoRadioExampleState extends State<CupertinoRadioExample> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoListSection(
|
||||
children: <Widget>[
|
||||
CupertinoListTile(
|
||||
title: const Text('Hercules Mulligan'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.mulligan,
|
||||
groupValue: _character,
|
||||
// TRY THIS: Try setting the toggleable value to false and
|
||||
// see how that changes the behavior of the widget.
|
||||
toggleable: true,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
return RadioGroup<SingingCharacter>(
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(
|
||||
title: Text('Hercules Mulligan'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.mulligan,
|
||||
// TRY THIS: Try setting the toggleable value to false and
|
||||
// see how that changes the behavior of the widget.
|
||||
toggleable: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: const Text('Eliza Hamilton'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.hamilton,
|
||||
groupValue: _character,
|
||||
toggleable: true,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
CupertinoListTile(
|
||||
title: Text('Eliza Hamilton'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.hamilton,
|
||||
toggleable: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -36,33 +36,25 @@ class _RadioExampleState extends State<RadioExample> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: const Text('Lafayette'),
|
||||
leading: Radio<SingingCharacter>(
|
||||
value: SingingCharacter.lafayette,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
return RadioGroup<SingingCharacter>(
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
child: const Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Lafayette'),
|
||||
leading: Radio<SingingCharacter>(value: SingingCharacter.lafayette),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Thomas Jefferson'),
|
||||
leading: Radio<SingingCharacter>(
|
||||
value: SingingCharacter.jefferson,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
ListTile(
|
||||
title: Text('Thomas Jefferson'),
|
||||
leading: Radio<SingingCharacter>(value: SingingCharacter.jefferson),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -42,28 +42,30 @@ class _ToggleableExampleState extends State<ToggleableExample> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Radio<int>(
|
||||
value: index,
|
||||
groupValue: groupValue,
|
||||
// TRY THIS: Try setting the toggleable value to false and
|
||||
// see how that changes the behavior of the widget.
|
||||
toggleable: true,
|
||||
onChanged: (int? value) {
|
||||
setState(() {
|
||||
groupValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(selections[index]),
|
||||
],
|
||||
);
|
||||
body: RadioGroup<int>(
|
||||
groupValue: groupValue,
|
||||
onChanged: (int? value) {
|
||||
setState(() {
|
||||
groupValue = value;
|
||||
});
|
||||
},
|
||||
itemCount: selections.length,
|
||||
child: ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Radio<int>(
|
||||
value: index,
|
||||
// TRY THIS: Try setting the toggleable value to false and
|
||||
// see how that changes the behavior of the widget.
|
||||
toggleable: true,
|
||||
),
|
||||
Text(selections[index]),
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: selections.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -29,16 +29,12 @@ class LinkedLabelRadio extends StatelessWidget {
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.padding,
|
||||
required this.groupValue,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final EdgeInsets padding;
|
||||
final bool groupValue;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -46,13 +42,7 @@ class LinkedLabelRadio extends StatelessWidget {
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Radio<bool>(
|
||||
groupValue: groupValue,
|
||||
value: value,
|
||||
onChanged: (bool? newValue) {
|
||||
onChanged(newValue!);
|
||||
},
|
||||
),
|
||||
Radio<bool>(value: value),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
@ -86,32 +76,28 @@ class _LabeledRadioExampleState extends State<LabeledRadioExample> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
LinkedLabelRadio(
|
||||
label: 'First tappable label text',
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: true,
|
||||
groupValue: _isRadioSelected,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
_isRadioSelected = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
LinkedLabelRadio(
|
||||
label: 'Second tappable label text',
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: false,
|
||||
groupValue: _isRadioSelected,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
_isRadioSelected = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
body: RadioGroup<bool>(
|
||||
groupValue: _isRadioSelected,
|
||||
onChanged: (bool? newValue) {
|
||||
setState(() {
|
||||
_isRadioSelected = newValue!;
|
||||
});
|
||||
},
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
LinkedLabelRadio(
|
||||
label: 'First tappable label text',
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: true,
|
||||
),
|
||||
LinkedLabelRadio(
|
||||
label: 'Second tappable label text',
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -23,43 +23,21 @@ class LabeledRadioApp extends StatelessWidget {
|
||||
}
|
||||
|
||||
class LabeledRadio extends StatelessWidget {
|
||||
const LabeledRadio({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.padding,
|
||||
required this.groupValue,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
const LabeledRadio({super.key, required this.label, required this.padding, required this.value});
|
||||
|
||||
final String label;
|
||||
final EdgeInsets padding;
|
||||
final bool groupValue;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (value != groupValue) {
|
||||
onChanged(value);
|
||||
}
|
||||
RadioGroup.maybeOf<bool>(context)?.onChanged(value);
|
||||
},
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Radio<bool>(
|
||||
groupValue: groupValue,
|
||||
value: value,
|
||||
onChanged: (bool? newValue) {
|
||||
onChanged(newValue!);
|
||||
},
|
||||
),
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
child: Row(children: <Widget>[Radio<bool>(value: value), Text(label)]),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -78,32 +56,28 @@ class _LabeledRadioExampleState extends State<LabeledRadioExample> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <LabeledRadio>[
|
||||
LabeledRadio(
|
||||
label: 'This is the first label text',
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: true,
|
||||
groupValue: _isRadioSelected,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
_isRadioSelected = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
LabeledRadio(
|
||||
label: 'This is the second label text',
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: false,
|
||||
groupValue: _isRadioSelected,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
_isRadioSelected = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
body: RadioGroup<bool>(
|
||||
groupValue: _isRadioSelected,
|
||||
onChanged: (bool? newValue) {
|
||||
setState(() {
|
||||
_isRadioSelected = newValue!;
|
||||
});
|
||||
},
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <LabeledRadio>[
|
||||
LabeledRadio(
|
||||
label: 'This is the first label text',
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: true,
|
||||
),
|
||||
LabeledRadio(
|
||||
label: 'This is the second label text',
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
value: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -36,29 +36,25 @@ class _RadioListTileExampleState extends State<RadioListTileExample> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
RadioListTile<SingingCharacter>(
|
||||
title: const Text('Lafayette'),
|
||||
value: SingingCharacter.lafayette,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
RadioListTile<SingingCharacter>(
|
||||
title: const Text('Thomas Jefferson'),
|
||||
value: SingingCharacter.jefferson,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
return RadioGroup<SingingCharacter>(
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
child: const Column(
|
||||
children: <Widget>[
|
||||
RadioListTile<SingingCharacter>(
|
||||
title: Text('Lafayette'),
|
||||
value: SingingCharacter.lafayette,
|
||||
),
|
||||
RadioListTile<SingingCharacter>(
|
||||
title: Text('Thomas Jefferson'),
|
||||
value: SingingCharacter.jefferson,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,47 +33,37 @@ class _RadioListTileExampleState extends State<RadioListTileExample> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('RadioListTile Sample')),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
RadioListTile<Groceries>(
|
||||
value: Groceries.pickles,
|
||||
groupValue: _groceryItem,
|
||||
onChanged: (Groceries? value) {
|
||||
setState(() {
|
||||
_groceryItem = value;
|
||||
});
|
||||
},
|
||||
title: const Text('Pickles'),
|
||||
subtitle: const Text('Supporting text'),
|
||||
),
|
||||
RadioListTile<Groceries>(
|
||||
value: Groceries.tomato,
|
||||
groupValue: _groceryItem,
|
||||
onChanged: (Groceries? value) {
|
||||
setState(() {
|
||||
_groceryItem = value;
|
||||
});
|
||||
},
|
||||
title: const Text('Tomato'),
|
||||
subtitle: const Text(
|
||||
'Longer supporting text to demonstrate how the text wraps and the radio is centered vertically with the text.',
|
||||
body: RadioGroup<Groceries>(
|
||||
groupValue: _groceryItem,
|
||||
onChanged: (Groceries? value) {
|
||||
setState(() {
|
||||
_groceryItem = value;
|
||||
});
|
||||
},
|
||||
child: const Column(
|
||||
children: <Widget>[
|
||||
RadioListTile<Groceries>(
|
||||
value: Groceries.pickles,
|
||||
title: Text('Pickles'),
|
||||
subtitle: Text('Supporting text'),
|
||||
),
|
||||
),
|
||||
RadioListTile<Groceries>(
|
||||
value: Groceries.lettuce,
|
||||
groupValue: _groceryItem,
|
||||
onChanged: (Groceries? value) {
|
||||
setState(() {
|
||||
_groceryItem = value;
|
||||
});
|
||||
},
|
||||
title: const Text('Lettuce'),
|
||||
subtitle: const Text(
|
||||
"Longer supporting text to demonstrate how the text wraps and how setting 'RadioListTile.isThreeLine = true' aligns the radio to the top vertically with the text.",
|
||||
RadioListTile<Groceries>(
|
||||
value: Groceries.tomato,
|
||||
title: Text('Tomato'),
|
||||
subtitle: Text(
|
||||
'Longer supporting text to demonstrate how the text wraps and the radio is centered vertically with the text.',
|
||||
),
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
],
|
||||
RadioListTile<Groceries>(
|
||||
value: Groceries.lettuce,
|
||||
title: Text('Lettuce'),
|
||||
subtitle: Text(
|
||||
"Longer supporting text to demonstrate how the text wraps and how setting 'RadioListTile.isThreeLine = true' aligns the radio to the top vertically with the text.",
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -42,21 +42,23 @@ class _RadioListTileExampleState extends State<RadioListTileExample> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return RadioListTile<int>(
|
||||
value: index,
|
||||
groupValue: groupValue,
|
||||
toggleable: true,
|
||||
title: Text(selections[index]),
|
||||
onChanged: (int? value) {
|
||||
setState(() {
|
||||
groupValue = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
body: RadioGroup<int>(
|
||||
groupValue: groupValue,
|
||||
onChanged: (int? value) {
|
||||
setState(() {
|
||||
groupValue = value;
|
||||
});
|
||||
},
|
||||
itemCount: selections.length,
|
||||
child: ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return RadioListTile<int>(
|
||||
value: index,
|
||||
toggleable: true,
|
||||
title: Text(selections[index]),
|
||||
);
|
||||
},
|
||||
itemCount: selections.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
108
examples/api/lib/widgets/radio_group/radio_group.0.dart
Normal file
108
examples/api/lib/widgets/radio_group/radio_group.0.dart
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [Radio].
|
||||
|
||||
void main() => runApp(const RadioExampleApp());
|
||||
|
||||
class RadioExampleApp extends StatelessWidget {
|
||||
const RadioExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('Radio Group Sample')),
|
||||
body: const Center(child: RadioExample()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum SingingCharacter { lafayette, jefferson }
|
||||
|
||||
enum Genre { metal, jazz, blues }
|
||||
|
||||
class RadioExample extends StatelessWidget {
|
||||
const RadioExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(children: <Widget>[SingingCharacterRadioGroup(), GenreRadioGroup()]);
|
||||
}
|
||||
}
|
||||
|
||||
class SingingCharacterRadioGroup extends StatefulWidget {
|
||||
const SingingCharacterRadioGroup({super.key});
|
||||
|
||||
@override
|
||||
State<SingingCharacterRadioGroup> createState() => SingingCharacterRadioGroupState();
|
||||
}
|
||||
|
||||
class SingingCharacterRadioGroupState extends State<SingingCharacterRadioGroup> {
|
||||
SingingCharacter? _character = SingingCharacter.lafayette;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RadioGroup<SingingCharacter>(
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('Selected: $_character'),
|
||||
const ListTile(
|
||||
title: Text('Lafayette'),
|
||||
leading: Radio<SingingCharacter>(value: SingingCharacter.lafayette),
|
||||
),
|
||||
const ListTile(
|
||||
title: Text('Thomas Jefferson'),
|
||||
leading: Radio<SingingCharacter>(value: SingingCharacter.jefferson),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GenreRadioGroup extends StatefulWidget {
|
||||
const GenreRadioGroup({super.key});
|
||||
|
||||
@override
|
||||
State<GenreRadioGroup> createState() => GenreRadioGroupState();
|
||||
}
|
||||
|
||||
class GenreRadioGroupState extends State<GenreRadioGroup> {
|
||||
Genre? _genre;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RadioGroup<Genre>(
|
||||
groupValue: _genre,
|
||||
onChanged: (Genre? value) {
|
||||
setState(() {
|
||||
_genre = value;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('Selected: $_genre'),
|
||||
const ListTile(
|
||||
title: Text('Metal'),
|
||||
leading: Radio<Genre>(toggleable: true, value: Genre.metal),
|
||||
),
|
||||
const ListTile(title: Text('Jazz'), leading: Radio<Genre>(value: Genre.jazz)),
|
||||
const ListTile(title: Text('Blues'), leading: Radio<Genre>(value: Genre.blues)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -12,21 +12,15 @@ void main() {
|
||||
|
||||
expect(find.byType(CupertinoRadio<example.SingingCharacter>), findsNWidgets(2));
|
||||
|
||||
CupertinoRadio<example.SingingCharacter> radio = tester.widget(
|
||||
find.byType(CupertinoRadio<example.SingingCharacter>).first,
|
||||
RadioGroup<example.SingingCharacter> group = tester.widget(
|
||||
find.byType(RadioGroup<example.SingingCharacter>),
|
||||
);
|
||||
expect(radio.groupValue, example.SingingCharacter.lafayette);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.lafayette);
|
||||
expect(group.groupValue, example.SingingCharacter.lafayette);
|
||||
|
||||
await tester.tap(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.jefferson);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).first);
|
||||
expect(radio.groupValue, example.SingingCharacter.jefferson);
|
||||
group = tester.widget(find.byType(RadioGroup<example.SingingCharacter>));
|
||||
expect(group.groupValue, example.SingingCharacter.jefferson);
|
||||
});
|
||||
}
|
||||
|
@ -12,27 +12,21 @@ void main() {
|
||||
|
||||
expect(find.byType(CupertinoRadio<example.SingingCharacter>), findsNWidgets(2));
|
||||
|
||||
CupertinoRadio<example.SingingCharacter> radio = tester.widget(
|
||||
find.byType(CupertinoRadio<example.SingingCharacter>).first,
|
||||
RadioGroup<example.SingingCharacter> group = tester.widget(
|
||||
find.byType(RadioGroup<example.SingingCharacter>),
|
||||
);
|
||||
expect(radio.groupValue, example.SingingCharacter.mulligan);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.mulligan);
|
||||
expect(group.groupValue, example.SingingCharacter.mulligan);
|
||||
|
||||
await tester.tap(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.hamilton);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).first);
|
||||
expect(radio.groupValue, example.SingingCharacter.hamilton);
|
||||
group = tester.widget(find.byType(RadioGroup<example.SingingCharacter>));
|
||||
expect(group.groupValue, example.SingingCharacter.hamilton);
|
||||
|
||||
await tester.tap(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, null);
|
||||
group = tester.widget(find.byType(RadioGroup<example.SingingCharacter>));
|
||||
expect(group.groupValue, null);
|
||||
});
|
||||
}
|
||||
|
@ -18,25 +18,26 @@ void main() {
|
||||
|
||||
final Finder radioButton1 = find.byType(Radio<example.SingingCharacter>).first;
|
||||
final Finder radioButton2 = find.byType(Radio<example.SingingCharacter>).last;
|
||||
final Finder radioGroup = find.byType(RadioGroup<example.SingingCharacter>).last;
|
||||
|
||||
await tester.tap(radioButton1);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton1).groupValue,
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton1).value,
|
||||
);
|
||||
expect(
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton2).groupValue,
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
isNot(tester.widget<Radio<example.SingingCharacter>>(radioButton2).value),
|
||||
);
|
||||
await tester.tap(radioButton2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton1).groupValue,
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
isNot(tester.widget<Radio<example.SingingCharacter>>(radioButton1).value),
|
||||
);
|
||||
expect(
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton2).groupValue,
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton2).value,
|
||||
);
|
||||
});
|
||||
|
@ -21,8 +21,10 @@ void main() {
|
||||
await tester.tap(find.byType(Radio<int>).at(i));
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.byWidgetPredicate((Widget widget) => widget is Radio<int> && widget.groupValue == i),
|
||||
findsExactly(5),
|
||||
find.byWidgetPredicate(
|
||||
(Widget widget) => widget is RadioGroup<int> && widget.groupValue == i,
|
||||
),
|
||||
findsOne,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -15,28 +15,16 @@ void main() {
|
||||
final RichText richText = tester.widget(find.byType(RichText).first);
|
||||
expect(richText.text.toPlainText(), 'First tappable label text');
|
||||
|
||||
// First Radio is initially unchecked.
|
||||
Radio<bool> radio = tester.widget(find.byType(Radio<bool>).first);
|
||||
expect(radio.value, true);
|
||||
expect(radio.groupValue, false);
|
||||
|
||||
// Last Radio is initially checked.
|
||||
radio = tester.widget(find.byType(Radio<bool>).last);
|
||||
expect(radio.value, false);
|
||||
expect(radio.groupValue, false);
|
||||
RadioGroup<bool> group = tester.widget<RadioGroup<bool>>(find.byType(RadioGroup<bool>));
|
||||
// Second radio is checked.
|
||||
expect(group.groupValue, isFalse);
|
||||
|
||||
// Tap the first radio.
|
||||
await tester.tap(find.byType(Radio<bool>).first);
|
||||
await tester.pump();
|
||||
|
||||
// First Radio is now checked.
|
||||
radio = tester.widget(find.byType(Radio<bool>).first);
|
||||
expect(radio.value, true);
|
||||
expect(radio.groupValue, true);
|
||||
|
||||
// Last Radio is now unchecked.
|
||||
radio = tester.widget(find.byType(Radio<bool>).last);
|
||||
expect(radio.value, false);
|
||||
expect(radio.groupValue, true);
|
||||
group = tester.widget<RadioGroup<bool>>(find.byType(RadioGroup<bool>));
|
||||
expect(group.groupValue, true);
|
||||
});
|
||||
}
|
||||
|
@ -11,28 +11,16 @@ void main() {
|
||||
testWidgets('Tapping LabeledRadio toggles the radio', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.LabeledRadioApp());
|
||||
|
||||
// First Radio is initially unchecked.
|
||||
Radio<bool> radio = tester.widget(find.byType(Radio<bool>).first);
|
||||
expect(radio.value, true);
|
||||
expect(radio.groupValue, false);
|
||||
|
||||
// Last Radio is initially checked.
|
||||
radio = tester.widget(find.byType(Radio<bool>).last);
|
||||
expect(radio.value, false);
|
||||
expect(radio.groupValue, false);
|
||||
RadioGroup<bool> group = tester.widget<RadioGroup<bool>>(find.byType(RadioGroup<bool>));
|
||||
// Second radio is checked.
|
||||
expect(group.groupValue, isFalse);
|
||||
|
||||
// Tap the first labeled radio to toggle the Radio widget.
|
||||
await tester.tap(find.byType(example.LabeledRadio).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// First Radio is now checked.
|
||||
radio = tester.widget(find.byType(Radio<bool>).first);
|
||||
expect(radio.value, true);
|
||||
expect(radio.groupValue, true);
|
||||
|
||||
// Last Radio is now unchecked.
|
||||
radio = tester.widget(find.byType(Radio<bool>).last);
|
||||
expect(radio.value, false);
|
||||
expect(radio.groupValue, true);
|
||||
group = tester.widget<RadioGroup<bool>>(find.byType(RadioGroup<bool>));
|
||||
// Second radio is checked.
|
||||
expect(group.groupValue, isTrue);
|
||||
});
|
||||
}
|
||||
|
@ -13,26 +13,23 @@ void main() {
|
||||
// Find the number of RadioListTiles.
|
||||
expect(find.byType(RadioListTile<example.SingingCharacter>), findsNWidgets(2));
|
||||
|
||||
// The initial group value is lafayette for the first RadioListTile.
|
||||
RadioListTile<example.SingingCharacter> radioListTile = tester.widget(
|
||||
find.byType(RadioListTile<example.SingingCharacter>).first,
|
||||
);
|
||||
expect(radioListTile.groupValue, example.SingingCharacter.lafayette);
|
||||
|
||||
// The initial group value is lafayette for the last RadioListTile.
|
||||
radioListTile = tester.widget(find.byType(RadioListTile<example.SingingCharacter>).last);
|
||||
expect(radioListTile.groupValue, example.SingingCharacter.lafayette);
|
||||
// The initial group value is lafayette.
|
||||
RadioGroup<example.SingingCharacter> group = tester
|
||||
.widget<RadioGroup<example.SingingCharacter>>(
|
||||
find.byType(RadioGroup<example.SingingCharacter>),
|
||||
);
|
||||
// Second radio is checked.
|
||||
expect(group.groupValue, example.SingingCharacter.lafayette);
|
||||
|
||||
// Tap the last RadioListTile to change the group value to jefferson.
|
||||
await tester.tap(find.byType(RadioListTile<example.SingingCharacter>).last);
|
||||
await tester.pump();
|
||||
|
||||
// The group value is now jefferson for the first RadioListTile.
|
||||
radioListTile = tester.widget(find.byType(RadioListTile<example.SingingCharacter>).first);
|
||||
expect(radioListTile.groupValue, example.SingingCharacter.jefferson);
|
||||
|
||||
// The group value is now jefferson for the last RadioListTile.
|
||||
radioListTile = tester.widget(find.byType(RadioListTile<example.SingingCharacter>).last);
|
||||
expect(radioListTile.groupValue, example.SingingCharacter.jefferson);
|
||||
// The group value is now jefferson.
|
||||
group = tester.widget<RadioGroup<example.SingingCharacter>>(
|
||||
find.byType(RadioGroup<example.SingingCharacter>),
|
||||
);
|
||||
// Second radio is checked.
|
||||
expect(group.groupValue, example.SingingCharacter.jefferson);
|
||||
});
|
||||
}
|
||||
|
@ -35,56 +35,31 @@ void main() {
|
||||
await tester.pumpWidget(const example.RadioListTileApp());
|
||||
|
||||
expect(find.byType(RadioListTile<example.Groceries>), findsNWidgets(3));
|
||||
final Finder radioListTile = find.byType(RadioListTile<example.Groceries>);
|
||||
|
||||
// Initially the first radio is checked.
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(0)).groupValue,
|
||||
example.Groceries.pickles,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(1)).groupValue,
|
||||
example.Groceries.pickles,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(2)).groupValue,
|
||||
example.Groceries.pickles,
|
||||
RadioGroup<example.Groceries> group = tester.widget<RadioGroup<example.Groceries>>(
|
||||
find.byType(RadioGroup<example.Groceries>),
|
||||
);
|
||||
expect(group.groupValue, example.Groceries.pickles);
|
||||
|
||||
// Tap the second radio.
|
||||
await tester.tap(find.byType(Radio<example.Groceries>).at(1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The second radio is checked.
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(0)).groupValue,
|
||||
example.Groceries.tomato,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(1)).groupValue,
|
||||
example.Groceries.tomato,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(2)).groupValue,
|
||||
example.Groceries.tomato,
|
||||
group = tester.widget<RadioGroup<example.Groceries>>(
|
||||
find.byType(RadioGroup<example.Groceries>),
|
||||
);
|
||||
expect(group.groupValue, example.Groceries.tomato);
|
||||
|
||||
// Tap the third radio.
|
||||
await tester.tap(find.byType(Radio<example.Groceries>).at(2));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The third radio is checked.
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(0)).groupValue,
|
||||
example.Groceries.lettuce,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(1)).groupValue,
|
||||
example.Groceries.lettuce,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioListTile<example.Groceries>>(radioListTile.at(2)).groupValue,
|
||||
example.Groceries.lettuce,
|
||||
group = tester.widget<RadioGroup<example.Groceries>>(
|
||||
find.byType(RadioGroup<example.Groceries>),
|
||||
);
|
||||
expect(group.groupValue, example.Groceries.lettuce);
|
||||
});
|
||||
}
|
||||
|
@ -12,26 +12,23 @@ void main() {
|
||||
await tester.pumpWidget(const example.RadioListTileApp());
|
||||
|
||||
// Initially the third radio button is not selected.
|
||||
Radio<int> radio = tester.widget(find.byType(Radio<int>).at(2));
|
||||
expect(radio.value, 2);
|
||||
expect(radio.groupValue, null);
|
||||
RadioGroup<int> group = tester.widget<RadioGroup<int>>(find.byType(RadioGroup<int>));
|
||||
expect(group.groupValue, null);
|
||||
|
||||
// Tap the third radio button.
|
||||
await tester.tap(find.text('Philip Schuyler'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The third radio button is now selected.
|
||||
radio = tester.widget(find.byType(Radio<int>).at(2));
|
||||
expect(radio.value, 2);
|
||||
expect(radio.groupValue, 2);
|
||||
group = tester.widget<RadioGroup<int>>(find.byType(RadioGroup<int>));
|
||||
expect(group.groupValue, 2);
|
||||
|
||||
// Tap the third radio button again.
|
||||
await tester.tap(find.text('Philip Schuyler'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The third radio button is now unselected.
|
||||
radio = tester.widget(find.byType(Radio<int>).at(2));
|
||||
expect(radio.value, 2);
|
||||
expect(radio.groupValue, null);
|
||||
group = tester.widget<RadioGroup<int>>(find.byType(RadioGroup<int>));
|
||||
expect(group.groupValue, null);
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/widgets/radio_group/radio_group.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Radio Smoke Test - character', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.RadioExampleApp());
|
||||
|
||||
expect(find.widgetWithText(AppBar, 'Radio Group Sample'), findsOneWidget);
|
||||
final Finder listTile1 = find.widgetWithText(ListTile, 'Lafayette');
|
||||
expect(listTile1, findsOneWidget);
|
||||
final Finder listTile2 = find.widgetWithText(ListTile, 'Thomas Jefferson');
|
||||
expect(listTile2, findsOneWidget);
|
||||
|
||||
final Finder radioButton1 = find.byType(Radio<example.SingingCharacter>).first;
|
||||
final Finder radioButton2 = find.byType(Radio<example.SingingCharacter>).last;
|
||||
final Finder radioGroup = find.byType(RadioGroup<example.SingingCharacter>).last;
|
||||
|
||||
await tester.tap(radioButton1);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton1).value,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
isNot(tester.widget<Radio<example.SingingCharacter>>(radioButton2).value),
|
||||
);
|
||||
await tester.tap(radioButton2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
isNot(tester.widget<Radio<example.SingingCharacter>>(radioButton1).value),
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.SingingCharacter>>(radioGroup).groupValue,
|
||||
tester.widget<Radio<example.SingingCharacter>>(radioButton2).value,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio Smoke Test - genre', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.RadioExampleApp());
|
||||
|
||||
expect(find.widgetWithText(AppBar, 'Radio Group Sample'), findsOneWidget);
|
||||
final Finder listTile1 = find.widgetWithText(ListTile, 'Metal');
|
||||
expect(listTile1, findsOneWidget);
|
||||
final Finder listTile2 = find.widgetWithText(ListTile, 'Jazz');
|
||||
expect(listTile2, findsOneWidget);
|
||||
|
||||
final Finder radioButton1 = find.byType(Radio<example.Genre>).first;
|
||||
final Finder radioButton2 = find.byType(Radio<example.Genre>).last;
|
||||
final Finder radioGroup = find.byType(RadioGroup<example.Genre>).last;
|
||||
|
||||
await tester.tap(radioButton1);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.Genre>>(radioGroup).groupValue,
|
||||
tester.widget<Radio<example.Genre>>(radioButton1).value,
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.Genre>>(radioGroup).groupValue,
|
||||
isNot(tester.widget<Radio<example.Genre>>(radioButton2).value),
|
||||
);
|
||||
await tester.tap(radioButton2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.Genre>>(radioGroup).groupValue,
|
||||
isNot(tester.widget<Radio<example.Genre>>(radioButton1).value),
|
||||
);
|
||||
expect(
|
||||
tester.widget<RadioGroup<example.Genre>>(radioGroup).groupValue,
|
||||
tester.widget<Radio<example.Genre>>(radioButton2).value,
|
||||
);
|
||||
});
|
||||
}
|
@ -57,7 +57,7 @@ const double _kBorderOutlineStrokeWidth = 0.3;
|
||||
const List<double> _kDarkGradientOpacities = <double>[0.14, 0.29];
|
||||
const List<double> _kDisabledDarkGradientOpacities = <double>[0.08, 0.14];
|
||||
|
||||
/// A macOS-style radio button.
|
||||
/// A widget that builds a [RawRadio] with a macOS-style UI.
|
||||
///
|
||||
/// Used to select between a number of mutually exclusive values. When one radio
|
||||
/// button in a group is selected, the other radio buttons in the group are
|
||||
@ -103,8 +103,16 @@ class CupertinoRadio<T> extends StatefulWidget {
|
||||
const CupertinoRadio({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.groupValue,
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.onChanged,
|
||||
this.mouseCursor,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
@ -114,15 +122,21 @@ class CupertinoRadio<T> extends StatefulWidget {
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.useCheckmarkStyle = false,
|
||||
this.enabled,
|
||||
this.groupRegistry,
|
||||
});
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.value}
|
||||
final T value;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.groupValue}
|
||||
/// {@macro flutter.material.Radio.groupValue}
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
final T? groupValue;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.onChanged}
|
||||
/// {@macro flutter.material.Radio.onChanged}
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
@ -137,6 +151,10 @@ class CupertinoRadio<T> extends StatefulWidget {
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.mouseCursor}
|
||||
@ -194,6 +212,15 @@ class CupertinoRadio<T> extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.groupRegistry}
|
||||
///
|
||||
/// Unless provided, the [BuildContext] will be used to look up the ancestor
|
||||
/// [RadioGroupRegistry].
|
||||
final RadioGroupRegistry<T>? groupRegistry;
|
||||
|
||||
/// {@macro flutter.material.Radio.enabled}
|
||||
final bool? enabled;
|
||||
|
||||
@override
|
||||
State<CupertinoRadio<T>> createState() => _CupertinoRadioState<T>();
|
||||
}
|
||||
@ -202,6 +229,27 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> {
|
||||
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_internalFocusNode ??= FocusNode());
|
||||
FocusNode? _internalFocusNode;
|
||||
|
||||
bool get _enabled =>
|
||||
widget.enabled ??
|
||||
(widget.onChanged != null ||
|
||||
widget.groupRegistry != null ||
|
||||
RadioGroup.maybeOf<T>(context) != null);
|
||||
|
||||
_RadioRegistry<T>? _internalRadioRegistry;
|
||||
RadioGroupRegistry<T> get _effectiveRegistry {
|
||||
if (widget.groupRegistry != null) {
|
||||
return widget.groupRegistry!;
|
||||
}
|
||||
|
||||
final RadioGroupRegistry<T>? inheritedRegistry = RadioGroup.maybeOf<T>(context);
|
||||
if (inheritedRegistry != null) {
|
||||
return inheritedRegistry;
|
||||
}
|
||||
|
||||
// Handles deprecated API.
|
||||
return _internalRadioRegistry ??= _RadioRegistry<T>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_internalFocusNode?.dispose();
|
||||
@ -210,6 +258,14 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(
|
||||
!(widget.enabled ?? false) ||
|
||||
widget.onChanged != null ||
|
||||
widget.groupRegistry != null ||
|
||||
RadioGroup.maybeOf<T>(context) != null,
|
||||
'Radio is enabled but has no CupertinoRadio.onChange, '
|
||||
'CupertinoRadio.groupRegistry, or RadioGroup above',
|
||||
);
|
||||
final WidgetStateProperty<MouseCursor> effectiveMouseCursor =
|
||||
WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
|
||||
return WidgetStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ??
|
||||
@ -220,12 +276,12 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> {
|
||||
|
||||
return RawRadio<T>(
|
||||
value: widget.value,
|
||||
groupValue: widget.groupValue,
|
||||
onChanged: widget.onChanged,
|
||||
groupRegistry: _effectiveRegistry,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
toggleable: widget.toggleable,
|
||||
focusNode: _effectiveFocusNode,
|
||||
autofocus: widget.autofocus,
|
||||
enabled: _enabled,
|
||||
builder: (BuildContext context, ToggleableStateMixin state) {
|
||||
return _RadioPaint(
|
||||
activeColor: widget.activeColor,
|
||||
@ -233,7 +289,7 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> {
|
||||
fillColor: widget.fillColor,
|
||||
focusColor: widget.focusColor,
|
||||
useCheckmarkStyle: widget.useCheckmarkStyle,
|
||||
isActive: widget.onChanged != null,
|
||||
isActive: _enabled,
|
||||
toggleableState: state,
|
||||
focused: _effectiveFocusNode.hasFocus,
|
||||
);
|
||||
@ -242,6 +298,24 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A registry for deprecated API.
|
||||
// TODO(chunhtai): Remove this once deprecated API is removed.
|
||||
class _RadioRegistry<T> extends RadioGroupRegistry<T> {
|
||||
_RadioRegistry(this.state);
|
||||
final _CupertinoRadioState<T> state;
|
||||
@override
|
||||
T? get groupValue => state.widget.groupValue;
|
||||
|
||||
@override
|
||||
ValueChanged<T?> get onChanged => state.widget.onChanged!;
|
||||
|
||||
@override
|
||||
void registerClient(RadioClient<T> radio) {}
|
||||
|
||||
@override
|
||||
void unregisterClient(RadioClient<T> radio) {}
|
||||
}
|
||||
|
||||
class _RadioPaint extends StatefulWidget {
|
||||
const _RadioPaint({
|
||||
required this.focused,
|
||||
|
@ -34,23 +34,23 @@ const double _kInnerRadius = 4.5;
|
||||
|
||||
/// A Material Design radio button.
|
||||
///
|
||||
/// This widget builds a [RawRadio] with a material UI.
|
||||
///
|
||||
/// Used to select between a number of mutually exclusive values. When one radio
|
||||
/// button in a group is selected, the other radio buttons in the group cease to
|
||||
/// be selected. The values are of type `T`, the type parameter of the [Radio]
|
||||
/// class. Enums are commonly used for this purpose.
|
||||
///
|
||||
/// The radio button itself does not maintain any state. Instead, selecting the
|
||||
/// radio invokes the [onChanged] callback, passing [value] as a parameter. If
|
||||
/// [groupValue] and [value] match, this radio will be selected. Most widgets
|
||||
/// will respond to [onChanged] by calling [State.setState] to update the
|
||||
/// radio button's [groupValue].
|
||||
/// This widget typically has a [RadioGroup] ancestor, which takes in a
|
||||
/// [RadioGroup.groupValue], and the [Radio] under it with matching [value]
|
||||
/// will be selected.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Here is an example of Radio widgets wrapped in ListTiles, which is similar
|
||||
/// to what you could get with the RadioListTile widget.
|
||||
///
|
||||
/// The currently selected character is passed into `groupValue`, which is
|
||||
/// maintained by the example's `State`. In this case, the first [Radio]
|
||||
/// The currently selected character is passed into `RadioGroup.groupValue`,
|
||||
/// which is maintained by the example's `State`. In this case, the first [Radio]
|
||||
/// will start off selected because `_character` is initialized to
|
||||
/// `SingingCharacter.lafayette`.
|
||||
///
|
||||
@ -71,25 +71,27 @@ const double _kInnerRadius = 4.5;
|
||||
/// * [Slider], for selecting a value in a range.
|
||||
/// * [Checkbox] and [Switch], for toggling a particular value on or off.
|
||||
/// * <https://material.io/design/components/selection-controls.html#radio-buttons>
|
||||
class Radio<T> extends StatelessWidget {
|
||||
class Radio<T> extends StatefulWidget {
|
||||
/// Creates a Material Design radio button.
|
||||
///
|
||||
/// The radio button itself does not maintain any state. Instead, when the
|
||||
/// radio button is selected, the widget calls the [onChanged] callback. Most
|
||||
/// widgets that use a radio button will listen for the [onChanged] callback
|
||||
/// and rebuild the radio button with a new [groupValue] to update the visual
|
||||
/// appearance of the radio button.
|
||||
/// This widget typically has a [RadioGroup] ancestor, which takes in a
|
||||
/// [RadioGroup.groupValue], and the [Radio] under it with matching [value]
|
||||
/// will be selected.
|
||||
///
|
||||
/// The following arguments are required:
|
||||
///
|
||||
/// * [value] and [groupValue] together determine whether the radio button is
|
||||
/// selected.
|
||||
/// * [onChanged] is called when the user selects this radio button.
|
||||
/// The [value] is required.
|
||||
const Radio({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.groupValue,
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.onChanged,
|
||||
this.mouseCursor,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
@ -102,6 +104,8 @@ class Radio<T> extends StatelessWidget {
|
||||
this.visualDensity,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.enabled,
|
||||
this.groupRegistry,
|
||||
}) : _radioType = _RadioType.material,
|
||||
useCupertinoCheckmarkStyle = false;
|
||||
|
||||
@ -124,8 +128,16 @@ class Radio<T> extends StatelessWidget {
|
||||
const Radio.adaptive({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.groupValue,
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.onChanged,
|
||||
this.mouseCursor,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
@ -139,15 +151,46 @@ class Radio<T> extends StatelessWidget {
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.useCupertinoCheckmarkStyle = false,
|
||||
this.enabled,
|
||||
this.groupRegistry,
|
||||
}) : _radioType = _RadioType.adaptive;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.value}
|
||||
final T value;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.groupValue}
|
||||
/// {@template flutter.material.Radio.groupValue}
|
||||
/// The currently selected value for a group of radio buttons.
|
||||
///
|
||||
/// This radio button is considered selected if its [value] matches the
|
||||
/// [groupValue].
|
||||
///
|
||||
/// This is deprecated, use [RadioGroup] to manage group value instead.
|
||||
/// {@endtemplate}
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
final T? groupValue;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.onChanged}
|
||||
/// {@template flutter.material.Radio.onChanged}
|
||||
/// Called when the user selects this radio button.
|
||||
///
|
||||
/// The radio button passes [value] as a parameter to this callback. The radio
|
||||
/// button does not actually change state until the parent widget rebuilds the
|
||||
/// radio button with the new [groupValue].
|
||||
///
|
||||
/// If null, the radio button will be displayed as disabled.
|
||||
///
|
||||
/// The provided callback will not be invoked if this radio button is already
|
||||
/// selected and [toggleable] is not set to true.
|
||||
///
|
||||
/// If the [toggleable] is set to true, tapping a already selected radio will
|
||||
/// invoke this callback with `null` as value.
|
||||
///
|
||||
/// The callback provided to [onChanged] should update the state of the parent
|
||||
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
||||
/// gets rebuilt.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
@ -162,6 +205,12 @@ class Radio<T> extends StatelessWidget {
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// This is deprecated, use [RadioGroup] to handle value change instead.
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.mouseCursor}
|
||||
@ -205,8 +254,6 @@ class Radio<T> extends StatelessWidget {
|
||||
/// ```dart
|
||||
/// Radio<int>(
|
||||
/// value: 1,
|
||||
/// groupValue: 1,
|
||||
/// onChanged: (_){},
|
||||
/// fillColor: WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
|
||||
/// if (states.contains(WidgetState.disabled)) {
|
||||
/// return Colors.orange.withOpacity(.32);
|
||||
@ -328,12 +375,75 @@ class Radio<T> extends StatelessWidget {
|
||||
/// Defaults to false.
|
||||
final bool useCupertinoCheckmarkStyle;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.groupRegistry}
|
||||
///
|
||||
/// Unless provided, the [BuildContext] will be used to look up the ancestor
|
||||
/// [RadioGroupRegistry].
|
||||
final RadioGroupRegistry<T>? groupRegistry;
|
||||
|
||||
final _RadioType _radioType;
|
||||
|
||||
/// {@template flutter.material.Radio.enabled}
|
||||
/// Whether this widget is interactive.
|
||||
///
|
||||
/// If not provided, this widget will be interactable if one of the following
|
||||
/// is true:
|
||||
///
|
||||
/// * A [onChanged] is provided.
|
||||
/// * Having a [RadioGroup] with the same type T above this widget.
|
||||
/// * A [groupRegistry] is provided.
|
||||
///
|
||||
/// If this is set to true, one of the above condition must also be true.
|
||||
/// Otherwise, an assertion error is thrown.
|
||||
/// {@endtemplate}
|
||||
final bool? enabled;
|
||||
|
||||
@override
|
||||
State<Radio<T>> createState() => _RadioState<T>();
|
||||
}
|
||||
|
||||
class _RadioState<T> extends State<Radio<T>> {
|
||||
FocusNode? _internalFocusNode;
|
||||
FocusNode get _focusNode => widget.focusNode ?? (_internalFocusNode ??= FocusNode());
|
||||
|
||||
bool get _enabled =>
|
||||
widget.enabled ??
|
||||
(widget.onChanged != null ||
|
||||
widget.groupRegistry != null ||
|
||||
RadioGroup.maybeOf<T>(context) != null);
|
||||
|
||||
_RadioRegistry<T>? _internalRadioRegistry;
|
||||
RadioGroupRegistry<T> get _effectiveRegistry {
|
||||
if (widget.groupRegistry != null) {
|
||||
return widget.groupRegistry!;
|
||||
}
|
||||
|
||||
final RadioGroupRegistry<T>? inheritedRegistry = RadioGroup.maybeOf<T>(context);
|
||||
if (inheritedRegistry != null) {
|
||||
return inheritedRegistry;
|
||||
}
|
||||
|
||||
// Handles deprecated API.
|
||||
return _internalRadioRegistry ??= _RadioRegistry<T>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_internalFocusNode?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(
|
||||
!(widget.enabled ?? false) ||
|
||||
widget.onChanged != null ||
|
||||
widget.groupRegistry != null ||
|
||||
RadioGroup.maybeOf<T>(context) != null,
|
||||
'Radio is enabled but has no Radio.onChange or registry above',
|
||||
);
|
||||
assert(debugCheckHasMaterial(context));
|
||||
switch (_radioType) {
|
||||
switch (widget._radioType) {
|
||||
case _RadioType.material:
|
||||
break;
|
||||
|
||||
@ -348,16 +458,18 @@ class Radio<T> extends StatelessWidget {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return CupertinoRadio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
mouseCursor: mouseCursor,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
focusColor: focusColor,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
useCheckmarkStyle: useCupertinoCheckmarkStyle,
|
||||
value: widget.value,
|
||||
groupValue: widget.groupValue,
|
||||
onChanged: widget.onChanged,
|
||||
mouseCursor: widget.mouseCursor,
|
||||
toggleable: widget.toggleable,
|
||||
activeColor: widget.activeColor,
|
||||
focusColor: widget.focusColor,
|
||||
focusNode: _focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
useCheckmarkStyle: widget.useCupertinoCheckmarkStyle,
|
||||
groupRegistry: _effectiveRegistry,
|
||||
enabled: _enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -365,39 +477,56 @@ class Radio<T> extends StatelessWidget {
|
||||
final RadioThemeData radioTheme = RadioTheme.of(context);
|
||||
final MaterialStateProperty<MouseCursor> effectiveMouseCursor =
|
||||
MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
|
||||
return MaterialStateProperty.resolveAs<MouseCursor?>(mouseCursor, states) ??
|
||||
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ??
|
||||
radioTheme.mouseCursor?.resolve(states) ??
|
||||
MaterialStateProperty.resolveAs<MouseCursor>(
|
||||
MaterialStateMouseCursor.clickable,
|
||||
states,
|
||||
);
|
||||
});
|
||||
|
||||
return RawRadio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
value: widget.value,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
toggleable: toggleable,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
toggleable: widget.toggleable,
|
||||
focusNode: _focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
groupRegistry: _effectiveRegistry,
|
||||
enabled: _enabled,
|
||||
builder: (BuildContext context, ToggleableStateMixin state) {
|
||||
return _RadioPaint(
|
||||
toggleableState: state,
|
||||
activeColor: activeColor,
|
||||
fillColor: fillColor,
|
||||
hoverColor: hoverColor,
|
||||
focusColor: focusColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
visualDensity: visualDensity,
|
||||
materialTapTargetSize: materialTapTargetSize,
|
||||
activeColor: widget.activeColor,
|
||||
fillColor: widget.fillColor,
|
||||
hoverColor: widget.hoverColor,
|
||||
focusColor: widget.focusColor,
|
||||
overlayColor: widget.overlayColor,
|
||||
splashRadius: widget.splashRadius,
|
||||
visualDensity: widget.visualDensity,
|
||||
materialTapTargetSize: widget.materialTapTargetSize,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A registry for deprecated API.
|
||||
// TODO(chunhtai): Remove this once deprecated API is removed.
|
||||
class _RadioRegistry<T> extends RadioGroupRegistry<T> {
|
||||
_RadioRegistry(this.state);
|
||||
final _RadioState<T> state;
|
||||
@override
|
||||
T? get groupValue => state.widget.groupValue;
|
||||
|
||||
@override
|
||||
ValueChanged<T?> get onChanged => state.widget.onChanged!;
|
||||
|
||||
@override
|
||||
void registerClient(RadioClient<T> radio) {}
|
||||
|
||||
@override
|
||||
void unregisterClient(RadioClient<T> radio) {}
|
||||
}
|
||||
|
||||
class _RadioPaint extends StatefulWidget {
|
||||
const _RadioPaint({
|
||||
required this.toggleableState,
|
||||
|
@ -37,10 +37,9 @@ enum _RadioType { material, adaptive }
|
||||
/// The entire list tile is interactive: tapping anywhere in the tile selects
|
||||
/// the radio button.
|
||||
///
|
||||
/// The [value], [groupValue], [onChanged], and [activeColor] properties of this
|
||||
/// widget are identical to the similarly-named properties on the [Radio]
|
||||
/// widget. The type parameter `T` serves the same purpose as that of the
|
||||
/// [Radio] class' type parameter.
|
||||
/// This widget typically has a [RadioGroup] ancestor, which takes in a
|
||||
/// [RadioGroup.groupValue], and the [RadioListTile] under it with matching
|
||||
/// [value] will be selected.
|
||||
///
|
||||
/// The [title], [subtitle], [isThreeLine], and [dense] properties are like
|
||||
/// those of the same name on [ListTile].
|
||||
@ -67,15 +66,13 @@ enum _RadioType { material, adaptive }
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// ```dart
|
||||
/// ColoredBox(
|
||||
/// const ColoredBox(
|
||||
/// color: Colors.green,
|
||||
/// child: Material(
|
||||
/// child: RadioListTile<Meridiem>(
|
||||
/// tileColor: Colors.red,
|
||||
/// title: const Text('AM'),
|
||||
/// groupValue: Meridiem.am,
|
||||
/// title: Text('AM'),
|
||||
/// value: Meridiem.am,
|
||||
/// onChanged:(Meridiem? value) { },
|
||||
/// ),
|
||||
/// ),
|
||||
/// )
|
||||
@ -88,9 +85,6 @@ enum _RadioType { material, adaptive }
|
||||
/// is expensive. Consider only wrapping the [RadioListTile]s that require it
|
||||
/// or include a common [Material] ancestor where possible.
|
||||
///
|
||||
/// To show the [RadioListTile] as disabled, pass null as the [onChanged]
|
||||
/// callback.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// 
|
||||
///
|
||||
@ -157,25 +151,27 @@ enum _RadioType { material, adaptive }
|
||||
/// * [CheckboxListTile], a similar widget for checkboxes.
|
||||
/// * [SwitchListTile], a similar widget for switches.
|
||||
/// * [ListTile] and [Radio], the widgets from which this widget is made.
|
||||
class RadioListTile<T> extends StatelessWidget {
|
||||
class RadioListTile<T> extends StatefulWidget {
|
||||
/// Creates a combination of a list tile and a radio button.
|
||||
///
|
||||
/// The radio tile itself does not maintain any state. Instead, when the radio
|
||||
/// button is selected, the widget calls the [onChanged] callback. Most
|
||||
/// widgets that use a radio button will listen for the [onChanged] callback
|
||||
/// and rebuild the radio tile with a new [groupValue] to update the visual
|
||||
/// appearance of the radio button.
|
||||
/// This widget typically has a [RadioGroup] ancestor, which takes in a
|
||||
/// [RadioGroup.groupValue], and the [RadioListTile] under it with matching
|
||||
/// [value] will be selected.
|
||||
///
|
||||
/// The following arguments are required:
|
||||
///
|
||||
/// * [value] and [groupValue] together determine whether the radio button is
|
||||
/// selected.
|
||||
/// * [onChanged] is called when the user selects this radio button.
|
||||
/// [value] must be provided
|
||||
const RadioListTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.groupValue,
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.onChanged,
|
||||
this.mouseCursor,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
@ -202,6 +198,7 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
this.enableFeedback,
|
||||
this.radioScaleFactor = 1.0,
|
||||
this.titleAlignment,
|
||||
this.enabled,
|
||||
this.internalAddSemanticForOnTap = false,
|
||||
}) : _radioType = _RadioType.material,
|
||||
useCupertinoCheckmarkStyle = false,
|
||||
@ -216,8 +213,16 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
const RadioListTile.adaptive({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.groupValue,
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
this.onChanged,
|
||||
this.mouseCursor,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
@ -243,6 +248,7 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
this.onFocusChange,
|
||||
this.enableFeedback,
|
||||
this.radioScaleFactor = 1.0,
|
||||
this.enabled,
|
||||
this.useCupertinoCheckmarkStyle = false,
|
||||
this.titleAlignment,
|
||||
this.internalAddSemanticForOnTap = false,
|
||||
@ -256,6 +262,12 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
///
|
||||
/// This radio button is considered selected if its [value] matches the
|
||||
/// [groupValue].
|
||||
///
|
||||
/// leave this unassigned or null if building this widget under [RadioGroup].
|
||||
@Deprecated(
|
||||
'Use a RadioGroup ancestor to manage group value instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
final T? groupValue;
|
||||
|
||||
/// Called when the user selects this radio button.
|
||||
@ -285,6 +297,10 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
@Deprecated(
|
||||
'Use RadioGroup to handle value change instead. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// The cursor for a mouse pointer when it enters or is hovering over the
|
||||
@ -307,14 +323,14 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
/// To indicate returning to an indeterminate state, [onChanged] will be
|
||||
/// called with null.
|
||||
///
|
||||
/// If true, [onChanged] is called with [value] when selected while
|
||||
/// [groupValue] != [value], and with null when selected again while
|
||||
/// [groupValue] == [value].
|
||||
/// If true, [RadioGroup.onChanged] is called with [value] when selected while
|
||||
/// [RadioGroup.groupValue] != [value], and with null when selected again while
|
||||
/// [RadioGroup.groupValue] == [value].
|
||||
///
|
||||
/// If false, [onChanged] will be called with [value] when it is selected
|
||||
/// while [groupValue] != [value], and only by selecting another radio button
|
||||
/// in the group (i.e. changing the value of [groupValue]) can this radio
|
||||
/// list tile be unselected.
|
||||
/// If false, [RadioGroup.onChanged] will be called with [value] when it is
|
||||
/// selected while [groupValue] != [value], and only by selecting another
|
||||
/// radio button in the group (i.e. changing the value of
|
||||
/// [RadioGroup.groupValue]) can this radio list tile be unselected.
|
||||
///
|
||||
/// The default is false.
|
||||
///
|
||||
@ -402,7 +418,7 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
/// No effort is made to automatically coordinate the [selected] state and the
|
||||
/// [checked] state. To have the list tile appear selected when the radio
|
||||
/// button is the selected radio button, set [selected] to true when [value]
|
||||
/// matches [groupValue].
|
||||
/// matches [RadioGroup.groupValue].
|
||||
///
|
||||
/// Normally, this property is left to its default value, false.
|
||||
final bool selected;
|
||||
@ -421,11 +437,6 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
/// When null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
/// Whether this radio button is checked.
|
||||
///
|
||||
/// To control this value, set [value] and [groupValue] appropriately.
|
||||
bool get checked => value == groupValue;
|
||||
|
||||
/// If specified, [shape] defines the shape of the [RadioListTile]'s [InkWell] border.
|
||||
final ShapeBorder? shape;
|
||||
|
||||
@ -492,100 +503,203 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
/// Defaults to 1.0.
|
||||
final double radioScaleFactor;
|
||||
|
||||
/// Whether this widget is interactable.
|
||||
///
|
||||
/// If not provided, this widget will be interactable if one of the following
|
||||
/// is true:
|
||||
///
|
||||
/// * A [onChanged] is provided.
|
||||
/// * Having a [RadioGroup] with the same type T above this widget.
|
||||
///
|
||||
/// If this is set to true, one of the above condition must also be true.
|
||||
/// Otherwise, an assertion error is thrown.
|
||||
final bool? enabled;
|
||||
|
||||
/// Whether this radio button is checked.
|
||||
///
|
||||
/// To control this value, set [value] and [groupValue] appropriately.
|
||||
@Deprecated(
|
||||
'Use RadioGroup.groupValue to find which radio is checked. '
|
||||
'This feature was deprecated after v3.32.0-0.0.pre.',
|
||||
)
|
||||
bool get checked => value == groupValue;
|
||||
|
||||
@override
|
||||
State<RadioListTile<T>> createState() => _RadioListTileState<T>();
|
||||
}
|
||||
|
||||
class _RadioListTileState<T> extends State<RadioListTile<T>> with RadioClient<T> {
|
||||
FocusNode? _internalFocusNode;
|
||||
@override
|
||||
FocusNode get focusNode => widget.focusNode ?? (_internalFocusNode ??= FocusNode());
|
||||
|
||||
@override
|
||||
T get radioValue => widget.value;
|
||||
|
||||
@override
|
||||
bool get tristate => widget.toggleable;
|
||||
|
||||
bool get checked => radioValue == effectiveGroupValue;
|
||||
|
||||
late final _RadioRegistry<T> _radioRegistry = _RadioRegistry<T>(this);
|
||||
|
||||
T? get effectiveGroupValue => registry?.groupValue ?? widget.groupValue;
|
||||
|
||||
bool get _enabled => widget.enabled ?? (widget.onChanged != null || registry != null);
|
||||
|
||||
void _handleListTileTap() {
|
||||
if (!widget.toggleable && checked) {
|
||||
return;
|
||||
}
|
||||
T? newValue;
|
||||
if (checked) {
|
||||
newValue = null;
|
||||
} else {
|
||||
newValue = radioValue;
|
||||
}
|
||||
handleChange(newValue);
|
||||
}
|
||||
|
||||
void handleChange(T? value) {
|
||||
if (registry != null) {
|
||||
registry!.onChanged(value);
|
||||
}
|
||||
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
registry = RadioGroup.maybeOf(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
registry = null;
|
||||
_internalFocusNode?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(
|
||||
!(widget.enabled ?? false) ||
|
||||
widget.onChanged != null ||
|
||||
RadioGroup.maybeOf<T>(context) != null,
|
||||
'Radio is enabled but has no RadioListTile.onChange or registry above',
|
||||
);
|
||||
Widget control;
|
||||
switch (_radioType) {
|
||||
switch (widget._radioType) {
|
||||
case _RadioType.material:
|
||||
control = ExcludeFocus(
|
||||
child: Radio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
fillColor: fillColor,
|
||||
mouseCursor: mouseCursor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
value: radioValue,
|
||||
groupValue: _radioRegistry.groupValue,
|
||||
toggleable: widget.toggleable,
|
||||
activeColor: widget.activeColor,
|
||||
materialTapTargetSize: widget.materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: widget.autofocus,
|
||||
fillColor: widget.fillColor,
|
||||
mouseCursor: widget.mouseCursor,
|
||||
hoverColor: widget.hoverColor,
|
||||
overlayColor: widget.overlayColor,
|
||||
splashRadius: widget.splashRadius,
|
||||
enabled: _enabled,
|
||||
groupRegistry: _radioRegistry,
|
||||
),
|
||||
);
|
||||
case _RadioType.adaptive:
|
||||
control = ExcludeFocus(
|
||||
child: Radio<T>.adaptive(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
fillColor: fillColor,
|
||||
mouseCursor: mouseCursor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
|
||||
value: radioValue,
|
||||
groupValue: _radioRegistry.groupValue,
|
||||
toggleable: widget.toggleable,
|
||||
activeColor: widget.activeColor,
|
||||
materialTapTargetSize: widget.materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: widget.autofocus,
|
||||
fillColor: widget.fillColor,
|
||||
mouseCursor: widget.mouseCursor,
|
||||
hoverColor: widget.hoverColor,
|
||||
overlayColor: widget.overlayColor,
|
||||
splashRadius: widget.splashRadius,
|
||||
useCupertinoCheckmarkStyle: widget.useCupertinoCheckmarkStyle,
|
||||
enabled: _enabled,
|
||||
groupRegistry: _radioRegistry,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (radioScaleFactor != 1.0) {
|
||||
control = Transform.scale(scale: radioScaleFactor, child: control);
|
||||
if (widget.radioScaleFactor != 1.0) {
|
||||
control = Transform.scale(scale: widget.radioScaleFactor, child: control);
|
||||
}
|
||||
|
||||
final ListTileThemeData listTileTheme = ListTileTheme.of(context);
|
||||
final ListTileControlAffinity effectiveControlAffinity =
|
||||
controlAffinity ?? listTileTheme.controlAffinity ?? ListTileControlAffinity.platform;
|
||||
widget.controlAffinity ?? listTileTheme.controlAffinity ?? ListTileControlAffinity.platform;
|
||||
Widget? leading, trailing;
|
||||
(leading, trailing) = switch (effectiveControlAffinity) {
|
||||
ListTileControlAffinity.leading || ListTileControlAffinity.platform => (control, secondary),
|
||||
ListTileControlAffinity.trailing => (secondary, control),
|
||||
ListTileControlAffinity.leading ||
|
||||
ListTileControlAffinity.platform => (control, widget.secondary),
|
||||
ListTileControlAffinity.trailing => (widget.secondary, control),
|
||||
};
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final RadioThemeData radioThemeData = RadioTheme.of(context);
|
||||
final Set<MaterialState> states = <MaterialState>{if (selected) MaterialState.selected};
|
||||
final Set<MaterialState> states = <MaterialState>{if (widget.selected) MaterialState.selected};
|
||||
final Color effectiveActiveColor =
|
||||
activeColor ?? radioThemeData.fillColor?.resolve(states) ?? theme.colorScheme.secondary;
|
||||
widget.activeColor ??
|
||||
radioThemeData.fillColor?.resolve(states) ??
|
||||
theme.colorScheme.secondary;
|
||||
return MergeSemantics(
|
||||
child: ListTile(
|
||||
selectedColor: effectiveActiveColor,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
title: widget.title,
|
||||
subtitle: widget.subtitle,
|
||||
trailing: trailing,
|
||||
isThreeLine: isThreeLine,
|
||||
dense: dense,
|
||||
enabled: onChanged != null,
|
||||
shape: shape,
|
||||
tileColor: tileColor,
|
||||
selectedTileColor: selectedTileColor,
|
||||
onTap:
|
||||
onChanged != null
|
||||
? () {
|
||||
if (toggleable && checked) {
|
||||
onChanged!(null);
|
||||
return;
|
||||
}
|
||||
if (!checked) {
|
||||
onChanged!(value);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
selected: selected,
|
||||
autofocus: autofocus,
|
||||
contentPadding: contentPadding,
|
||||
visualDensity: visualDensity,
|
||||
isThreeLine: widget.isThreeLine,
|
||||
dense: widget.dense,
|
||||
enabled: _enabled,
|
||||
shape: widget.shape,
|
||||
tileColor: widget.tileColor,
|
||||
selectedTileColor: widget.selectedTileColor,
|
||||
onTap: _enabled ? _handleListTileTap : null,
|
||||
selected: widget.selected,
|
||||
autofocus: widget.autofocus,
|
||||
contentPadding: widget.contentPadding,
|
||||
visualDensity: widget.visualDensity,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
enableFeedback: enableFeedback,
|
||||
titleAlignment: titleAlignment,
|
||||
internalAddSemanticForOnTap: internalAddSemanticForOnTap,
|
||||
onFocusChange: widget.onFocusChange,
|
||||
enableFeedback: widget.enableFeedback,
|
||||
titleAlignment: widget.titleAlignment,
|
||||
internalAddSemanticForOnTap: widget.internalAddSemanticForOnTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A registry to controls internal [Radio] and hides it from [RadioGroup]
|
||||
/// ancestor.
|
||||
///
|
||||
/// [RadioListTile] implements the [RadioClient] directly to register to
|
||||
/// [RadioGroup] ancestor. Therefore, it has to hide the internal [Radio] from
|
||||
/// participate in the [RadioGroup] ancestor.
|
||||
class _RadioRegistry<T> extends RadioGroupRegistry<T> {
|
||||
_RadioRegistry(this.state);
|
||||
|
||||
final _RadioListTileState<T> state;
|
||||
|
||||
@override
|
||||
T? get groupValue => state.effectiveGroupValue;
|
||||
|
||||
@override
|
||||
ValueChanged<T?> get onChanged => state.handleChange;
|
||||
|
||||
@override
|
||||
void registerClient(RadioClient<T> radio) {}
|
||||
|
||||
@override
|
||||
void unregisterClient(RadioClient<T> radio) {}
|
||||
}
|
||||
|
@ -1570,10 +1570,41 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy
|
||||
/// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback}
|
||||
ReadingOrderTraversalPolicy({super.requestFocusCallback});
|
||||
|
||||
/// Sorts the input focus nodes into reading order.
|
||||
static Iterable<FocusNode> sort(Iterable<FocusNode> nodes) {
|
||||
if (nodes.length <= 1) {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
final List<_ReadingOrderSortData> data = <_ReadingOrderSortData>[
|
||||
for (final FocusNode node in nodes) _ReadingOrderSortData(node),
|
||||
];
|
||||
|
||||
final List<FocusNode> sortedList = <FocusNode>[];
|
||||
final List<_ReadingOrderSortData> unplaced = data;
|
||||
|
||||
// Pick the initial widget as the one that is at the beginning of the band
|
||||
// of the topmost, or the topmost, if there are no others in its band.
|
||||
_ReadingOrderSortData current = _pickNext(unplaced);
|
||||
sortedList.add(current.node);
|
||||
unplaced.remove(current);
|
||||
|
||||
// Go through each node, picking the next one after eliminating the previous
|
||||
// one, since removing the previously picked node will expose a new band in
|
||||
// which to choose candidates.
|
||||
while (unplaced.isNotEmpty) {
|
||||
final _ReadingOrderSortData next = _pickNext(unplaced);
|
||||
current = next;
|
||||
sortedList.add(current.node);
|
||||
unplaced.remove(current);
|
||||
}
|
||||
return sortedList;
|
||||
}
|
||||
|
||||
// Collects the given candidates into groups by directionality. The candidates
|
||||
// have already been sorted as if they all had the directionality of the
|
||||
// nearest Directionality ancestor.
|
||||
List<_ReadingOrderDirectionalGroupData> _collectDirectionalityGroups(
|
||||
static List<_ReadingOrderDirectionalGroupData> _collectDirectionalityGroups(
|
||||
Iterable<_ReadingOrderSortData> candidates,
|
||||
) {
|
||||
TextDirection? currentDirection = candidates.first.directionality;
|
||||
@ -1602,7 +1633,7 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy
|
||||
return result;
|
||||
}
|
||||
|
||||
_ReadingOrderSortData _pickNext(List<_ReadingOrderSortData> candidates) {
|
||||
static _ReadingOrderSortData _pickNext(List<_ReadingOrderSortData> candidates) {
|
||||
// Find the topmost node by sorting on the top of the rectangles.
|
||||
mergeSort<_ReadingOrderSortData>(
|
||||
candidates,
|
||||
@ -1674,35 +1705,8 @@ class ReadingOrderTraversalPolicy extends FocusTraversalPolicy
|
||||
// Sorts the list of nodes based on their geometry into the desired reading
|
||||
// order based on the directionality of the context for each node.
|
||||
@override
|
||||
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) {
|
||||
if (descendants.length <= 1) {
|
||||
return descendants;
|
||||
}
|
||||
|
||||
final List<_ReadingOrderSortData> data = <_ReadingOrderSortData>[
|
||||
for (final FocusNode node in descendants) _ReadingOrderSortData(node),
|
||||
];
|
||||
|
||||
final List<FocusNode> sortedList = <FocusNode>[];
|
||||
final List<_ReadingOrderSortData> unplaced = data;
|
||||
|
||||
// Pick the initial widget as the one that is at the beginning of the band
|
||||
// of the topmost, or the topmost, if there are no others in its band.
|
||||
_ReadingOrderSortData current = _pickNext(unplaced);
|
||||
sortedList.add(current.node);
|
||||
unplaced.remove(current);
|
||||
|
||||
// Go through each node, picking the next one after eliminating the previous
|
||||
// one, since removing the previously picked node will expose a new band in
|
||||
// which to choose candidates.
|
||||
while (unplaced.isNotEmpty) {
|
||||
final _ReadingOrderSortData next = _pickNext(unplaced);
|
||||
current = next;
|
||||
sortedList.add(current.node);
|
||||
unplaced.remove(current);
|
||||
}
|
||||
return sortedList;
|
||||
}
|
||||
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) =>
|
||||
sort(descendants);
|
||||
}
|
||||
|
||||
/// Base class for all sort orders for [OrderedTraversalPolicy] traversal.
|
||||
|
327
packages/flutter/lib/src/widgets/radio_group.dart
Normal file
327
packages/flutter/lib/src/widgets/radio_group.dart
Normal file
@ -0,0 +1,327 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'focus_traversal.dart';
|
||||
import 'framework.dart';
|
||||
import 'shortcuts.dart';
|
||||
|
||||
/// A group for radios.
|
||||
///
|
||||
/// This widget treats all radios, such as [RawRadio], [Radio], [CupertinoRadio]
|
||||
/// in the sub tree with the same type T as a group. Radios with different types
|
||||
/// are not included in the group.
|
||||
///
|
||||
/// This widget handles the group value for the radios in the subtree with the
|
||||
/// same value type.
|
||||
///
|
||||
/// Using this widget also provides keyboard navigation and semantics for the
|
||||
/// radio buttons that matches [APG](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).
|
||||
///
|
||||
/// The keyboard behaviors are:
|
||||
/// * Tab and Shift+Tab: moves focus into and out of radio group. When focus
|
||||
/// moves into a radio group and a radio button is select, focus is set on
|
||||
/// selected button. Otherwise, it focus the first radio button in reading
|
||||
/// order.
|
||||
/// * Space: toggle the selection on the focused radio button.
|
||||
/// * Right and down arrow key: move selection to next radio button in the group
|
||||
/// in reading order.
|
||||
/// * Left and up arrow key: move selection to previous radio button in the
|
||||
/// group in reading order.
|
||||
///
|
||||
/// Arrow keys will wrap around if it reach the first or last radio in the
|
||||
/// group.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Here is an example of RadioGroup widget.
|
||||
///
|
||||
/// Try using tab, arrow keys, and space to see how the widget responds.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/radio_group/radio_group.0.dart **
|
||||
/// {@end-tool}
|
||||
class RadioGroup<T> extends StatefulWidget {
|
||||
/// Creates a radio group.
|
||||
///
|
||||
/// The `groupValue` set the selection on a subtree radio with the same
|
||||
/// [RawRadio.value].
|
||||
///
|
||||
/// The `onChanged` is called when the selection has changed in the subtree
|
||||
/// radios.
|
||||
const RadioGroup({super.key, this.groupValue, required this.onChanged, required this.child});
|
||||
|
||||
/// The selected value under this radio group.
|
||||
///
|
||||
/// [RawRadio] under this radio group where its [RawRadio.value] equals to this
|
||||
/// value will be selected.
|
||||
final T? groupValue;
|
||||
|
||||
/// Called when selection has changed.
|
||||
///
|
||||
/// The value can be null when unselect the [RawRadio] with
|
||||
/// [RawRadio.toggleable] set to true.
|
||||
final ValueChanged<T?> onChanged;
|
||||
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget child;
|
||||
|
||||
/// Gets the [RadioGroupRegistry] from the above the context.
|
||||
///
|
||||
/// This registers a dependencies on the context that it causes rebuild
|
||||
/// if [RadioGroupRegistry] has changed or its
|
||||
/// [RadioGroupRegistry.groupValue] has changed.
|
||||
static RadioGroupRegistry<T>? maybeOf<T>(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<_RadioGroupStateScope<T>>()?.state;
|
||||
}
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _RadioGroupState<T>();
|
||||
}
|
||||
|
||||
class _RadioGroupState<T> extends State<RadioGroup<T>> implements RadioGroupRegistry<T> {
|
||||
late final Map<ShortcutActivator, Intent> _radioGroupShortcuts = <ShortcutActivator, Intent>{
|
||||
const SingleActivator(LogicalKeyboardKey.arrowLeft): VoidCallbackIntent(_selectPreviousRadio),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowRight): VoidCallbackIntent(_selectNextRadio),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): VoidCallbackIntent(_selectNextRadio),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp): VoidCallbackIntent(_selectPreviousRadio),
|
||||
const SingleActivator(LogicalKeyboardKey.space): VoidCallbackIntent(_toggleFocusedRadio),
|
||||
};
|
||||
|
||||
final Set<RadioClient<T>> _radios = <RadioClient<T>>{};
|
||||
|
||||
bool _debugCheckOnlySingleSelection() {
|
||||
return _radios.where((RadioClient<T> radio) => radio.radioValue == groupValue).length < 2;
|
||||
}
|
||||
|
||||
@override
|
||||
T? get groupValue => widget.groupValue;
|
||||
|
||||
@override
|
||||
void registerClient(RadioClient<T> radio) {
|
||||
_radios.add(radio);
|
||||
assert(
|
||||
_debugCheckOnlySingleSelection(),
|
||||
"RadioGroupPolicy can't be used for a radio group that allows multiple selection",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void unregisterClient(RadioClient<T> radio) => _radios.remove(radio);
|
||||
|
||||
void _toggleFocusedRadio() {
|
||||
final RadioClient<T>? radio = _radios.firstWhereOrNull(
|
||||
(RadioClient<T> radio) => radio.focusNode.hasFocus,
|
||||
);
|
||||
if (radio == null) {
|
||||
return;
|
||||
}
|
||||
if (radio.radioValue != widget.groupValue) {
|
||||
onChanged(radio.radioValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (radio.tristate) {
|
||||
onChanged(null);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ValueChanged<T?> get onChanged => widget.onChanged;
|
||||
|
||||
void _selectNextRadio() => _selectRadioInDirection(true);
|
||||
|
||||
void _selectPreviousRadio() => _selectRadioInDirection(false);
|
||||
|
||||
void _selectRadioInDirection(bool forward) {
|
||||
if (_radios.length < 2) {
|
||||
return;
|
||||
}
|
||||
final FocusNode? currentFocus =
|
||||
_radios.firstWhereOrNull((RadioClient<T> radio) => radio.focusNode.hasFocus)?.focusNode;
|
||||
if (currentFocus == null) {
|
||||
// The focused node is either a non interactive radio or other controls.
|
||||
return;
|
||||
}
|
||||
final List<FocusNode> sorted =
|
||||
ReadingOrderTraversalPolicy.sort(
|
||||
_radios.map<FocusNode>((RadioClient<T> radio) => radio.focusNode),
|
||||
).toList();
|
||||
final Iterable<FocusNode> nodesInEffectiveOrder = forward ? sorted : sorted.reversed;
|
||||
|
||||
final Iterator<FocusNode> iterator = nodesInEffectiveOrder.iterator;
|
||||
FocusNode? nextFocus;
|
||||
while (iterator.moveNext()) {
|
||||
if (iterator.current == currentFocus) {
|
||||
if (iterator.moveNext()) {
|
||||
nextFocus = iterator.current;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Current focus is at the end, the next focus should wrap around.
|
||||
nextFocus ??= nodesInEffectiveOrder.first;
|
||||
final RadioClient<T> radioToSelect = _radios.firstWhere(
|
||||
(RadioClient<T> radio) => radio.focusNode == nextFocus,
|
||||
);
|
||||
onChanged(radioToSelect.radioValue);
|
||||
nextFocus.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
role: SemanticsRole.radioGroup,
|
||||
child: Shortcuts(
|
||||
shortcuts: _radioGroupShortcuts,
|
||||
child: FocusTraversalGroup(
|
||||
policy: _SkipUnselectedRadioPolicy<T>(_radios, widget.groupValue),
|
||||
child: _RadioGroupStateScope<T>(
|
||||
state: this,
|
||||
groupValue: widget.groupValue,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioGroupStateScope<T> extends InheritedWidget {
|
||||
const _RadioGroupStateScope({required this.state, required this.groupValue, required super.child})
|
||||
: super();
|
||||
final _RadioGroupState<T> state;
|
||||
// Need to include group value to notify listener when group value changes.
|
||||
final T? groupValue;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant _RadioGroupStateScope<T> oldWidget) {
|
||||
return state != oldWidget.state || groupValue != oldWidget.groupValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract interface for registering a group of radios.
|
||||
///
|
||||
/// Use [registerClient] or [unregisterClient] to handle registrations of radios.
|
||||
///
|
||||
/// The registry manages the group value for the radios. The radio needs to call
|
||||
/// [onChanged] to notify the group value needs to be changed.
|
||||
abstract class RadioGroupRegistry<T> {
|
||||
/// The group value for the group.
|
||||
T? get groupValue;
|
||||
|
||||
/// Registers a radio client.
|
||||
///
|
||||
/// The subclass provides additional features, such as keyboard navigation
|
||||
/// for the registered clients.
|
||||
void registerClient(RadioClient<T> radio);
|
||||
|
||||
/// Unregisters a radio client.
|
||||
void unregisterClient(RadioClient<T> radio);
|
||||
|
||||
/// Notifies the registry that the a radio is selected or unselected.
|
||||
ValueChanged<T?> get onChanged;
|
||||
}
|
||||
|
||||
/// A client for a [RadioGroupRegistry].
|
||||
///
|
||||
/// This is typically mixed with a [State].
|
||||
///
|
||||
/// To register to a [RadioGroupRegistry], assign the registry to [registry].
|
||||
///
|
||||
/// To unregister from previous [RadioGroupRegistry], either assign a different
|
||||
/// value to [registry] or set it to null.
|
||||
mixin RadioClient<T> {
|
||||
/// Whether this radio support toggles.
|
||||
///
|
||||
/// Used by registry to provide additional feature such as keyboard support.
|
||||
bool get tristate;
|
||||
|
||||
/// This value this radio represents.
|
||||
///
|
||||
/// Used by registry to provide additional feature such as keyboard support.
|
||||
T get radioValue;
|
||||
|
||||
/// Focus node for this radio.
|
||||
///
|
||||
/// Used by registry to provide additional feature such as keyboard support.
|
||||
FocusNode get focusNode;
|
||||
|
||||
/// The [RadioGroupRegistry] this client register to.
|
||||
///
|
||||
/// Setting this property automatically register to the new value and
|
||||
/// unregister the old value.
|
||||
///
|
||||
/// This should set to null when dispose.
|
||||
RadioGroupRegistry<T>? get registry => _registry;
|
||||
RadioGroupRegistry<T>? _registry;
|
||||
set registry(RadioGroupRegistry<T>? newRegistry) {
|
||||
if (_registry != newRegistry) {
|
||||
_registry?.unregisterClient(this);
|
||||
}
|
||||
_registry = newRegistry;
|
||||
_registry?.registerClient(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// A traversal policy that is the same as [ReadingOrderTraversalPolicy] except
|
||||
/// it skips nodes of unselected radio button if there is one selected radio
|
||||
/// button.
|
||||
///
|
||||
/// If none of the radio is selected, this defaults to
|
||||
/// [ReadingOrderTraversalPolicy] for all nodes.
|
||||
///
|
||||
/// This policy is to ensure when tabbing into a radio group, it will only focus
|
||||
/// the current selected radio button and prevent focus from reaching unselected
|
||||
/// ones.
|
||||
class _SkipUnselectedRadioPolicy<T> extends ReadingOrderTraversalPolicy {
|
||||
_SkipUnselectedRadioPolicy(this.radios, this.groupValue);
|
||||
final Set<RadioClient<T>> radios;
|
||||
final T? groupValue;
|
||||
|
||||
bool _radioSelected(RadioClient<T> radio) => radio.radioValue == groupValue;
|
||||
|
||||
@override
|
||||
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) {
|
||||
final Iterable<FocusNode> nodesInReadOrder = super.sortDescendants(descendants, currentNode);
|
||||
RadioClient<T>? selected = radios.firstWhereOrNull(_radioSelected);
|
||||
|
||||
if (selected == null) {
|
||||
// None of the radio are selected. Select the first radio in read order.
|
||||
final Map<FocusNode, RadioClient<T>> radioFocusNodes = <FocusNode, RadioClient<T>>{};
|
||||
for (final RadioClient<T> radio in radios) {
|
||||
radioFocusNodes[radio.focusNode] = radio;
|
||||
}
|
||||
|
||||
for (final FocusNode node in nodesInReadOrder) {
|
||||
selected = radioFocusNodes[node];
|
||||
if (selected != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selected == null) {
|
||||
// None of the radio is selected or focusable, defaults to reading order
|
||||
return nodesInReadOrder;
|
||||
}
|
||||
|
||||
// Nodes that are not selected AND not currently focused, since we can't
|
||||
// remove the focused node from the sorted result.
|
||||
final Set<FocusNode> nodeToSkip =
|
||||
radios
|
||||
.where((RadioClient<T> radio) => selected != radio && radio.focusNode != currentNode)
|
||||
.map<FocusNode>((RadioClient<T> radio) => radio.focusNode)
|
||||
.toSet();
|
||||
final Iterable<FocusNode> skipsNonSelected = descendants.where(
|
||||
(FocusNode node) => !nodeToSkip.contains(node),
|
||||
);
|
||||
return super.sortDescendants(skipsNonSelected, currentNode);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
import 'framework.dart';
|
||||
import 'radio_group.dart';
|
||||
import 'ticker_provider.dart';
|
||||
import 'toggleable.dart';
|
||||
import 'widget_state.dart';
|
||||
@ -30,11 +31,9 @@ typedef RadioBuilder = Widget Function(BuildContext context, ToggleableStateMixi
|
||||
/// group cease to be selected. The values are of type `T`, the type parameter
|
||||
/// of the radio class. Enums are commonly used for this purpose.
|
||||
///
|
||||
/// The radio button itself does not maintain any state. Instead, selecting the
|
||||
/// radio invokes the [onChanged] callback, passing [value] as a parameter. If
|
||||
/// [groupValue] and [value] match, this radio will be selected. Most widgets
|
||||
/// will respond to [onChanged] by calling [State.setState] to update the
|
||||
/// radio button's [groupValue].
|
||||
/// {@macro flutter.widget.RawRadio.groupValue}
|
||||
///
|
||||
/// If [enabled] is false, the radio will not be interactive.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -44,57 +43,24 @@ typedef RadioBuilder = Widget Function(BuildContext context, ToggleableStateMixi
|
||||
class RawRadio<T> extends StatefulWidget {
|
||||
/// Creates a radio button.
|
||||
///
|
||||
/// The radio button itself does not maintain any state. Instead, when the
|
||||
/// radio button is selected, the widget calls the [onChanged] callback. Most
|
||||
/// widgets that use a radio button will listen for the [onChanged] callback
|
||||
/// and rebuild the radio button with a new [groupValue] to update the visual
|
||||
/// appearance of the radio button.
|
||||
/// If [enabled] is true, the [groupRegistry] must not be null.
|
||||
const RawRadio({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
required this.mouseCursor,
|
||||
required this.toggleable,
|
||||
required this.focusNode,
|
||||
required this.autofocus,
|
||||
required this.groupRegistry,
|
||||
required this.enabled,
|
||||
required this.builder,
|
||||
});
|
||||
}) : assert(!enabled || groupRegistry != null, 'an enabled raw radio must have a registry');
|
||||
|
||||
/// {@template flutter.widget.RawRadio.value}
|
||||
/// The value represented by this radio button.
|
||||
/// {@endtemplate}
|
||||
final T value;
|
||||
|
||||
/// {@template flutter.widget.RawRadio.groupValue}
|
||||
/// The currently selected value for a group of radio buttons.
|
||||
///
|
||||
/// This radio button is considered selected if its [value] matches the
|
||||
/// [groupValue].
|
||||
/// {@endtemplate}
|
||||
final T? groupValue;
|
||||
|
||||
/// {@template flutter.widget.RawRadio.onChanged}
|
||||
/// Called when the user selects this radio button.
|
||||
///
|
||||
/// The radio button passes [value] as a parameter to this callback. The radio
|
||||
/// button does not actually change state until the parent widget rebuilds the
|
||||
/// radio button with the new [groupValue].
|
||||
///
|
||||
/// If null, the radio button will be displayed as disabled.
|
||||
///
|
||||
/// The provided callback will not be invoked if this radio button is already
|
||||
/// selected and [toggleable] is not set to true.
|
||||
///
|
||||
/// If the [toggleable] is set to true, tapping a already selected radio will
|
||||
/// invoke this callback with `null` as value.
|
||||
///
|
||||
/// The callback provided to [onChanged] should update the state of the parent
|
||||
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
||||
/// gets rebuilt.
|
||||
/// {@endtemplate}
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// {@template flutter.widget.RawRadio.mouseCursor}
|
||||
/// The cursor for a mouse pointer when it enters or is hovering over the
|
||||
/// widget.
|
||||
@ -113,24 +79,24 @@ class RawRadio<T> extends StatefulWidget {
|
||||
/// Set to true if this radio button is allowed to be returned to an
|
||||
/// indeterminate state by selecting it again when selected.
|
||||
///
|
||||
/// To indicate returning to an indeterminate state, [onChanged] will be
|
||||
/// called with null.
|
||||
/// To indicate returning to an indeterminate state, [RadioGroup.onChanged]
|
||||
/// of the [RadioGroup] above the widget tree will be called with null.
|
||||
///
|
||||
/// If true, [onChanged] is called with [value] when selected while
|
||||
/// [groupValue] != [value], and with null when selected again while
|
||||
/// [groupValue] == [value].
|
||||
/// If true, [RadioGroup.onChanged] is called with [value] when selected while
|
||||
/// [RadioGroup.groupValue] != [value], and with null when selected again while
|
||||
/// [RadioGroup.groupValue] == [value].
|
||||
///
|
||||
/// If false, [onChanged] will be called with [value] when it is selected
|
||||
/// while [groupValue] != [value], and only by selecting another radio button
|
||||
/// in the group (i.e. changing the value of [groupValue]) can this radio
|
||||
/// button be unselected.
|
||||
/// If false, [RadioGroup.onChanged] will be called with [value] when it is
|
||||
/// selected while [RadioGroup.groupValue] != [value], and only by selecting
|
||||
/// another radio button in the group (i.e. changing the value of
|
||||
/// [RadioGroup.groupValue]) can this radio button be unselected.
|
||||
///
|
||||
/// The default is false.
|
||||
/// {@endtemplate}
|
||||
final bool toggleable;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
@ -142,40 +108,86 @@ class RawRadio<T> extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.ToggleableStateMixin.buildToggleableWithChild}
|
||||
final RadioBuilder builder;
|
||||
|
||||
bool get _selected => value == groupValue;
|
||||
/// Whether this widget is enabled.
|
||||
final bool enabled;
|
||||
|
||||
/// {@template flutter.widget.RawRadio.groupRegistry}
|
||||
/// The registry this radio registers to.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@template flutter.widget.RawRadio.groupValue}
|
||||
/// The radio relies on [groupRegistry] to maintains the state for selection.
|
||||
/// If use in conjunction with a [RadioGroup] widget, use [RadioGroup.maybeOf]
|
||||
/// to get the group registry from the context.
|
||||
/// {@endtemplate}
|
||||
final RadioGroupRegistry<T>? groupRegistry;
|
||||
|
||||
@override
|
||||
State<RawRadio<T>> createState() => _RawRadioState<T>();
|
||||
}
|
||||
|
||||
class _RawRadioState<T> extends State<RawRadio<T>>
|
||||
with TickerProviderStateMixin, ToggleableStateMixin {
|
||||
with TickerProviderStateMixin, ToggleableStateMixin, RadioClient<T> {
|
||||
@override
|
||||
FocusNode get focusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
T get radioValue => widget.value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// This has to be before the init state because the [ToggleableStateMixin]
|
||||
// expect the [value] is up-to-date when init its state.
|
||||
registry = widget.groupRegistry;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
/// Handle selection status changed.
|
||||
///
|
||||
/// if `selected` is false, nothing happens.
|
||||
///
|
||||
/// if `selected` is true, select this radio. i.e. [Radio.onChanged] is called
|
||||
/// with [Radio.value]. This also updates the group value in [RadioGroup] if it
|
||||
/// is in use.
|
||||
///
|
||||
/// if `selected` is null, unselect this radio. Same as `selected` is true
|
||||
/// except group value is set to null.
|
||||
void _handleChanged(bool? selected) {
|
||||
if (selected == null) {
|
||||
widget.onChanged!(null);
|
||||
assert(registry != null);
|
||||
if (!(selected ?? true)) {
|
||||
return;
|
||||
}
|
||||
if (selected) {
|
||||
widget.onChanged!(widget.value);
|
||||
if (selected ?? false) {
|
||||
registry!.onChanged(widget.value);
|
||||
} else {
|
||||
registry!.onChanged(null);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(RawRadio<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget._selected != oldWidget._selected) {
|
||||
animateToValue();
|
||||
}
|
||||
registry = widget.groupRegistry;
|
||||
animateToValue(); // The registry's group value may have changed
|
||||
}
|
||||
|
||||
@override
|
||||
ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
registry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
ValueChanged<bool?>? get onChanged => registry != null ? _handleChanged : null;
|
||||
|
||||
@override
|
||||
bool get tristate => widget.toggleable;
|
||||
|
||||
@override
|
||||
bool? get value => widget._selected;
|
||||
bool? get value => widget.value == registry?.groupValue;
|
||||
|
||||
@override
|
||||
bool get isInteractive => widget.enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -188,15 +200,15 @@ class _RawRadioState<T> extends State<RawRadio<T>>
|
||||
accessibilitySelected = null;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
accessibilitySelected = widget._selected;
|
||||
accessibilitySelected = value;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
inMutuallyExclusiveGroup: true,
|
||||
checked: widget._selected,
|
||||
checked: value,
|
||||
selected: accessibilitySelected,
|
||||
child: buildToggleableWithChild(
|
||||
focusNode: widget.focusNode,
|
||||
focusNode: focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
mouseCursor: widget.mouseCursor,
|
||||
child: widget.builder(context, this),
|
||||
|
@ -104,6 +104,7 @@ export 'src/widgets/platform_view.dart';
|
||||
export 'src/widgets/pop_scope.dart';
|
||||
export 'src/widgets/preferred_size.dart';
|
||||
export 'src/widgets/primary_scroll_controller.dart';
|
||||
export 'src/widgets/radio_group.dart';
|
||||
export 'src/widgets/raw_keyboard_listener.dart';
|
||||
export 'src/widgets/raw_menu_anchor.dart';
|
||||
export 'src/widgets/raw_radio.dart';
|
||||
|
@ -53,17 +53,36 @@ void main() {
|
||||
|
||||
expect(log, isEmpty);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(home: Center(child: CupertinoRadio<int>(key: key, value: 1, groupValue: 2))),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Radio disabled', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
final List<int?> log = <int?>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(key: key, value: 1, groupValue: 2, onChanged: null),
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
enabled: false,
|
||||
onChanged: log.add,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, isEmpty);
|
||||
expect(log, equals(<int>[]));
|
||||
});
|
||||
|
||||
testWidgets('Radio can be toggled when toggleable is set', (WidgetTester tester) async {
|
||||
@ -111,13 +130,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: null,
|
||||
onChanged: log.add,
|
||||
toggleable: true,
|
||||
),
|
||||
child: CupertinoRadio<int>(key: key, value: 1, onChanged: log.add, toggleable: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -204,9 +217,7 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(child: CupertinoRadio<int>(value: 1, groupValue: 2, onChanged: null)),
|
||||
),
|
||||
const CupertinoApp(home: Center(child: CupertinoRadio<int>(value: 1, groupValue: 2))),
|
||||
);
|
||||
|
||||
expect(
|
||||
@ -233,9 +244,7 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(child: CupertinoRadio<int>(value: 2, groupValue: 2, onChanged: null)),
|
||||
),
|
||||
const CupertinoApp(home: Center(child: CupertinoRadio<int>(value: 2, groupValue: 2))),
|
||||
);
|
||||
|
||||
expect(
|
||||
@ -531,7 +540,7 @@ void main() {
|
||||
return CupertinoApp(
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
child: CupertinoRadio<int>(value: value, groupValue: groupValue, onChanged: null),
|
||||
child: CupertinoRadio<int>(value: value, groupValue: groupValue),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -558,7 +567,7 @@ void main() {
|
||||
theme: const CupertinoThemeData(brightness: Brightness.dark),
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
child: CupertinoRadio<int>(value: value, groupValue: groupValue, onChanged: null),
|
||||
child: CupertinoRadio<int>(value: value, groupValue: groupValue),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -892,12 +901,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 1,
|
||||
onChanged: null,
|
||||
mouseCursor: _RadioMouseCursor(),
|
||||
),
|
||||
child: CupertinoRadio<int>(value: 1, groupValue: 1, mouseCursor: _RadioMouseCursor()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -27,8 +27,7 @@ void main() {
|
||||
int? selectedValue;
|
||||
// Constructor parameters are required for [RadioListTile], but they are
|
||||
// irrelevant when searching with [find.byType].
|
||||
final Type radioListTileType =
|
||||
const RadioListTile<int>(value: 0, groupValue: 0, onChanged: null).runtimeType;
|
||||
final Type radioListTileType = const RadioListTile<int>(value: 0, groupValue: 0).runtimeType;
|
||||
|
||||
List<RadioListTile<int>> generatedRadioListTiles;
|
||||
List<RadioListTile<int>> findTiles() =>
|
||||
@ -126,7 +125,6 @@ void main() {
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: null,
|
||||
title: Text('Title', key: titleKey),
|
||||
),
|
||||
),
|
||||
@ -158,7 +156,7 @@ void main() {
|
||||
int? selectedValue;
|
||||
// Constructor parameters are required for [Radio], but they are irrelevant
|
||||
// when searching with [find.byType].
|
||||
final Type radioType = const Radio<int>(value: 0, groupValue: 0, onChanged: null).runtimeType;
|
||||
final Type radioType = const Radio<int>(value: 0, groupValue: 0).runtimeType;
|
||||
final List<dynamic> log = <dynamic>[];
|
||||
|
||||
Widget buildFrame() {
|
||||
@ -223,7 +221,7 @@ void main() {
|
||||
int? selectedValue;
|
||||
// Constructor parameters are required for [Radio], but they are irrelevant
|
||||
// when searching with [find.byType].
|
||||
final Type radioType = const Radio<int>(value: 0, groupValue: 0, onChanged: null).runtimeType;
|
||||
final Type radioType = const Radio<int>(value: 0, groupValue: 0).runtimeType;
|
||||
final List<dynamic> log = <dynamic>[];
|
||||
|
||||
Widget buildFrame() {
|
||||
@ -273,7 +271,7 @@ void main() {
|
||||
int? selectedValue;
|
||||
// Constructor parameters are required for [Radio], but they are irrelevant
|
||||
// when searching with [find.byType].
|
||||
final Type radioType = const Radio<int>(value: 0, groupValue: 0, onChanged: null).runtimeType;
|
||||
final Type radioType = const Radio<int>(value: 0, groupValue: 0).runtimeType;
|
||||
final List<dynamic> log = <dynamic>[];
|
||||
|
||||
Widget buildFrame() {
|
||||
@ -362,15 +360,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
Material(
|
||||
child: Center(
|
||||
child: Radio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: null,
|
||||
onChanged: log.add,
|
||||
toggleable: true,
|
||||
),
|
||||
),
|
||||
child: Center(child: Radio<int>(key: key, value: 1, onChanged: log.add, toggleable: true)),
|
||||
),
|
||||
);
|
||||
|
||||
@ -466,7 +456,6 @@ void main() {
|
||||
child: const RadioListTile<int>(
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: null,
|
||||
title: Text('Title'),
|
||||
internalAddSemanticForOnTap: true,
|
||||
),
|
||||
@ -504,7 +493,6 @@ void main() {
|
||||
child: const RadioListTile<int>(
|
||||
value: 2,
|
||||
groupValue: 2,
|
||||
onChanged: null,
|
||||
title: Text('Title'),
|
||||
internalAddSemanticForOnTap: true,
|
||||
),
|
||||
@ -607,7 +595,6 @@ void main() {
|
||||
child: RadioListTile<int>(
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: null,
|
||||
title: Text('Title', key: childKey),
|
||||
autofocus: true,
|
||||
),
|
||||
@ -619,8 +606,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('RadioListTile contentPadding test', (WidgetTester tester) async {
|
||||
final Type radioType =
|
||||
const Radio<bool>(groupValue: true, value: true, onChanged: null).runtimeType;
|
||||
final Type radioType = const Radio<bool>(groupValue: true, value: true).runtimeType;
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
@ -666,7 +652,6 @@ void main() {
|
||||
child: RadioListTile<bool>(
|
||||
value: true,
|
||||
groupValue: true,
|
||||
onChanged: null,
|
||||
title: Text('Title'),
|
||||
shape: shapeBorder,
|
||||
),
|
||||
@ -686,7 +671,6 @@ void main() {
|
||||
child: RadioListTile<bool>(
|
||||
value: false,
|
||||
groupValue: true,
|
||||
onChanged: null,
|
||||
title: const Text('Title'),
|
||||
tileColor: tileColor,
|
||||
),
|
||||
@ -706,7 +690,6 @@ void main() {
|
||||
child: RadioListTile<bool>(
|
||||
value: false,
|
||||
groupValue: true,
|
||||
onChanged: null,
|
||||
title: const Text('Title'),
|
||||
selected: true,
|
||||
selectedTileColor: selectedTileColor,
|
||||
@ -887,7 +870,7 @@ void main() {
|
||||
wrap(
|
||||
child: const MouseRegion(
|
||||
cursor: SystemMouseCursors.forbidden,
|
||||
child: RadioListTile<int>(value: 1, onChanged: null, groupValue: 2),
|
||||
child: RadioListTile<int>(value: 1, groupValue: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -1610,9 +1593,7 @@ void main() {
|
||||
testWidgets('RadioListTile renders with default scale', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Material(
|
||||
child: RadioListTile<bool>(value: false, groupValue: false, onChanged: null),
|
||||
),
|
||||
home: Material(child: RadioListTile<bool>(value: false, groupValue: false)),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1629,12 +1610,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Material(
|
||||
child: RadioListTile<bool>(
|
||||
value: false,
|
||||
groupValue: false,
|
||||
onChanged: null,
|
||||
radioScaleFactor: scale,
|
||||
),
|
||||
child: RadioListTile<bool>(value: false, groupValue: false, radioScaleFactor: scale),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -1668,7 +1644,6 @@ void main() {
|
||||
title: const Text('A'),
|
||||
subtitle: const Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'),
|
||||
value: 0,
|
||||
onChanged: null,
|
||||
groupValue: 1,
|
||||
),
|
||||
RadioListTile<int>(
|
||||
@ -1676,7 +1651,6 @@ void main() {
|
||||
title: const Text('A'),
|
||||
subtitle: const Text('A'),
|
||||
value: 0,
|
||||
onChanged: null,
|
||||
groupValue: 2,
|
||||
),
|
||||
],
|
||||
@ -1787,7 +1761,6 @@ void main() {
|
||||
title: const Text('A'),
|
||||
subtitle: const Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'),
|
||||
value: 0,
|
||||
onChanged: null,
|
||||
groupValue: 1,
|
||||
),
|
||||
RadioListTile<int>.adaptive(
|
||||
@ -1795,7 +1768,6 @@ void main() {
|
||||
title: const Text('A'),
|
||||
subtitle: const Text('A'),
|
||||
value: 0,
|
||||
onChanged: null,
|
||||
groupValue: 2,
|
||||
),
|
||||
],
|
||||
|
@ -63,9 +63,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
child: Material(
|
||||
child: Center(child: Radio<int>(key: key, value: 1, groupValue: 2, onChanged: null)),
|
||||
),
|
||||
child: Material(child: Center(child: Radio<int>(key: key, value: 1, groupValue: 2))),
|
||||
),
|
||||
);
|
||||
|
||||
@ -74,6 +72,32 @@ void main() {
|
||||
expect(log, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Radio disabled', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
final List<int?> log = <int?>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
child: Material(
|
||||
child: Center(
|
||||
child: Radio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
enabled: false,
|
||||
onChanged: log.add,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, equals(<int>[]));
|
||||
});
|
||||
|
||||
testWidgets('Radio can be toggled when toggleable is set', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
final List<int?> log = <int?>[];
|
||||
@ -127,13 +151,7 @@ void main() {
|
||||
data: theme,
|
||||
child: Material(
|
||||
child: Center(
|
||||
child: Radio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: null,
|
||||
onChanged: log.add,
|
||||
toggleable: true,
|
||||
),
|
||||
child: Radio<int>(key: key, value: 1, onChanged: log.add, toggleable: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -291,10 +309,7 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
child: const Material(child: Radio<int>(value: 1, groupValue: 2, onChanged: null)),
|
||||
),
|
||||
Theme(data: theme, child: const Material(child: Radio<int>(value: 1, groupValue: 2))),
|
||||
);
|
||||
|
||||
expect(
|
||||
@ -343,10 +358,7 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
child: const Material(child: Radio<int>(value: 2, groupValue: 2, onChanged: null)),
|
||||
),
|
||||
Theme(data: theme, child: const Material(child: Radio<int>(value: 2, groupValue: 2))),
|
||||
);
|
||||
|
||||
expect(
|
||||
@ -1091,7 +1103,7 @@ void main() {
|
||||
child: Material(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.forbidden,
|
||||
child: Radio<int>(value: 1, onChanged: null, groupValue: 2),
|
||||
child: Radio<int>(value: 1, groupValue: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1566,7 +1578,7 @@ void main() {
|
||||
home: const Material(
|
||||
child: Tooltip(
|
||||
message: longPressTooltip,
|
||||
child: Radio<bool>(value: true, groupValue: false, onChanged: null),
|
||||
child: Radio<bool>(value: true, groupValue: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1596,7 +1608,7 @@ void main() {
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
message: tapTooltip,
|
||||
child: Radio<bool>(value: true, groupValue: false, onChanged: null),
|
||||
child: Radio<bool>(value: true, groupValue: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
248
packages/flutter/test/widgets/radio_group_test.dart
Normal file
248
packages/flutter/test/widgets/radio_group_test.dart
Normal file
@ -0,0 +1,248 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Radio group control test', (WidgetTester tester) async {
|
||||
final UniqueKey key0 = UniqueKey();
|
||||
final UniqueKey key1 = UniqueKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Material(
|
||||
child: TestRadioGroup<int>(
|
||||
child: Column(
|
||||
children: <Widget>[Radio<int>(key: key0, value: 0), Radio<int>(key: key1, value: 1)],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key0)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key1)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key0)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: true, isEnabled: true),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key1)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key0)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key1)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: true, isEnabled: true),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio group can have disabled radio', (WidgetTester tester) async {
|
||||
final UniqueKey key0 = UniqueKey();
|
||||
final UniqueKey key1 = UniqueKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Material(
|
||||
child: TestRadioGroup<int>(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Radio<int>(key: key0, value: 0, enabled: false),
|
||||
Radio<int>(key: key1, value: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key0)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: false),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key1)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key0));
|
||||
await tester.pumpAndSettle();
|
||||
// Can't be select because the radio is disabled.
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key0)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: false),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byKey(key1)),
|
||||
containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio group can use arrow key', (WidgetTester tester) async {
|
||||
final UniqueKey key0 = UniqueKey();
|
||||
final UniqueKey key1 = UniqueKey();
|
||||
final UniqueKey key2 = UniqueKey();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: TestRadioGroup<int>(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Radio<int>(key: key0, focusNode: focusNode, value: 0),
|
||||
Radio<int>(key: key1, value: 1),
|
||||
Radio<int>(key: key2, value: 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
|
||||
find.byType(TestRadioGroup<int>),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key0));
|
||||
focusNode.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.groupValue, 0);
|
||||
expect(focusNode.hasFocus, isTrue);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.groupValue, 1);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.groupValue, 2);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pumpAndSettle();
|
||||
// Wrap around
|
||||
expect(state.groupValue, 0);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.pumpAndSettle();
|
||||
// Wrap around
|
||||
expect(state.groupValue, 2);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.pumpAndSettle();
|
||||
// Wrap around
|
||||
expect(state.groupValue, 1);
|
||||
});
|
||||
|
||||
testWidgets('Radio group can tab in and out', (WidgetTester tester) async {
|
||||
final UniqueKey key0 = UniqueKey();
|
||||
final UniqueKey key1 = UniqueKey();
|
||||
final UniqueKey key2 = UniqueKey();
|
||||
final FocusNode radio0 = FocusNode();
|
||||
addTearDown(radio0.dispose);
|
||||
final FocusNode radio1 = FocusNode();
|
||||
addTearDown(radio1.dispose);
|
||||
final FocusNode textFieldBefore = FocusNode();
|
||||
addTearDown(textFieldBefore.dispose);
|
||||
final FocusNode textFieldAfter = FocusNode();
|
||||
addTearDown(textFieldAfter.dispose);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextField(focusNode: textFieldBefore),
|
||||
TestRadioGroup<int>(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Radio<int>(key: key0, focusNode: radio0, value: 0),
|
||||
Radio<int>(key: key1, focusNode: radio1, value: 1),
|
||||
Radio<int>(key: key2, value: 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextField(focusNode: textFieldAfter),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
textFieldBefore.requestFocus();
|
||||
await tester.pump();
|
||||
expect(textFieldBefore.hasFocus, isTrue);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pump();
|
||||
// If no selected radio, focus the first.
|
||||
expect(textFieldBefore.hasFocus, isFalse);
|
||||
expect(radio0.hasFocus, isTrue);
|
||||
|
||||
// tab out the radio group.
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pump();
|
||||
expect(radio0.hasFocus, isFalse);
|
||||
expect(radio1.hasFocus, isFalse);
|
||||
expect(textFieldAfter.hasFocus, isTrue);
|
||||
|
||||
// Select the radio 1
|
||||
await tester.tap(find.byKey(key1));
|
||||
await tester.pump();
|
||||
final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
|
||||
find.byType(TestRadioGroup<int>),
|
||||
);
|
||||
expect(state.groupValue, 1);
|
||||
// focus textFieldAfter again.
|
||||
textFieldAfter.requestFocus();
|
||||
await tester.pump();
|
||||
expect(textFieldAfter.hasFocus, isTrue);
|
||||
|
||||
// shift+tab in the radio again.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||
await tester.pump();
|
||||
// Should focus selected radio
|
||||
expect(radio0.hasFocus, isFalse);
|
||||
expect(radio1.hasFocus, isTrue);
|
||||
expect(textFieldAfter.hasFocus, isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
class TestRadioGroup<T> extends StatefulWidget {
|
||||
const TestRadioGroup({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => TestRadioGroupState<T>();
|
||||
}
|
||||
|
||||
class TestRadioGroupState<T> extends State<TestRadioGroup<T>> {
|
||||
T? groupValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RadioGroup<T>(
|
||||
onChanged: (T? newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
},
|
||||
groupValue: groupValue,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
@ -14,20 +14,18 @@ void main() {
|
||||
testWidgets('RawRadio control test', (WidgetTester tester) async {
|
||||
final FocusNode node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
int? actualValue;
|
||||
ToggleableStateMixin? actualState;
|
||||
final TestRegistry<int> registry = TestRegistry<int>();
|
||||
|
||||
Widget buildWidget() {
|
||||
return RawRadio<int>(
|
||||
value: 0,
|
||||
groupValue: actualValue,
|
||||
onChanged: (int? value) {
|
||||
actualValue = value;
|
||||
},
|
||||
mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.click),
|
||||
toggleable: true,
|
||||
focusNode: node,
|
||||
autofocus: false,
|
||||
enabled: true,
|
||||
groupRegistry: registry,
|
||||
builder: (BuildContext context, ToggleableStateMixin state) {
|
||||
actualState = state;
|
||||
return CustomPaint(size: const Size(40, 40), painter: TestPainter());
|
||||
@ -38,14 +36,88 @@ void main() {
|
||||
await tester.pumpWidget(buildWidget());
|
||||
expect(actualState!.tristate, isTrue);
|
||||
expect(actualState!.value, isFalse);
|
||||
expect(registry.groupValue, isNull);
|
||||
|
||||
final State state = tester.state(find.byType(RawRadio<int>));
|
||||
expect(registry.clients.contains(state as RadioClient<int>), isTrue);
|
||||
|
||||
await tester.tap(find.byType(RawRadio<int>));
|
||||
// Rebuilds with new group value
|
||||
await tester.pumpWidget(buildWidget());
|
||||
|
||||
expect(actualValue, 0);
|
||||
expect(registry.groupValue, 0);
|
||||
expect(actualState!.value, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('RawRadio disabled', (WidgetTester tester) async {
|
||||
final FocusNode node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
final TestRegistry<int> registry = TestRegistry<int>();
|
||||
|
||||
Widget buildWidget() {
|
||||
return RawRadio<int>(
|
||||
value: 0,
|
||||
mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.click),
|
||||
toggleable: true,
|
||||
focusNode: node,
|
||||
autofocus: false,
|
||||
enabled: false,
|
||||
groupRegistry: registry,
|
||||
builder: (BuildContext context, ToggleableStateMixin state) {
|
||||
return CustomPaint(size: const Size(40, 40), painter: TestPainter());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.tap(find.byType(RawRadio<int>));
|
||||
// onChanged won't fire
|
||||
expect(registry.groupValue, isNull);
|
||||
});
|
||||
|
||||
testWidgets('RawRadio enabled without registry throws', (WidgetTester tester) async {
|
||||
final FocusNode node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
|
||||
Widget buildWidget() {
|
||||
return RawRadio<int>(
|
||||
value: 0,
|
||||
mouseCursor: WidgetStateProperty.all<MouseCursor>(SystemMouseCursors.click),
|
||||
toggleable: true,
|
||||
focusNode: node,
|
||||
autofocus: false,
|
||||
enabled: true,
|
||||
groupRegistry: null,
|
||||
builder: (BuildContext context, ToggleableStateMixin state) {
|
||||
return CustomPaint(size: const Size(40, 40), painter: TestPainter());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Object? error;
|
||||
try {
|
||||
await tester.pumpWidget(buildWidget());
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error, isA<AssertionError>());
|
||||
});
|
||||
}
|
||||
|
||||
class TestRegistry<T> extends RadioGroupRegistry<T> {
|
||||
final Set<RadioClient<T>> clients = <RadioClient<T>>{};
|
||||
@override
|
||||
T? groupValue;
|
||||
|
||||
@override
|
||||
ValueChanged<T?> get onChanged => (T? newValue) => groupValue = newValue;
|
||||
|
||||
@override
|
||||
void registerClient(RadioClient<T> radio) => clients.add(radio);
|
||||
|
||||
@override
|
||||
void unregisterClient(RadioClient<T> radio) => clients.remove(radio);
|
||||
}
|
||||
|
||||
class TestPainter extends CustomPainter {
|
||||
|
Loading…
Reference in New Issue
Block a user