Reputation: 9933
How can I create a custom radio button group like this in flutter
Upvotes: 45
Views: 82007
Reputation: 1
import 'package:flutter/material.dart';
enum CustomRadioType {
listRadio, // For a list of radio buttons
customRadio, // For a single customizable radio button
booleanRadio, // For yes/no or true/false options
}
/// A reusable and customizable radio button component supporting images.
class CustomRadio extends StatelessWidget {
final CustomRadioType type;
// Common parameters
final List<String>? options; // For list or boolean radio
final int? selectedIndex; // Index of the selected radio
final ValueChanged<int>? onChanged; // Callback for radio selection change
final String? labelText; // For labeling a single radio or boolean group
// Visual customization
final Color? activeColor;
final Color? inactiveColor;
final Color? textColor;
final TextStyle? textStyle;
final double? radioSize;
final EdgeInsetsGeometry? padding;
// For custom images
final String? selectedImage; // Image path for selected state
final String? unselectedImage; // Image path for unselected state
// For BooleanRadio specifically
final String? trueLabel;
final String? falseLabel;
const CustomRadio({
required this.type,
this.options,
this.selectedIndex,
this.onChanged,
this.labelText,
this.activeColor,
this.inactiveColor,
this.textColor,
this.textStyle,
this.radioSize,
this.padding,
this.selectedImage,
this.unselectedImage,
this.trueLabel = "Yes",
this.falseLabel = "No",
super.key,
});
@override
Widget build(BuildContext context) {
switch (type) {
case CustomRadioType.listRadio:
return _buildListRadio(context);
case CustomRadioType.customRadio:
return _buildCustomRadio(context);
case CustomRadioType.booleanRadio:
return _buildBooleanRadio(context);
}
}
/// Builds a list of radio buttons with labels from `options`.
Widget _buildListRadio(BuildContext context) {
return Padding(
padding: _getPadding(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: options?.asMap().entries.map((entry) {
final index = entry.key;
final label = entry.value;
return ListTile(
leading: _buildCustomImageRadio(
context,
index: index,
isSelected: selectedIndex == index,
),
title: Text(
label,
style: _getTextStyle(context),
),
onTap: () => onChanged?.call(index),
);
}).toList() ??
[],
),
);
}
/// Builds a single customizable radio button with an optional label.
Widget _buildCustomRadio(BuildContext context) {
return Padding(
padding: _getPadding(),
child: Row(
children: [
_buildCustomImageRadio(
context,
index: 0,
isSelected: selectedIndex == 0,
),
const SizedBox(width: 8),
Text(
labelText ?? '',
style: _getTextStyle(context),
),
],
),
);
}
/// Builds a radio button for boolean options (e.g., Yes/No or True/False).
Widget _buildBooleanRadio(BuildContext context) {
return Padding(
padding: _getPadding(),
child: Row(
children: [
Expanded(
child: ListTile(
leading: _buildCustomImageRadio(
context,
index: 0,
isSelected: selectedIndex == 0,
),
title: Text(
trueLabel ?? "Yes",
style: _getTextStyle(context),
),
onTap: () => onChanged?.call(0),
),
),
Expanded(
child: ListTile(
leading: _buildCustomImageRadio(
context,
index: 1,
isSelected: selectedIndex == 1,
),
title: Text(
falseLabel ?? "No",
style: _getTextStyle(context),
),
onTap: () => onChanged?.call(1),
),
),
],
),
);
}
/// Builds a single radio button using images for selected/unselected states.
Widget _buildCustomImageRadio(
BuildContext context, {
required int index,
required bool isSelected,
}) {
final imageSize = radioSize ?? 24.0;
return GestureDetector(
onTap: () => onChanged?.call(index),
child: SizedBox(
height: imageSize,
width: imageSize,
child: Image.asset(
isSelected ? selectedImage ?? '' : unselectedImage ?? '',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => Icon(
Icons.radio_button_unchecked,
color: isSelected
? activeColor ?? Theme.of(context).primaryColor
: inactiveColor ?? Colors.grey,
size: imageSize,
),
),
),
);
}
/// Gets the padding for the component or returns a default value.
EdgeInsetsGeometry _getPadding() {
return padding ?? const EdgeInsets.all(8.0);
}
/// Gets the text style or returns a default style.
TextStyle _getTextStyle(BuildContext context) {
return textStyle ??
TextStyle(
color: textColor ?? Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16.0,
);
}
}
class RadioExamplesWithImages extends StatelessWidget {
const RadioExamplesWithImages({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Radio Examples with Images')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'List Radio Buttons with Images',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
CustomRadio(
type: CustomRadioType.listRadio,
options: ['Option 1', 'Option 2', 'Option 3'],
selectedIndex: 1,
selectedImage: 'assets/selected.png',
unselectedImage: 'assets/unselected.png',
onChanged: (index) {
print('Selected ListRadio index: $index');
},
),
const SizedBox(height: 20),
const Text(
'Custom Radio Button with Image',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
CustomRadio(
type: CustomRadioType.customRadio,
labelText: 'Agree to Terms',
selectedIndex: 0,
selectedImage: 'assets/selected.png',
unselectedImage: 'assets/unselected.png',
onChanged: (index) {
print('Selected CustomRadio index: $index');
},
),
const SizedBox(height: 20),
const Text(
'Boolean Radio Buttons with Images',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
CustomRadio(
type: CustomRadioType.booleanRadio,
trueLabel: 'Yes',
falseLabel: 'No',
selectedIndex: 1,
selectedImage: 'assets/selected.png',
unselectedImage: 'assets/unselected.png',
onChanged: (index) {
print('Selected BooleanRadio index: $index');
},
),
],
),
),
);
}
}
Upvotes: 0
Reputation: 1247
First create A CustomRadioButton class
class CustomRadioButton<T> extends StatelessWidget {
final T value;
final T groupValue;
final String? leading;
final Widget? title;
final ValueChanged<T?> onChanged;
final MaterialColor colorbackground;
const CustomRadioButton({
super.key,
required this.value,
required this.groupValue,
required this.onChanged,
this.leading,
required this.colorbackground,
this.title,
});
@override
Widget build(BuildContext context) {
// final title = this.title;
return InkWell(
onTap: () => onChanged(value),
child: SizedBox(height: 3.h, width: 3.h, child: _customRadioButton),
);
}
Widget get _customRadioButton {
final isSelected = value == groupValue;
return Container(
padding: EdgeInsets.symmetric(horizontal: 1.h, vertical: 1.w),
decoration: BoxDecoration(
color: isSelected ? colorbackground : colorbackground,
shape: BoxShape.circle,
border: Border.all(
width: 0.4.h,
color: isSelected ? Colors.red : Colors.transparent,
),
),
);
}
}
Now call it from your mainClass
int _value = 0;
Container(
padding: const EdgeInsets.all(1),
height: 18.h, // Set the desired height for the container
width: 20.w,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
CustomRadioButton<int>(
colorbackground: Colors.orange,
value: 0,
groupValue: _value,
onChanged: (value) {
setState(() => _value = value!);
print(_value);
},
),
CustomRadioButton<int>(
colorbackground: Colors.green,
value: 1,
groupValue: _value,
onChanged: (value) {
setState(() => _value = value!);
print(_value);
},
),
CustomRadioButton<int>(
colorbackground: Colors.blue,
value: 2,
groupValue: _value,
onChanged: (value) {
setState(() => _value = value!);
print(_value);
},
),
CustomRadioButton<int>(
colorbackground: Colors.yellow,
value: 3,
groupValue: _value,
onChanged: (value) {
setState(() => _value = value!);
print(_value);
},
),
],
),
);
Upvotes: 1
Reputation: 89
I achieved that with the following logic. reply if you need a detailed explanation
import 'package:flutter/material.dart';
class Parent extends StatefulWidget {
Parent({
Key key,
}) : super(key: key);
@override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> {
int _selectedItem = 0;
selectItem(index) {
setState(() {
_selectedItem = index;
print(selectItem.toString());
});
}
@override
Widget build(BuildContext context) {
//...YOUR WIDGET TREE HERE
return ListView.builder(
shrinkWrap: true,
itemCount: 5,
itemBuilder: (context, index) {
return CustomItem(
selectItem, // callback function, setstate for parent
index: index,
isSelected: _selectedItem == index,
title: index.toString(),
);
},
);
}
}
class CustomItem extends StatefulWidget {
final String title;
final int index;
final bool isSelected;
Function(int) selectItem;
CustomItem(
this.selectItem, {
Key key,
this.title,
this.index,
this.isSelected,
}) : super(key: key);
_CustomItemState createState() => _CustomItemState();
}
class _CustomItemState extends State<CustomItem> {
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Text("${widget.isSelected ? "true" : "false"}"),
RaisedButton(
onPressed: () {
widget.selectItem(widget.index);
},
child: Text("${widget.title}"),
)
],
);
}
}
Upvotes: 6
Reputation: 164
https://i.sstatic.net/Hq0O2.png
Here is Custom Radio Group Button Widget. You can easily customize all property as per your requirement. Use:
GroupRadioButton(
label: [Text("A"), Text("B"), Text("C"), Text("D")],
padding: EdgeInsets.symmetric(vertical: 10),
spaceBetween: 5,
radioRadius: 10,
color: Const.mainColor,
onChanged: (listIndex) {
print(listIndex);
},
),
This is GroupRadioButton
widget
import 'package:flutter/material.dart';
class GroupRadioButton extends StatefulWidget {
GroupRadioButton({
@required this.label,
@required this.padding,
@required this.onChanged,
this.color = Colors.blue,
this.radioRadius = 14.0,
this.spaceBetween = 5.0,
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.start,
});
final Color color;
final List<Widget> label;
final EdgeInsets padding;
final Function(int) onChanged;
final double radioRadius;
final double spaceBetween;
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
@override
_GroupRadioButtonState createState() => _GroupRadioButtonState();
}
class _GroupRadioButtonState extends State<GroupRadioButton> {
int selectedIndex = 0;
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
itemCount: widget.label != null ? widget.label.length : 0,
itemBuilder: (context, index) {
return LabeledRadio(
selectedIndex: selectedIndex,
color: widget.color,
onChanged: (value) {
setState(() {
selectedIndex = value;
widget.onChanged(value);
// print(value);
});
},
index: index,
label: widget.label[index],
crossAxisAlignment: widget.crossAxisAlignment,
mainAxisAlignment: widget.mainAxisAlignment,
radioRadius: widget.radioRadius,
spaceBetween: widget.spaceBetween,
padding: widget.padding,
);
});
}
}
class LabeledRadio extends StatelessWidget {
LabeledRadio({
@required this.label,
@required this.index,
@required this.color,
//@required this.groupValue,
//@required this.value,
@required this.onChanged,
@required this.radioRadius,
@required this.padding,
@required this.spaceBetween,
@required this.mainAxisAlignment,
@required this.crossAxisAlignment,
this.selectedIndex,
});
final Color color;
final int selectedIndex;
final Widget label;
final index;
final EdgeInsets padding;
//final bool groupValue;
//final bool value;
final Function(int) onChanged;
final double radioRadius;
final double spaceBetween;
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
onChanged(index);
},
child: Padding(
padding: padding,
child: Row(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: <Widget>[
Container(
decoration: BoxDecoration(
//color: Const.mainColor,
shape: BoxShape.circle,
border: Border.all(color: color, width: 2),
),
padding: EdgeInsets.all(2),
child: selectedIndex == index
? Container(
height: radioRadius,
width: radioRadius,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
)
: Container(
height: radioRadius,
width: radioRadius,
),
),
SizedBox(
width: spaceBetween,
),
label,
],
),
),
);
}
}
Upvotes: 1
Reputation: 267584
Full code:
Create this custom class.
class MyRadioListTile<T> extends StatelessWidget {
final T value;
final T groupValue;
final String leading;
final Widget? title;
final ValueChanged<T?> onChanged;
const MyRadioListTile({
required this.value,
required this.groupValue,
required this.onChanged,
required this.leading,
this.title,
});
@override
Widget build(BuildContext context) {
final title = this.title;
return InkWell(
onTap: () => onChanged(value),
child: Container(
height: 56,
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_customRadioButton,
SizedBox(width: 12),
if (title != null) title,
],
),
),
);
}
Widget get _customRadioButton {
final isSelected = value == groupValue;
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : null,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey[300]!,
width: 2,
),
),
child: Text(
leading,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey[600]!,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
);
}
}
Use it in your widget like a regular RadioListTile
.
class _MyPageState extends State<MyPage> {
int _value = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
MyRadioListTile<int>(
value: 1,
groupValue: _value,
leading: 'A',
title: Text('One'),
onChanged: (value) => setState(() => _value = value!),
),
MyRadioListTile<int>(
value: 2,
groupValue: _value,
leading: 'B',
title: Text('Two'),
onChanged: (value) => setState(() => _value = value!),
),
MyRadioListTile<int>(
value: 3,
groupValue: _value,
leading: 'C',
title: Text('Three'),
onChanged: (value) => setState(() => _value = value!),
),
],
),
);
}
}
Upvotes: 31
Reputation: 4567
My RadioButton is like the 'Radio' widget:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class RadioButton<T> extends StatefulWidget {
RadioButton({
Key key,
@required this.value,
@required this.caption,
@required this.groupValue,
@required this.onChanged,
}) : assert(value != null),
assert(caption != null),
assert(groupValue != null),
assert(onChanged != null),
super(key: key);
final T value;
final T groupValue;
final String caption;
final Function onChanged;
@override
State<StatefulWidget> createState() => _RadioButtonState();
}
class _RadioButtonState extends State<RadioButton> {
@override
Widget build(BuildContext context) {
final bool selected = widget.value == widget.groupValue;
return GestureDetector(
onTap: () {
widget.onChanged(widget.value);
},
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: selected ? Colors.red : Colors.white),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
widget.caption,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.button
.copyWith(color: selected ? Colors.white : Colors.red),
),
),
),
);
}
}
Upvotes: 0
Reputation: 145
import 'package:flutter/material.dart';
class CustomRadio extends StatefulWidget {
@override
createState() {
return new CustomRadioState();
}
}
class CustomRadioState extends State<CustomRadio> {
List<RadioModel> sampleData = new List<RadioModel>();
@override
void initState() {
// TODO: implement initState
super.initState();
sampleData.add(new RadioModel(true, 'A',0xffe6194B));
sampleData.add(new RadioModel(false, 'B',0xfff58231));
sampleData.add(new RadioModel(false, 'C',0xffffe119));
sampleData.add(new RadioModel(false, 'D',0xffbfef45));
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("ListItem"),
),
body: new ListView.builder(
itemCount: sampleData.length,
itemBuilder: (BuildContext context, int index) {
return new InkWell(
splashColor: Colors.blueAccent,
onTap: () {
setState(() {
sampleData.forEach((element) => element.isSelected = false);
sampleData[index].isSelected = true;
});
},
child: new RadioItem(sampleData[index]),
);
},
),
);
}
}
class RadioItem extends StatelessWidget {
final RadioModel _item;
RadioItem(this._item);
@override
Widget build(BuildContext context) {
return new Container(
margin: new EdgeInsets.all(15.0),
child: new Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
new Container(
height: 25.0,
width: 25.0,
alignment: Alignment.center,
child:Container(
height: 15.0,
width: 15.0,
decoration: new BoxDecoration(
color:Color(_item.colorCode),
borderRadius: const BorderRadius.all(const Radius.circular(15)),
)
),
decoration: new BoxDecoration(
color: Colors.transparent,
border: new Border.all(
width: 3.0,
color: _item.isSelected
? Color(_item.colorCode)
: Colors.transparent),
borderRadius: const BorderRadius.all(const Radius.circular(25)),
),
),
new Container(
margin: new EdgeInsets.only(left: 10.0)
)
],
),
);
}
}
class RadioModel {
bool isSelected;
final String buttonText;
final int colorCode;
RadioModel(this.isSelected, this.buttonText,this.colorCode);
}
void main() {
runApp(new MaterialApp(
home: new CustomRadio(),
));
}
Click here to check out put-> Here
Upvotes: -3
Reputation: 9933
Here is the full code
class CustomRadio extends StatefulWidget {
@override
createState() {
return new CustomRadioState();
}
}
class CustomRadioState extends State<CustomRadio> {
List<RadioModel> sampleData = new List<RadioModel>();
@override
void initState() {
// TODO: implement initState
super.initState();
sampleData.add(new RadioModel(false, 'A', 'April 18'));
sampleData.add(new RadioModel(false, 'B', 'April 17'));
sampleData.add(new RadioModel(false, 'C', 'April 16'));
sampleData.add(new RadioModel(false, 'D', 'April 15'));
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("ListItem"),
),
body: new ListView.builder(
itemCount: sampleData.length,
itemBuilder: (BuildContext context, int index) {
return new InkWell(
//highlightColor: Colors.red,
splashColor: Colors.blueAccent,
onTap: () {
setState(() {
sampleData.forEach((element) => element.isSelected = false);
sampleData[index].isSelected = true;
});
},
child: new RadioItem(sampleData[index]),
);
},
),
);
}
}
class RadioItem extends StatelessWidget {
final RadioModel _item;
RadioItem(this._item);
@override
Widget build(BuildContext context) {
return new Container(
margin: new EdgeInsets.all(15.0),
child: new Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
new Container(
height: 50.0,
width: 50.0,
child: new Center(
child: new Text(_item.buttonText,
style: new TextStyle(
color:
_item.isSelected ? Colors.white : Colors.black,
//fontWeight: FontWeight.bold,
fontSize: 18.0)),
),
decoration: new BoxDecoration(
color: _item.isSelected
? Colors.blueAccent
: Colors.transparent,
border: new Border.all(
width: 1.0,
color: _item.isSelected
? Colors.blueAccent
: Colors.grey),
borderRadius: const BorderRadius.all(const Radius.circular(2.0)),
),
),
new Container(
margin: new EdgeInsets.only(left: 10.0),
child: new Text(_item.text),
)
],
),
);
}
}
class RadioModel {
bool isSelected;
final String buttonText;
final String text;
RadioModel(this.isSelected, this.buttonText, this.text);
}
To use :
void main() {
runApp(new MaterialApp(
home: new CustomRadio(),
));
}
Upvotes: 81
Reputation: 15789
You can create it with ListView and List Item with one local variable to store the selected item. And you can render the selected the ListItem based on the variable.
P.S. Let me know if you need code snippet.
[EDIT]
As you have requested, Here is code snipper which will show you how you can maintain the state of each ListView item.
Now you can play with it and make it the way you want. If you want only one selected item you can write the logic that way.
void main() {
runApp(new MaterialApp(
home: new ListItemDemo(),
));
}
class ListItemDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("ListItem"),
),
body: new ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return new MyListItem(
title: "Hello ${index + 1}",
);
}),
);
}
}
class MyListItem extends StatefulWidget {
final String title;
MyListItem({this.title});
@override
_MyListItemState createState() => new _MyListItemState();
}
class _MyListItemState extends State<MyListItem> {
bool isSelected;
@override
void initState() {
super.initState();
isSelected = false;
}
@override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new Text("${widget.title} ${isSelected ? "true" : "false"}"),
new RaisedButton(
onPressed: () {
if (isSelected) {
setState(() {
isSelected = false;
});
} else {
setState(() {
isSelected = true;
});
}
},
child: new Text("Select"),
)
],
);
}
}
Upvotes: 2