| // Copyright 2015 The Chromium 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/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'theme.dart'; |
| import 'toggleable.dart'; |
| |
| const double _kDiameter = 16.0; |
| const double _kOuterRadius = _kDiameter / 2.0; |
| const double _kInnerRadius = 5.0; |
| |
| /// A material design radio button. |
| /// |
| /// 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, when the state |
| /// of the radio button changes, the widget calls the [onChanged] callback. |
| /// Most widget 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. |
| /// |
| /// Requires one of its ancestors to be a [Material] widget. |
| /// |
| /// See also: |
| /// |
| /// * [RadioListTile], which combines this widget with a [ListTile] so that |
| /// you can give the radio button a label. |
| /// * [Slider], for selecting a value in a range. |
| /// * [Checkbox] and [Switch], for toggling a particular value on or off. |
| /// * <https://material.google.com/components/selection-controls.html#selection-controls-radio-button> |
| 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. |
| /// |
| /// 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. |
| const Radio({ |
| Key key, |
| @required this.value, |
| @required this.groupValue, |
| @required this.onChanged, |
| this.activeColor |
| }) : super(key: key); |
| |
| /// The value represented by this radio button. |
| final T value; |
| |
| /// The currently selected value for this group of radio buttons. |
| /// |
| /// This radio button is considered selected if its [value] matches the |
| /// [groupValue]. |
| final T groupValue; |
| |
| /// 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 callback provided to [onChanged] should update the state of the parent |
| /// [StatefulWidget] using the [State.setState] method, so that the parent |
| /// gets rebuilt; for example: |
| /// |
| /// ```dart |
| /// new Radio<SingingCharacter>( |
| /// value: SingingCharacter.lafayette, |
| /// groupValue: _character, |
| /// onChanged: (SingingCharacter newValue) { |
| /// setState(() { |
| /// _character = newValue; |
| /// }); |
| /// }, |
| /// ) |
| /// ``` |
| final ValueChanged<T> onChanged; |
| |
| /// The color to use when this radio button is selected. |
| /// |
| /// Defaults to accent color of the current [Theme]. |
| final Color activeColor; |
| |
| @override |
| _RadioState<T> createState() => new _RadioState<T>(); |
| } |
| |
| class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { |
| bool get _enabled => widget.onChanged != null; |
| |
| Color _getInactiveColor(ThemeData themeData) { |
| return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor; |
| } |
| |
| void _handleChanged(bool selected) { |
| if (selected) |
| widget.onChanged(widget.value); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterial(context)); |
| final ThemeData themeData = Theme.of(context); |
| return new Semantics( |
| checked: widget.value == widget.groupValue, |
| child: new _RadioRenderObjectWidget( |
| selected: widget.value == widget.groupValue, |
| activeColor: widget.activeColor ?? themeData.accentColor, |
| inactiveColor: _getInactiveColor(themeData), |
| onChanged: _enabled ? _handleChanged : null, |
| vsync: this, |
| ) |
| ); |
| } |
| } |
| |
| class _RadioRenderObjectWidget extends LeafRenderObjectWidget { |
| const _RadioRenderObjectWidget({ |
| Key key, |
| @required this.selected, |
| @required this.activeColor, |
| @required this.inactiveColor, |
| this.onChanged, |
| @required this.vsync, |
| }) : assert(selected != null), |
| assert(activeColor != null), |
| assert(inactiveColor != null), |
| assert(vsync != null), |
| super(key: key); |
| |
| final bool selected; |
| final Color inactiveColor; |
| final Color activeColor; |
| final ValueChanged<bool> onChanged; |
| final TickerProvider vsync; |
| |
| @override |
| _RenderRadio createRenderObject(BuildContext context) => new _RenderRadio( |
| value: selected, |
| activeColor: activeColor, |
| inactiveColor: inactiveColor, |
| onChanged: onChanged, |
| vsync: vsync, |
| ); |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderRadio renderObject) { |
| renderObject |
| ..value = selected |
| ..activeColor = activeColor |
| ..inactiveColor = inactiveColor |
| ..onChanged = onChanged |
| ..vsync = vsync; |
| } |
| } |
| |
| class _RenderRadio extends RenderToggleable { |
| _RenderRadio({ |
| bool value, |
| Color activeColor, |
| Color inactiveColor, |
| ValueChanged<bool> onChanged, |
| @required TickerProvider vsync, |
| }): super( |
| value: value, |
| activeColor: activeColor, |
| inactiveColor: inactiveColor, |
| onChanged: onChanged, |
| size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), |
| vsync: vsync, |
| ); |
| |
| @override |
| bool get isInteractive => super.isInteractive && !value; |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| final Canvas canvas = context.canvas; |
| |
| paintRadialReaction(canvas, offset, const Offset(kRadialReactionRadius, kRadialReactionRadius)); |
| |
| final Offset center = (offset & size).center; |
| final Color radioColor = onChanged != null ? activeColor : inactiveColor; |
| |
| // Outer circle |
| final Paint paint = new Paint() |
| ..color = Color.lerp(inactiveColor, radioColor, position.value) |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 2.0; |
| canvas.drawCircle(center, _kOuterRadius, paint); |
| |
| // Inner circle |
| if (!position.isDismissed) { |
| paint.style = PaintingStyle.fill; |
| canvas.drawCircle(center, _kInnerRadius * position.value, paint); |
| } |
| } |
| } |