blob: 25cbd498b2b3d209a4bba70f7a2fae6c25d0b20e [file] [log] [blame]
// 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);
}
}
}