radio.dart 9.59 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/rendering.dart';
6
import 'package:flutter/widgets.dart';
7

8
import 'constants.dart';
9
import 'debug.dart';
10
import 'theme.dart';
11
import 'theme_data.dart';
12
import 'toggleable.dart';
13

14 15
const double _kOuterRadius = 8.0;
const double _kInnerRadius = 4.5;
16

17 18
/// A material design radio button.
///
19 20 21 22
/// 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.
23
///
24 25 26 27 28 29
/// 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].
///
30
/// {@tool snippet --template=stateful_widget_scaffold_center}
31 32 33 34 35 36 37 38 39 40 41 42 43 44
///
/// 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`
/// will start off selected because `_character` is initialized to
/// `SingingCharacter.lafayette`.
///
/// If the second radio button is pressed, the example's state is updated
/// with `setState`, updating `_character` to `SingingCharacter.jefferson`.
/// This causes the buttons to rebuild with the updated `groupValue`, and
/// therefore the selection of the second button.
///
45 46
/// Requires one of its ancestors to be a [Material] widget.
///
47 48 49 50 51 52 53 54
/// ```dart preamble
/// enum SingingCharacter { lafayette, jefferson }
/// ```
///
/// ```dart
/// SingingCharacter _character = SingingCharacter.lafayette;
///
/// Widget build(BuildContext context) {
55 56 57 58 59 60 61 62 63 64
///   return Column(
///     children: <Widget>[
///       ListTile(
///         title: const Text('Lafayette'),
///         leading: Radio(
///           value: SingingCharacter.lafayette,
///           groupValue: _character,
///           onChanged: (SingingCharacter value) {
///             setState(() { _character = value; });
///           },
65
///         ),
66 67 68 69 70 71 72 73 74
///       ),
///       ListTile(
///         title: const Text('Thomas Jefferson'),
///         leading: Radio(
///           value: SingingCharacter.jefferson,
///           groupValue: _character,
///           onChanged: (SingingCharacter value) {
///             setState(() { _character = value; });
///           },
75
///         ),
76 77
///       ),
///     ],
78 79 80 81
///   );
/// }
/// ```
/// {@end-tool}
82 83
///
/// See also:
84
///
85 86
///  * [RadioListTile], which combines this widget with a [ListTile] so that
///    you can give the radio button a label.
87 88
///  * [Slider], for selecting a value in a range.
///  * [Checkbox] and [Switch], for toggling a particular value on or off.
89
///  * <https://material.io/design/components/selection-controls.html#radio-buttons>
90
class Radio<T> extends StatefulWidget {
91 92
  /// Creates a material design radio button.
  ///
93 94 95 96 97
  /// 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.
98
  ///
99 100 101 102 103
  /// 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.
104
  const Radio({
105
    Key key,
106 107 108
    @required this.value,
    @required this.groupValue,
    @required this.onChanged,
109 110
    this.activeColor,
    this.materialTapTargetSize,
111
  }) : super(key: key);
112

113
  /// The value represented by this radio button.
Hixie's avatar
Hixie committed
114
  final T value;
115

116
  /// The currently selected value for a group of radio buttons.
117 118 119
  ///
  /// This radio button is considered selected if its [value] matches the
  /// [groupValue].
Hixie's avatar
Hixie committed
120
  final T groupValue;
121 122 123

  /// Called when the user selects this radio button.
  ///
124 125 126 127 128
  /// 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.
129
  ///
130 131 132
  /// The provided callback will not be invoked if this radio button is already
  /// selected.
  ///
133
  /// The callback provided to [onChanged] should update the state of the parent
134 135 136 137
  /// [StatefulWidget] using the [State.setState] method, so that the parent
  /// gets rebuilt; for example:
  ///
  /// ```dart
138
  /// Radio<SingingCharacter>(
139 140 141 142 143 144 145
  ///   value: SingingCharacter.lafayette,
  ///   groupValue: _character,
  ///   onChanged: (SingingCharacter newValue) {
  ///     setState(() {
  ///       _character = newValue;
  ///     });
  ///   },
146
  /// )
147
  /// ```
Hixie's avatar
Hixie committed
148
  final ValueChanged<T> onChanged;
149

150 151
  /// The color to use when this radio button is selected.
  ///
152
  /// Defaults to [ThemeData.toggleableActiveColor].
153 154
  final Color activeColor;

155 156 157 158 159 160
  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
161
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
162 163
  final MaterialTapTargetSize materialTapTargetSize;

164
  @override
165
  _RadioState<T> createState() => _RadioState<T>();
166 167 168
}

class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
169
  bool get _enabled => widget.onChanged != null;
170

171
  Color _getInactiveColor(ThemeData themeData) {
172
    return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
173 174
  }

175 176
  void _handleChanged(bool selected) {
    if (selected)
177
      widget.onChanged(widget.value);
178 179
  }

180
  @override
181
  Widget build(BuildContext context) {
182
    assert(debugCheckHasMaterial(context));
183
    final ThemeData themeData = Theme.of(context);
184 185 186 187 188 189 190 191 192
    Size size;
    switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) {
      case MaterialTapTargetSize.padded:
        size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0);
        break;
      case MaterialTapTargetSize.shrinkWrap:
        size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius);
        break;
    }
193 194
    final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
    return _RadioRenderObjectWidget(
195
      selected: widget.value == widget.groupValue,
196
      activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
197 198
      inactiveColor: _getInactiveColor(themeData),
      onChanged: _enabled ? _handleChanged : null,
199
      additionalConstraints: additionalConstraints,
200
      vsync: this,
201 202 203
    );
  }
}
204 205

class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
206
  const _RadioRenderObjectWidget({
207
    Key key,
208 209 210
    @required this.selected,
    @required this.activeColor,
    @required this.inactiveColor,
211
    @required this.additionalConstraints,
212 213
    this.onChanged,
    @required this.vsync,
214 215 216 217 218
  }) : assert(selected != null),
       assert(activeColor != null),
       assert(inactiveColor != null),
       assert(vsync != null),
       super(key: key);
219 220 221

  final bool selected;
  final Color inactiveColor;
222
  final Color activeColor;
223
  final ValueChanged<bool> onChanged;
224
  final TickerProvider vsync;
225
  final BoxConstraints additionalConstraints;
226

227
  @override
228
  _RenderRadio createRenderObject(BuildContext context) => _RenderRadio(
229
    value: selected,
230
    activeColor: activeColor,
231
    inactiveColor: inactiveColor,
232 233
    onChanged: onChanged,
    vsync: vsync,
234
    additionalConstraints: additionalConstraints,
235 236
  );

237
  @override
238
  void updateRenderObject(BuildContext context, _RenderRadio renderObject) {
239 240 241 242
    renderObject
      ..value = selected
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
243
      ..onChanged = onChanged
244
      ..additionalConstraints = additionalConstraints
245
      ..vsync = vsync;
246 247 248 249 250 251
  }
}

class _RenderRadio extends RenderToggleable {
  _RenderRadio({
    bool value,
252
    Color activeColor,
253
    Color inactiveColor,
254
    ValueChanged<bool> onChanged,
255
    BoxConstraints additionalConstraints,
256
    @required TickerProvider vsync,
257 258 259 260 261 262 263 264 265
  }) : super(
         value: value,
         tristate: false,
         activeColor: activeColor,
         inactiveColor: inactiveColor,
         onChanged: onChanged,
         additionalConstraints: additionalConstraints,
         vsync: vsync,
       );
266

267
  @override
268 269 270
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

271
    paintRadialReaction(canvas, offset, size.center(Offset.zero));
272

273
    final Offset center = (offset & size).center;
274
    final Color radioColor = onChanged != null ? activeColor : inactiveColor;
275 276

    // Outer circle
277
    final Paint paint = Paint()
278
      ..color = Color.lerp(inactiveColor, radioColor, position.value)
279
      ..style = PaintingStyle.stroke
280 281 282 283 284
      ..strokeWidth = 2.0;
    canvas.drawCircle(center, _kOuterRadius, paint);

    // Inner circle
    if (!position.isDismissed) {
285
      paint.style = PaintingStyle.fill;
286 287 288
      canvas.drawCircle(center, _kInnerRadius * position.value, paint);
    }
  }
289 290 291 292

  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
293 294 295
    config
      ..isInMutuallyExclusiveGroup = true
      ..isChecked = value == true;
296
  }
297
}