284 lines
8.6 KiB
Dart
284 lines
8.6 KiB
Dart
|
|
/*
|
||
|
|
* Redistribution and use in source and binary forms, with or without
|
||
|
|
* modification, are permitted provided that the following conditions are met:
|
||
|
|
*
|
||
|
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||
|
|
* this list of conditions and the following disclaimer.
|
||
|
|
*
|
||
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||
|
|
* this list of conditions and the following disclaimer in the
|
||
|
|
* documentation and/or other materials provided with the distribution.
|
||
|
|
*
|
||
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
|
||
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
||
|
|
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||
|
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||
|
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import 'form_field_controller.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
|
||
|
|
class FlutterRadioButton extends StatefulWidget {
|
||
|
|
const FlutterRadioButton({
|
||
|
|
super.key,
|
||
|
|
required this.options,
|
||
|
|
required this.onChanged,
|
||
|
|
required this.controller,
|
||
|
|
required this.optionHeight,
|
||
|
|
required this.textStyle,
|
||
|
|
this.optionWidth,
|
||
|
|
this.selectedTextStyle,
|
||
|
|
this.textPadding = EdgeInsets.zero,
|
||
|
|
this.buttonPosition = RadioButtonPosition.left,
|
||
|
|
this.direction = Axis.vertical,
|
||
|
|
required this.radioButtonColor,
|
||
|
|
this.inactiveRadioButtonColor,
|
||
|
|
this.toggleable = false,
|
||
|
|
this.horizontalAlignment = WrapAlignment.start,
|
||
|
|
this.verticalAlignment = WrapCrossAlignment.start,
|
||
|
|
});
|
||
|
|
|
||
|
|
final List<String> options;
|
||
|
|
final Function(String?)? onChanged;
|
||
|
|
final FormFieldController<String> controller;
|
||
|
|
final double optionHeight;
|
||
|
|
final double? optionWidth;
|
||
|
|
final TextStyle textStyle;
|
||
|
|
final TextStyle? selectedTextStyle;
|
||
|
|
final EdgeInsetsGeometry textPadding;
|
||
|
|
final RadioButtonPosition buttonPosition;
|
||
|
|
final Axis direction;
|
||
|
|
final Color radioButtonColor;
|
||
|
|
final Color? inactiveRadioButtonColor;
|
||
|
|
final bool toggleable;
|
||
|
|
final WrapAlignment horizontalAlignment;
|
||
|
|
final WrapCrossAlignment verticalAlignment;
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<FlutterRadioButton> createState() => _FlutterRadioButtonState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _FlutterRadioButtonState extends State<FlutterRadioButton> {
|
||
|
|
bool get enabled => widget.onChanged != null;
|
||
|
|
FormFieldController<String> get controller => widget.controller;
|
||
|
|
void Function()? _listener;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_maybeSetOnChangedListener();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_maybeRemoveOnChangedListener();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void didUpdateWidget(FlutterRadioButton oldWidget) {
|
||
|
|
super.didUpdateWidget(oldWidget);
|
||
|
|
final oldWidgetEnabled = oldWidget.onChanged != null;
|
||
|
|
if (oldWidgetEnabled != enabled) {
|
||
|
|
_maybeRemoveOnChangedListener();
|
||
|
|
_maybeSetOnChangedListener();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _maybeSetOnChangedListener() {
|
||
|
|
if (enabled) {
|
||
|
|
_listener = () => widget.onChanged!(controller.value);
|
||
|
|
controller.addListener(_listener!);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _maybeRemoveOnChangedListener() {
|
||
|
|
if (_listener != null) {
|
||
|
|
controller.removeListener(_listener!);
|
||
|
|
_listener = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
List<String> get effectiveOptions =>
|
||
|
|
widget.options.isEmpty ? ['[Option]'] : widget.options;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return Theme(
|
||
|
|
data: Theme.of(context)
|
||
|
|
.copyWith(unselectedWidgetColor: widget.inactiveRadioButtonColor),
|
||
|
|
child: RadioGroup<String>.builder(
|
||
|
|
direction: widget.direction,
|
||
|
|
groupValue: controller.value,
|
||
|
|
onChanged: enabled ? (value) => controller.value = value : null,
|
||
|
|
activeColor: widget.radioButtonColor,
|
||
|
|
toggleable: widget.toggleable,
|
||
|
|
textStyle: widget.textStyle,
|
||
|
|
selectedTextStyle: widget.selectedTextStyle ?? widget.textStyle,
|
||
|
|
textPadding: widget.textPadding,
|
||
|
|
optionHeight: widget.optionHeight,
|
||
|
|
optionWidth: widget.optionWidth,
|
||
|
|
horizontalAlignment: widget.horizontalAlignment,
|
||
|
|
verticalAlignment: widget.verticalAlignment,
|
||
|
|
items: effectiveOptions,
|
||
|
|
itemBuilder: (item) =>
|
||
|
|
RadioButtonBuilder(item, buttonPosition: widget.buttonPosition),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum RadioButtonPosition {
|
||
|
|
right,
|
||
|
|
left,
|
||
|
|
}
|
||
|
|
|
||
|
|
class RadioButtonBuilder<T> {
|
||
|
|
RadioButtonBuilder(
|
||
|
|
this.description, {
|
||
|
|
this.buttonPosition = RadioButtonPosition.left,
|
||
|
|
});
|
||
|
|
|
||
|
|
final String description;
|
||
|
|
final RadioButtonPosition buttonPosition;
|
||
|
|
}
|
||
|
|
|
||
|
|
class RadioButton<T> extends StatelessWidget {
|
||
|
|
const RadioButton({
|
||
|
|
super.key,
|
||
|
|
required this.description,
|
||
|
|
required this.value,
|
||
|
|
required this.groupValue,
|
||
|
|
required this.onChanged,
|
||
|
|
required this.buttonPosition,
|
||
|
|
required this.activeColor,
|
||
|
|
required this.toggleable,
|
||
|
|
required this.textStyle,
|
||
|
|
required this.selectedTextStyle,
|
||
|
|
required this.textPadding,
|
||
|
|
this.shouldFlex = false,
|
||
|
|
});
|
||
|
|
|
||
|
|
final String description;
|
||
|
|
final T value;
|
||
|
|
final T? groupValue;
|
||
|
|
final void Function(T?)? onChanged;
|
||
|
|
final RadioButtonPosition buttonPosition;
|
||
|
|
final Color activeColor;
|
||
|
|
final bool toggleable;
|
||
|
|
final TextStyle textStyle;
|
||
|
|
final TextStyle selectedTextStyle;
|
||
|
|
final EdgeInsetsGeometry textPadding;
|
||
|
|
final bool shouldFlex;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final selectedStyle = selectedTextStyle;
|
||
|
|
final isSelected = value == groupValue;
|
||
|
|
Widget radioButtonText = Padding(
|
||
|
|
padding: textPadding,
|
||
|
|
child: Text(
|
||
|
|
description,
|
||
|
|
style: isSelected ? selectedStyle : textStyle,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
if (shouldFlex) {
|
||
|
|
radioButtonText = Flexible(child: radioButtonText);
|
||
|
|
}
|
||
|
|
return InkWell(
|
||
|
|
onTap: onChanged != null ? () => onChanged!(value) : null,
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: <Widget>[
|
||
|
|
if (buttonPosition == RadioButtonPosition.right) radioButtonText,
|
||
|
|
Radio<T>(
|
||
|
|
groupValue: groupValue,
|
||
|
|
onChanged: onChanged,
|
||
|
|
value: value,
|
||
|
|
activeColor: activeColor,
|
||
|
|
toggleable: toggleable,
|
||
|
|
),
|
||
|
|
if (buttonPosition == RadioButtonPosition.left) radioButtonText,
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class RadioGroup<T> extends StatelessWidget {
|
||
|
|
const RadioGroup.builder({
|
||
|
|
super.key,
|
||
|
|
required this.groupValue,
|
||
|
|
required this.onChanged,
|
||
|
|
required this.items,
|
||
|
|
required this.itemBuilder,
|
||
|
|
required this.direction,
|
||
|
|
required this.optionHeight,
|
||
|
|
required this.horizontalAlignment,
|
||
|
|
required this.activeColor,
|
||
|
|
required this.toggleable,
|
||
|
|
required this.textStyle,
|
||
|
|
required this.selectedTextStyle,
|
||
|
|
required this.textPadding,
|
||
|
|
this.optionWidth,
|
||
|
|
this.verticalAlignment = WrapCrossAlignment.center,
|
||
|
|
});
|
||
|
|
|
||
|
|
final T? groupValue;
|
||
|
|
final List<T> items;
|
||
|
|
final RadioButtonBuilder Function(T value) itemBuilder;
|
||
|
|
final void Function(T?)? onChanged;
|
||
|
|
final Axis direction;
|
||
|
|
final double optionHeight;
|
||
|
|
final double? optionWidth;
|
||
|
|
final WrapAlignment horizontalAlignment;
|
||
|
|
final WrapCrossAlignment verticalAlignment;
|
||
|
|
final Color activeColor;
|
||
|
|
final bool toggleable;
|
||
|
|
final TextStyle textStyle;
|
||
|
|
final TextStyle selectedTextStyle;
|
||
|
|
final EdgeInsetsGeometry textPadding;
|
||
|
|
|
||
|
|
List<Widget> get _group => items.map(
|
||
|
|
(item) {
|
||
|
|
final radioButtonBuilder = itemBuilder(item);
|
||
|
|
return SizedBox(
|
||
|
|
height: optionHeight,
|
||
|
|
width: optionWidth,
|
||
|
|
child: RadioButton(
|
||
|
|
description: radioButtonBuilder.description,
|
||
|
|
value: item,
|
||
|
|
groupValue: groupValue,
|
||
|
|
onChanged: onChanged,
|
||
|
|
buttonPosition: radioButtonBuilder.buttonPosition,
|
||
|
|
activeColor: activeColor,
|
||
|
|
toggleable: toggleable,
|
||
|
|
textStyle: textStyle,
|
||
|
|
selectedTextStyle: selectedTextStyle,
|
||
|
|
textPadding: textPadding,
|
||
|
|
shouldFlex: optionWidth != null,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
).toList();
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) => direction == Axis.horizontal
|
||
|
|
? Wrap(
|
||
|
|
direction: direction,
|
||
|
|
alignment: horizontalAlignment,
|
||
|
|
children: _group,
|
||
|
|
)
|
||
|
|
: Wrap(
|
||
|
|
direction: direction,
|
||
|
|
crossAxisAlignment: verticalAlignment,
|
||
|
|
children: _group,
|
||
|
|
);
|
||
|
|
}
|