radio.dart 6.81 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/foundation.dart';
6
import 'package:flutter/rendering.dart';
7
import 'package:flutter/widgets.dart';
8

9
import 'constants.dart';
10
import 'debug.dart';
11
import 'theme.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 30 31 32
///
/// 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:
33
///
34 35
///  * [RadioListTile], which combines this widget with a [ListTile] so that
///    you can give the radio button a label.
36 37
///  * [Slider], for selecting a value in a range.
///  * [Checkbox] and [Switch], for toggling a particular value on or off.
38
///  * <https://material.google.com/components/selection-controls.html#selection-controls-radio-button>
39
class Radio<T> extends StatefulWidget {
40 41
  /// Creates a material design radio button.
  ///
42 43 44 45 46
  /// 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.
47
  ///
48 49 50 51 52
  /// 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.
53
  const Radio({
54
    Key key,
55 56 57 58
    @required this.value,
    @required this.groupValue,
    @required this.onChanged,
    this.activeColor
59
  }) : super(key: key);
60

61
  /// The value represented by this radio button.
Hixie's avatar
Hixie committed
62
  final T value;
63 64 65 66 67

  /// The currently selected value for this group of radio buttons.
  ///
  /// This radio button is considered selected if its [value] matches the
  /// [groupValue].
Hixie's avatar
Hixie committed
68
  final T groupValue;
69 70 71

  /// Called when the user selects this radio button.
  ///
72 73 74 75 76
  /// 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.
77
  ///
78
  /// The callback provided to [onChanged] should update the state of the parent
79 80 81 82 83 84 85 86 87 88 89 90
  /// [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;
  ///     });
  ///   },
91
  /// )
92
  /// ```
Hixie's avatar
Hixie committed
93
  final ValueChanged<T> onChanged;
94

95 96 97 98 99
  /// The color to use when this radio button is selected.
  ///
  /// Defaults to accent color of the current [Theme].
  final Color activeColor;

100 101 102 103 104
  @override
  _RadioState<T> createState() => new _RadioState<T>();
}

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

107
  Color _getInactiveColor(ThemeData themeData) {
108
    return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
109 110
  }

111 112
  void _handleChanged(bool selected) {
    if (selected)
113
      widget.onChanged(widget.value);
114 115
  }

116
  @override
117
  Widget build(BuildContext context) {
118
    assert(debugCheckHasMaterial(context));
119
    final ThemeData themeData = Theme.of(context);
120 121 122 123 124 125
    return new _RadioRenderObjectWidget(
      selected: widget.value == widget.groupValue,
      activeColor: widget.activeColor ?? themeData.accentColor,
      inactiveColor: _getInactiveColor(themeData),
      onChanged: _enabled ? _handleChanged : null,
      vsync: this,
126 127 128
    );
  }
}
129 130

class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
131
  const _RadioRenderObjectWidget({
132
    Key key,
133 134 135 136 137
    @required this.selected,
    @required this.activeColor,
    @required this.inactiveColor,
    this.onChanged,
    @required this.vsync,
138 139 140 141 142
  }) : assert(selected != null),
       assert(activeColor != null),
       assert(inactiveColor != null),
       assert(vsync != null),
       super(key: key);
143 144 145

  final bool selected;
  final Color inactiveColor;
146
  final Color activeColor;
147
  final ValueChanged<bool> onChanged;
148
  final TickerProvider vsync;
149

150
  @override
151
  _RenderRadio createRenderObject(BuildContext context) => new _RenderRadio(
152
    value: selected,
153
    activeColor: activeColor,
154
    inactiveColor: inactiveColor,
155 156
    onChanged: onChanged,
    vsync: vsync,
157 158
  );

159
  @override
160
  void updateRenderObject(BuildContext context, _RenderRadio renderObject) {
161 162 163 164
    renderObject
      ..value = selected
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
165 166
      ..onChanged = onChanged
      ..vsync = vsync;
167 168 169 170 171 172
  }
}

class _RenderRadio extends RenderToggleable {
  _RenderRadio({
    bool value,
173
    Color activeColor,
174
    Color inactiveColor,
175 176
    ValueChanged<bool> onChanged,
    @required TickerProvider vsync,
177 178
  }): super(
    value: value,
179
    tristate: false,
180 181 182
    activeColor: activeColor,
    inactiveColor: inactiveColor,
    onChanged: onChanged,
183 184
    size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
    vsync: vsync,
185
  );
186

187
  @override
188 189 190
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

191
    paintRadialReaction(canvas, offset, const Offset(kRadialReactionRadius, kRadialReactionRadius));
192

193
    final Offset center = (offset & size).center;
194
    final Color radioColor = onChanged != null ? activeColor : inactiveColor;
195 196

    // Outer circle
197
    final Paint paint = new Paint()
198
      ..color = Color.lerp(inactiveColor, radioColor, position.value)
199
      ..style = PaintingStyle.stroke
200 201 202 203 204
      ..strokeWidth = 2.0;
    canvas.drawCircle(center, _kOuterRadius, paint);

    // Inner circle
    if (!position.isDismissed) {
205
      paint.style = PaintingStyle.fill;
206 207 208
      canvas.drawCircle(center, _kInnerRadius * position.value, paint);
    }
  }
209 210 211 212 213 214

  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.isInMutuallyExclusiveGroup = true;
  }
215
}