radio.dart 6.42 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 20 21 22 23 24 25 26 27 28 29 30 31 32
/// 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 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 36
///  * [CheckBox]
///  * [Slider]
///  * [Switch]
37
///  * <https://material.google.com/components/selection-controls.html#selection-controls-radio-button>
38
class Radio<T> extends StatefulWidget {
39 40 41 42 43 44 45 46 47 48
  /// Creates a material design radio button.
  ///
  /// 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.
  ///
  /// * [value] and [groupValue] together determines whether the radio button is selected.
  /// * [onChanged] is when the user selects this radio button.
49
  Radio({
50
    Key key,
51 52 53 54
    @required this.value,
    @required this.groupValue,
    @required this.onChanged,
    this.activeColor
55
  }) : super(key: key);
56

57
  /// The value represented by this radio button.
Hixie's avatar
Hixie committed
58
  final T value;
59 60 61 62 63

  /// 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
64
  final T groupValue;
65 66 67

  /// Called when the user selects this radio button.
  ///
68 69 70 71 72
  /// 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.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
  ///
  /// 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;
  ///     });
  ///   },
  /// ),
  /// ```
Hixie's avatar
Hixie committed
89
  final ValueChanged<T> onChanged;
90

91 92 93 94 95
  /// The color to use when this radio button is selected.
  ///
  /// Defaults to accent color of the current [Theme].
  final Color activeColor;

96 97 98 99 100 101
  @override
  _RadioState<T> createState() => new _RadioState<T>();
}

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

103
  Color _getInactiveColor(ThemeData themeData) {
104
    return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
105 106
  }

107 108
  void _handleChanged(bool selected) {
    if (selected)
109
      config.onChanged(config.value);
110 111
  }

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

class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
  _RadioRenderObjectWidget({
    Key key,
132 133 134 135 136
    @required this.selected,
    @required this.activeColor,
    @required this.inactiveColor,
    this.onChanged,
    @required this.vsync,
137
  }) : super(key: key) {
138 139
    assert(selected != null);
    assert(activeColor != null);
140
    assert(inactiveColor != null);
141
    assert(vsync != null);
142 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 179 180 181
  }): super(
    value: value,
    activeColor: activeColor,
    inactiveColor: inactiveColor,
    onChanged: onChanged,
182 183
    size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
    vsync: vsync,
184
  );
185

186
  @override
187 188
  bool get isInteractive => super.isInteractive && !value;

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

Adam Barth's avatar
Adam Barth committed
193
    paintRadialReaction(canvas, offset, const Point(kRadialReactionRadius, kRadialReactionRadius));
194 195

    Point center = (offset & size).center;
196
    Color radioColor = onChanged != null ? activeColor : inactiveColor;
197 198 199

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

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