radio.dart 6.82 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 16 17
const double _kDiameter = 16.0;
const double _kOuterRadius = _kDiameter / 2.0;
const double _kInnerRadius = 5.0;

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

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

  /// 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
69
  final T groupValue;
70 71 72

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

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

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

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

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

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

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

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

  final bool selected;
  final Color inactiveColor;
150
  final Color activeColor;
151
  final ValueChanged<bool> onChanged;
152
  final TickerProvider vsync;
153

154
  @override
155
  _RenderRadio createRenderObject(BuildContext context) => new _RenderRadio(
156
    value: selected,
157
    activeColor: activeColor,
158
    inactiveColor: inactiveColor,
159 160
    onChanged: onChanged,
    vsync: vsync,
161 162
  );

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

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

190
  @override
191 192
  bool get isInteractive => super.isInteractive && !value;

193
  @override
194 195 196
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

197
    paintRadialReaction(canvas, offset, const Offset(kRadialReactionRadius, kRadialReactionRadius));
198

199
    final Offset center = (offset & size).center;
200
    final Color radioColor = onChanged != null ? activeColor : inactiveColor;
201 202

    // Outer circle
203
    final Paint paint = new Paint()
204
      ..color = Color.lerp(inactiveColor, radioColor, position.value)
205
      ..style = PaintingStyle.stroke
206 207 208 209 210
      ..strokeWidth = 2.0;
    canvas.drawCircle(center, _kOuterRadius, paint);

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