| // Copyright 2014 The Flutter 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/widgets.dart'; |
| |
| import 'colors.dart'; |
| import 'toggleable.dart'; |
| |
| // Examples can assume: |
| // late BuildContext context; |
| // enum SingingCharacter { lafayette } |
| // late SingingCharacter? _character; |
| // late StateSetter setState; |
| |
| const Size _size = Size(18.0, 18.0); |
| const double _kOuterRadius = 7.0; |
| const double _kInnerRadius = 2.975; |
| |
| // The relative values needed to transform a color to its equivilant focus |
| // outline color. |
| const double _kCupertinoFocusColorOpacity = 0.80; |
| const double _kCupertinoFocusColorBrightness = 0.69; |
| const double _kCupertinoFocusColorSaturation = 0.835; |
| |
| /// A macOS-style 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 are |
| /// deselected. The values are of type `T`, the type parameter of the |
| /// [CupertinoRadio] class. Enums are commonly used for this purpose. |
| /// |
| /// 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]. |
| /// |
| /// {@tool dartpad} |
| /// Here is an example of CupertinoRadio widgets wrapped in CupertinoListTiles. |
| /// |
| /// The currently selected character is passed into `groupValue`, which is |
| /// maintained by the example's `State`. In this case, the first [CupertinoRadio] |
| /// 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. |
| /// |
| /// ** See code in examples/api/lib/cupertino/radio/cupertino_radio.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [CupertinoSlider], for selecting a value in a range. |
| /// * [CupertinoCheckbox] and [CupertinoSwitch], for toggling a particular value on or off. |
| /// * [Radio], the Material Design equivalent. |
| /// * <https://developer.apple.com/design/human-interface-guidelines/components/selection-and-input/toggles/> |
| class CupertinoRadio<T> extends StatefulWidget { |
| /// Creates a macOS-styled 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 CupertinoRadio({ |
| super.key, |
| required this.value, |
| required this.groupValue, |
| required this.onChanged, |
| this.toggleable = false, |
| this.activeColor, |
| this.inactiveColor, |
| this.fillColor, |
| this.focusColor, |
| this.focusNode, |
| this.autofocus = false, |
| this.useCheckmarkStyle = false, |
| }); |
| |
| /// The value represented by this radio button. |
| /// |
| /// If this equals the [groupValue], then this radio button will appear |
| /// selected. |
| final T value; |
| |
| /// The currently selected value for a 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 [CupertinoRadio] button. |
| /// |
| /// The radio button passes [value] as a parameter to this callback. It does |
| /// not actually change state until the parent widget rebuilds the radio |
| /// button with a new [groupValue]. |
| /// |
| /// If null, the radio button will be displayed as disabled. |
| /// |
| /// The provided callback will not be invoked if this radio button is already |
| /// selected. |
| /// |
| /// 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 |
| /// CupertinoRadio<SingingCharacter>( |
| /// value: SingingCharacter.lafayette, |
| /// groupValue: _character, |
| /// onChanged: (SingingCharacter? newValue) { |
| /// setState(() { |
| /// _character = newValue; |
| /// }); |
| /// }, |
| /// ) |
| /// ``` |
| final ValueChanged<T?>? onChanged; |
| |
| /// Set to true if this radio button is allowed to be returned to an |
| /// indeterminate state by selecting it again when selected. |
| /// |
| /// To indicate returning to an indeterminate state, [onChanged] will be |
| /// called with null. |
| /// |
| /// If true, [onChanged] can be called with [value] when selected while |
| /// [groupValue] != [value], or with null when selected again while |
| /// [groupValue] == [value]. |
| /// |
| /// If false, [onChanged] will be called with [value] when it is selected |
| /// while [groupValue] != [value], and only by selecting another radio button |
| /// in the group (i.e. changing the value of [groupValue]) can this radio |
| /// button be unselected. |
| /// |
| /// The default is false. |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to enable deselecting a radio button by setting the |
| /// [toggleable] attribute. |
| /// |
| /// ** See code in examples/api/lib/cupertino/radio/cupertino_radio.toggleable.0.dart ** |
| /// {@end-tool} |
| final bool toggleable; |
| |
| /// Controls whether the radio displays in a checkbox style or the default iOS |
| /// radio style. |
| /// |
| /// Defaults to false. |
| final bool useCheckmarkStyle; |
| |
| /// The color to use when this radio button is selected. |
| /// |
| /// Defaults to [CupertinoColors.activeBlue]. |
| final Color? activeColor; |
| |
| /// The color to use when this radio button is not selected. |
| /// |
| /// Defaults to [CupertinoColors.white]. |
| final Color? inactiveColor; |
| |
| /// The color that fills the inner circle of the radio button when selected. |
| /// |
| /// Defaults to [CupertinoColors.white]. |
| final Color? fillColor; |
| |
| /// The color for the radio's border when it has the input focus. |
| /// |
| /// If null, then a paler form of the [activeColor] will be used. |
| final Color? focusColor; |
| |
| /// {@macro flutter.widgets.Focus.focusNode} |
| final FocusNode? focusNode; |
| |
| /// {@macro flutter.widgets.Focus.autofocus} |
| final bool autofocus; |
| |
| bool get _selected => value == groupValue; |
| |
| @override |
| State<CupertinoRadio<T>> createState() => _CupertinoRadioState<T>(); |
| } |
| |
| class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProviderStateMixin, ToggleableStateMixin { |
| final _RadioPainter _painter = _RadioPainter(); |
| |
| bool focused = false; |
| |
| void _handleChanged(bool? selected) { |
| if (selected == null) { |
| widget.onChanged!(null); |
| return; |
| } |
| if (selected) { |
| widget.onChanged!(widget.value); |
| } |
| } |
| |
| @override |
| void dispose() { |
| _painter.dispose(); |
| super.dispose(); |
| } |
| |
| @override |
| ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null; |
| |
| @override |
| bool get tristate => widget.toggleable; |
| |
| @override |
| bool? get value => widget._selected; |
| |
| void onFocusChange(bool value) { |
| if (focused != value) { |
| focused = value; |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final Color effectiveActiveColor = widget.activeColor |
| ?? CupertinoColors.activeBlue; |
| final Color effectiveInactiveColor = widget.inactiveColor |
| ?? CupertinoColors.white; |
| |
| final Color effectiveFocusOverlayColor = widget.focusColor |
| ?? HSLColor |
| .fromColor(effectiveActiveColor.withOpacity(_kCupertinoFocusColorOpacity)) |
| .withLightness(_kCupertinoFocusColorBrightness) |
| .withSaturation(_kCupertinoFocusColorSaturation) |
| .toColor(); |
| |
| final Color effectiveActivePressedOverlayColor = |
| HSLColor.fromColor(effectiveActiveColor).withLightness(0.45).toColor(); |
| |
| final Color effectiveFillColor = widget.fillColor ?? CupertinoColors.white; |
| |
| final bool? accessibilitySelected; |
| // Apple devices also use `selected` to annotate radio button's semantics |
| // state. |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| accessibilitySelected = null; |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| accessibilitySelected = widget._selected; |
| } |
| |
| return Semantics( |
| inMutuallyExclusiveGroup: true, |
| checked: widget._selected, |
| selected: accessibilitySelected, |
| child: buildToggleable( |
| focusNode: widget.focusNode, |
| autofocus: widget.autofocus, |
| onFocusChange: onFocusChange, |
| size: _size, |
| painter: _painter |
| ..focusColor = effectiveFocusOverlayColor |
| ..downPosition = downPosition |
| ..isFocused = focused |
| ..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor |
| ..inactiveColor = effectiveInactiveColor |
| ..fillColor = effectiveFillColor |
| ..value = value |
| ..checkmarkStyle = widget.useCheckmarkStyle, |
| ), |
| ); |
| } |
| } |
| |
| class _RadioPainter extends ToggleablePainter { |
| bool? get value => _value; |
| bool? _value; |
| set value(bool? value) { |
| if (_value == value) { |
| return; |
| } |
| _value = value; |
| notifyListeners(); |
| } |
| |
| Color get fillColor => _fillColor!; |
| Color? _fillColor; |
| set fillColor(Color value) { |
| if (value == _fillColor) { |
| return; |
| } |
| _fillColor = value; |
| notifyListeners(); |
| } |
| |
| bool get checkmarkStyle => _checkmarkStyle; |
| bool _checkmarkStyle = false; |
| set checkmarkStyle(bool value) { |
| if (value == _checkmarkStyle) { |
| return; |
| } |
| _checkmarkStyle = value; |
| notifyListeners(); |
| } |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| |
| final Offset center = (Offset.zero & size).center; |
| |
| final Paint paint = Paint() |
| ..color = inactiveColor |
| ..style = PaintingStyle.fill |
| ..strokeWidth = 0.1; |
| |
| if (checkmarkStyle) { |
| if (value ?? false) { |
| final Path path = Path(); |
| final Paint checkPaint = Paint() |
| ..color = activeColor |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 2 |
| ..strokeCap = StrokeCap.round; |
| final double width = _size.width; |
| final Offset origin = Offset(center.dx - (width/2), center.dy - (width/2)); |
| final Offset start = Offset(width * 0.25, width * 0.52); |
| final Offset mid = Offset(width * 0.46, width * 0.75); |
| final Offset end = Offset(width * 0.85, width * 0.29); |
| path.moveTo(origin.dx + start.dx, origin.dy + start.dy); |
| path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy); |
| canvas.drawPath(path, checkPaint); |
| path.moveTo(origin.dx + mid.dx, origin.dy + mid.dy); |
| path.lineTo(origin.dx + end.dx, origin.dy + end.dy); |
| canvas.drawPath(path, checkPaint); |
| } |
| } else { |
| // Outer border |
| canvas.drawCircle(center, _kOuterRadius, paint); |
| |
| paint.style = PaintingStyle.stroke; |
| paint.color = CupertinoColors.inactiveGray; |
| canvas.drawCircle(center, _kOuterRadius, paint); |
| |
| if (value ?? false) { |
| paint.style = PaintingStyle.fill; |
| paint.color = activeColor; |
| canvas.drawCircle(center, _kOuterRadius, paint); |
| paint.color = fillColor; |
| canvas.drawCircle(center, _kInnerRadius, paint); |
| } |
| } |
| |
| if (isFocused) { |
| paint.style = PaintingStyle.stroke; |
| paint.color = focusColor; |
| paint.strokeWidth = 3.0; |
| canvas.drawCircle(center, _kOuterRadius + 1.5, paint); |
| } |
| } |
| } |