Skip to content

Commit 66d87f0

Browse files
Merge pull request #1344 from freemansoft/features/item-decoration
Add itemDecoration to checkbox groups nad radio groups to add borders to checkboxes and radio buttons
2 parents e518c0e + dde086c commit 66d87f0

8 files changed

+392
-9
lines changed

example/lib/main.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:example/sources/conditional_fields.dart';
2+
import 'package:example/sources/decorated_radio_checkbox.dart';
23
import 'package:example/sources/dynamic_fields.dart';
34
import 'package:example/sources/related_fields.dart';
45
import 'package:flutter/material.dart';
@@ -17,16 +18,19 @@ class MyApp extends StatelessWidget {
1718

1819
@override
1920
Widget build(BuildContext context) {
20-
return const MaterialApp(
21+
return MaterialApp(
2122
title: 'Flutter FormBuilder Demo',
2223
debugShowCheckedModeBanner: false,
23-
localizationsDelegates: [
24+
localizationsDelegates: const [
2425
FormBuilderLocalizations.delegate,
2526
...GlobalMaterialLocalizations.delegates,
2627
GlobalWidgetsLocalizations.delegate,
2728
],
2829
supportedLocales: FormBuilderLocalizations.supportedLocales,
29-
home: _HomePage(),
30+
theme: ThemeData.light().copyWith(
31+
appBarTheme: const AppBarTheme()
32+
.copyWith(backgroundColor: Colors.blue.shade200)),
33+
home: const _HomePage(),
3034
);
3135
}
3236
}
@@ -141,6 +145,23 @@ class _HomePage extends StatelessWidget {
141145
);
142146
},
143147
),
148+
const Divider(),
149+
ListTile(
150+
title: const Text('Radio Checkbox itemDecorator'),
151+
trailing: const Icon(Icons.arrow_right_sharp),
152+
onTap: () {
153+
Navigator.of(context).push(
154+
MaterialPageRoute(
155+
builder: (context) {
156+
return const CodePage(
157+
title: 'ItemDecorators',
158+
child: DecoratedRadioCheckbox(),
159+
);
160+
},
161+
),
162+
);
163+
},
164+
),
144165
],
145166
),
146167
);
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_form_builder/flutter_form_builder.dart';
3+
4+
/// Demonstrates the use of itemDecorators to wrap a box around selection items
5+
class DecoratedRadioCheckbox extends StatefulWidget {
6+
const DecoratedRadioCheckbox({Key? key}) : super(key: key);
7+
8+
@override
9+
State<DecoratedRadioCheckbox> createState() => _DecoratedRadioCheckboxState();
10+
}
11+
12+
class _DecoratedRadioCheckboxState extends State<DecoratedRadioCheckbox> {
13+
final _formKey = GlobalKey<FormBuilderState>();
14+
int? option;
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return SingleChildScrollView(
19+
child: FormBuilder(
20+
key: _formKey,
21+
child: Column(
22+
children: <Widget>[
23+
const SizedBox(height: 20),
24+
// this text appears correctly if the textScaler <> 1.0
25+
const Text(
26+
'label:column of Widgets itemBorder:true orient:wrap wrapSpacing:5.0',
27+
textScaler: TextScaler.linear(1.01)),
28+
FormBuilderCheckboxGroup(
29+
name: 'aCheckboxGroup1',
30+
options: getDemoOptionsWidgets(),
31+
wrapSpacing: 5.0,
32+
itemDecoration: BoxDecoration(
33+
color: Colors.orange.shade200,
34+
border: Border.all(color: Colors.blue),
35+
borderRadius: BorderRadius.circular(5.0)),
36+
),
37+
const SizedBox(height: 20),
38+
const Text(
39+
'label:column of Widgets itemBorder:true orient:wrap wrapSpacing:5.0',
40+
textScaler: TextScaler.linear(1.01)),
41+
FormBuilderCheckboxGroup(
42+
name: 'aCheckboxGroup2',
43+
options: getDemoOptionsWidgets(),
44+
wrapSpacing: 5.0,
45+
controlAffinity: ControlAffinity.trailing,
46+
itemDecoration: BoxDecoration(
47+
color: Colors.amber.shade200,
48+
border: Border.all(color: Colors.blueAccent),
49+
borderRadius: BorderRadius.circular(5.0)),
50+
),
51+
const SizedBox(height: 20),
52+
const Text(
53+
'label:column of Widgets itemBorder:true orient:wrap wrapSpacing: 5.0',
54+
textScaler: TextScaler.linear(1.01)),
55+
FormBuilderRadioGroup(
56+
name: 'aRadioGroup1',
57+
options: getDemoOptionsWidgets(),
58+
wrapSpacing: 5.0,
59+
itemDecoration: BoxDecoration(
60+
color: Colors.green.shade200,
61+
border: Border.all(color: Colors.blueAccent),
62+
borderRadius: BorderRadius.circular(5.0)),
63+
),
64+
const SizedBox(height: 20),
65+
const Text('label:value itemBorder:true orient:wrap wrapSpacing:10.0',
66+
textScaler: TextScaler.linear(1.01)),
67+
FormBuilderRadioGroup(
68+
name: 'aRadioGroup2',
69+
options: getDemoOptions(),
70+
wrapSpacing: 10.0,
71+
wrapRunSpacing: 10.0,
72+
decoration: InputDecoration(
73+
border: const OutlineInputBorder(),
74+
contentPadding: const EdgeInsets.only(left: 20, top: 40),
75+
labelText: 'hello there',
76+
icon: const Icon(Icons.access_alarm_outlined),
77+
fillColor: Colors.red.shade200),
78+
itemDecoration: BoxDecoration(
79+
color: Colors.blueGrey.shade200,
80+
border: Border.all(color: Colors.blueAccent),
81+
borderRadius: BorderRadius.circular(5.0)),
82+
),
83+
const SizedBox(height: 20),
84+
const Text(
85+
'itemDecoration:false label:value orient:wrap wrapSpacing:10.0',
86+
textScaler: TextScaler.linear(1.01)),
87+
FormBuilderRadioGroup(
88+
name: 'aRadioGroup3',
89+
options: getDemoOptions(),
90+
wrapSpacing: 10.0,
91+
),
92+
const SizedBox(height: 20),
93+
const Text('orient:horiz itemBorder:false wrapSpacing:5.0',
94+
textScaler: TextScaler.linear(1.01)),
95+
FormBuilderCheckboxGroup(
96+
name: 'aCheckboxGroup3',
97+
options: getDemoOptionsWidgets(),
98+
wrapSpacing: 5.0,
99+
orientation: OptionsOrientation.horizontal,
100+
itemDecoration: BoxDecoration(
101+
color: Colors.grey.shade300,
102+
// border: Border.all(color: Colors.blueAccent),
103+
borderRadius: BorderRadius.circular(5.0)),
104+
),
105+
const SizedBox(height: 20),
106+
const Text('orient:vert itemBorder:true wrapSpacing:5.0',
107+
textScaler: TextScaler.linear(1.01)),
108+
FormBuilderCheckboxGroup(
109+
name: 'aCheckboxGroup3',
110+
options: getDemoOptionsWidgets(),
111+
wrapSpacing: 5.0,
112+
orientation: OptionsOrientation.vertical,
113+
itemDecoration: BoxDecoration(
114+
color: Colors.red.shade100,
115+
border: Border.all(color: Colors.blueAccent),
116+
borderRadius: BorderRadius.circular(5.0)),
117+
),
118+
const SizedBox(height: 20),
119+
const Text(
120+
'label:w/sizebox orient:vert itemBorder:true wrapSpacing:5.0',
121+
textScaler: TextScaler.linear(1.01)),
122+
FormBuilderRadioGroup(
123+
name: 'aRadioGroup4',
124+
options: getDemoOptionsWidgets(forceMinWidth: 80.0),
125+
wrapSpacing: 5.0,
126+
orientation: OptionsOrientation.vertical,
127+
itemDecoration: BoxDecoration(
128+
color: Colors.lightBlue.shade100,
129+
border: Border.all(color: Colors.blueAccent),
130+
borderRadius: BorderRadius.circular(5.0)),
131+
),
132+
],
133+
),
134+
));
135+
}
136+
137+
/// options using column of widgets for the label
138+
/// We can force a min width by creating a sized box so we don't need another parameter
139+
List<FormBuilderFieldOption> getDemoOptionsWidgets({forceMinWidth = 0.0}) {
140+
return [
141+
FormBuilderFieldOption(
142+
value: "airplane",
143+
child: Container(
144+
padding: const EdgeInsets.all(5.0),
145+
child: Column(
146+
children: [
147+
const Text("Airplane"),
148+
const Icon(Icons.airplanemode_on),
149+
SizedBox(width: forceMinWidth, height: 0.0),
150+
],
151+
)),
152+
),
153+
FormBuilderFieldOption(
154+
value: "fire-truck",
155+
child: Container(
156+
padding: const EdgeInsets.all(5.0),
157+
child: Column(children: [
158+
const Text("Fire Truck"),
159+
const Icon(Icons.fire_truck),
160+
SizedBox(width: forceMinWidth, height: 0.0),
161+
])),
162+
),
163+
FormBuilderFieldOption(
164+
value: "bus-alert",
165+
child: Container(
166+
padding: const EdgeInsets.all(5.0),
167+
child: Column(children: [
168+
const Text("Bus Alert"),
169+
const Icon(Icons.bus_alert),
170+
SizedBox(width: forceMinWidth, height: 0.0),
171+
])),
172+
),
173+
FormBuilderFieldOption(
174+
value: "firetruck",
175+
child: Container(
176+
padding: const EdgeInsets.all(5.0),
177+
child: Column(children: [
178+
const Text("Motorcycle"),
179+
const Icon(Icons.motorcycle),
180+
SizedBox(width: forceMinWidth, height: 0.0),
181+
])),
182+
),
183+
];
184+
}
185+
186+
/// opens using just values
187+
List<FormBuilderFieldOption> getDemoOptions() {
188+
return const [
189+
FormBuilderFieldOption(
190+
value: "airplane",
191+
),
192+
FormBuilderFieldOption(
193+
value: "fire-truck",
194+
),
195+
FormBuilderFieldOption(
196+
value: "bus-alert",
197+
),
198+
FormBuilderFieldOption(
199+
value: "firetruck",
200+
),
201+
];
202+
}
203+
}

lib/src/fields/form_builder_checkbox_group.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class FormBuilderCheckboxGroup<T> extends FormBuilderFieldDecoration<List<T>> {
2525
final ControlAffinity controlAffinity;
2626
final OptionsOrientation orientation;
2727

28+
/// A BoxDecoration that is added to each item if provided
29+
/// WrapSpacing is reused for the the padding inside the itemDecoration
30+
/// on the side opposite from the control
31+
final BoxDecoration? itemDecoration;
32+
2833
/// Creates a list of Checkboxes for selecting multiple options
2934
FormBuilderCheckboxGroup({
3035
super.key,
@@ -60,6 +65,7 @@ class FormBuilderCheckboxGroup<T> extends FormBuilderFieldDecoration<List<T>> {
6065
this.separator,
6166
this.controlAffinity = ControlAffinity.leading,
6267
this.orientation = OptionsOrientation.wrap,
68+
this.itemDecoration,
6369
}) : super(
6470
builder: (FormFieldState<List<T>?> field) {
6571
final state = field as _FormBuilderCheckboxGroupState<T>;
@@ -93,6 +99,7 @@ class FormBuilderCheckboxGroup<T> extends FormBuilderFieldDecoration<List<T>> {
9399
wrapVerticalDirection: wrapVerticalDirection,
94100
separator: separator,
95101
controlAffinity: controlAffinity,
102+
itemDecoration: itemDecoration,
96103
),
97104
);
98105
},

lib/src/fields/form_builder_radio_group.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ class FormBuilderRadioGroup<T> extends FormBuilderFieldDecoration<T> {
2222
final WrapAlignment wrapRunAlignment;
2323
final WrapCrossAlignment wrapCrossAxisAlignment;
2424

25+
/// A BoxDecoration that is added to each item if provided
26+
/// WrapSpacing is reused for the the padding inside the itemDecoration
27+
/// on the side opposite from the control
28+
final BoxDecoration? itemDecoration;
29+
2530
/// Creates field to select one value from a list of Radio Widgets
2631
FormBuilderRadioGroup({
2732
super.autovalidateMode = AutovalidateMode.disabled,
@@ -54,6 +59,7 @@ class FormBuilderRadioGroup<T> extends FormBuilderFieldDecoration<T> {
5459
super.valueTransformer,
5560
super.onReset,
5661
super.restorationId,
62+
this.itemDecoration,
5763
}) : super(
5864
builder: (FormFieldState<T?> field) {
5965
final state = field as _FormBuilderRadioGroupState<T>;
@@ -84,6 +90,7 @@ class FormBuilderRadioGroup<T> extends FormBuilderFieldDecoration<T> {
8490
wrapSpacing: wrapSpacing,
8591
wrapTextDirection: wrapTextDirection,
8692
wrapVerticalDirection: wrapVerticalDirection,
93+
itemDecoration: itemDecoration,
8794
),
8895
);
8996
},

lib/src/widgets/grouped_checkbox.dart

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ class GroupedCheckbox<T> extends StatelessWidget {
181181

182182
final ControlAffinity controlAffinity;
183183

184+
/// A BoxDecoration that is added to each item if provided
185+
/// [wrapSpacing] is used as inter-item bottom margin for [Orientation.vertical]
186+
/// [wrapSpacing] is used as inter-item right margin for [Orientation.horizontal].
187+
/// on the side opposite from the control
188+
final BoxDecoration? itemDecoration;
189+
184190
const GroupedCheckbox({
185191
super.key,
186192
required this.options,
@@ -205,13 +211,14 @@ class GroupedCheckbox<T> extends StatelessWidget {
205211
this.separator,
206212
this.controlAffinity = ControlAffinity.leading,
207213
this.visualDensity,
214+
this.itemDecoration,
208215
});
209216

210217
@override
211218
Widget build(BuildContext context) {
212219
final widgetList = <Widget>[];
213220
for (var i = 0; i < options.length; i++) {
214-
widgetList.add(item(i));
221+
widgetList.add(buildItem(i));
215222
}
216223
Widget finalWidget;
217224
if (orientation == OptionsOrientation.vertical) {
@@ -249,7 +256,8 @@ class GroupedCheckbox<T> extends StatelessWidget {
249256
return finalWidget;
250257
}
251258

252-
Widget item(int index) {
259+
/// the composite of all the components for the option at index
260+
Widget buildItem(int index) {
253261
final option = options[index];
254262
final optionValue = option.value;
255263
final isOptionDisabled = true == disabled?.contains(optionValue);
@@ -287,7 +295,7 @@ class GroupedCheckbox<T> extends StatelessWidget {
287295
child: option,
288296
);
289297

290-
return Row(
298+
Widget compositeItem = Row(
291299
mainAxisSize: MainAxisSize.min,
292300
children: <Widget>[
293301
if (controlAffinity == ControlAffinity.leading) control,
@@ -296,5 +304,20 @@ class GroupedCheckbox<T> extends StatelessWidget {
296304
if (separator != null && index != options.length - 1) separator!,
297305
],
298306
);
307+
308+
if (this.itemDecoration != null) {
309+
compositeItem = Container(
310+
decoration: this.itemDecoration,
311+
margin: EdgeInsets.only(
312+
bottom:
313+
orientation == OptionsOrientation.vertical ? wrapSpacing : 0.0,
314+
right:
315+
orientation == OptionsOrientation.horizontal ? wrapSpacing : 0.0,
316+
),
317+
child: compositeItem,
318+
);
319+
}
320+
321+
return compositeItem;
299322
}
300323
}

0 commit comments

Comments
 (0)