blob: 9a8e70e67b3f8a43b15da3cd1d0ae9cb3992086a [file] [log] [blame] [edit]
// 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.
/// @docImport 'checkbox.dart';
/// @docImport 'list_tile.dart';
/// @docImport 'material.dart';
/// @docImport 'radio_list_tile.dart';
/// @docImport 'slider.dart';
/// @docImport 'switch.dart';
library;
import 'package:flutter/cupertino.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'material_state.dart';
import 'radio_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
// Examples can assume:
// late BuildContext context;
// enum SingingCharacter { lafayette }
// late SingingCharacter? _character;
// late StateSetter setState;
enum _RadioType { material, adaptive }
const double _kOuterRadius = 8.0;
const double _kInnerRadius = 4.5;
/// A Material Design radio button.
///
/// This widget builds a [RawRadio] with a material UI.
///
/// 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.
///
/// This widget typically has a [RadioGroup] ancestor, which takes in a
/// [RadioGroup.groupValue], and the [Radio] under it with matching [value]
/// will be selected.
///
/// {@tool dartpad}
/// Here is an example of Radio widgets wrapped in ListTiles, which is similar
/// to what you could get with the RadioListTile widget.
///
/// The currently selected character is passed into `RadioGroup.groupValue`,
/// which is maintained by the example's `State`. In this case, the first [Radio]
/// 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.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// ** See code in examples/api/lib/material/radio/radio.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// Here is an example of how the you can override the default theme of a
/// [Radio] with [WidgetStateProperty].
///
/// In this example:
/// - The first [Radio] uses a custom [fillColor] that changes depending on whether
/// the radio button is selected.
/// - The second [Radio] applies a different [backgroundColor] based on its selection state.
/// - The third [Radio] customizes the [side] property to display a different border color
/// when selected or unselected.
///
/// ** See code in examples/api/lib/material/radio/radio.1.dart **
/// {@end-tool}
///
/// 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.io/design/components/selection-controls.html#radio-buttons>
class Radio<T> extends StatefulWidget {
/// Creates a Material Design radio button.
///
/// This widget typically has a [RadioGroup] ancestor, which takes in a
/// [RadioGroup.groupValue], and the [Radio] under it with matching [value]
/// will be selected.
///
/// The [value] is required.
const Radio({
super.key,
required this.value,
@Deprecated(
'Use a RadioGroup ancestor to manage group value instead. '
'This feature was deprecated after v3.32.0-0.0.pre.',
)
this.groupValue,
@Deprecated(
'Use RadioGroup to handle value change instead. '
'This feature was deprecated after v3.32.0-0.0.pre.',
)
this.onChanged,
this.mouseCursor,
this.toggleable = false,
this.activeColor,
this.fillColor,
this.focusColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
this.autofocus = false,
this.enabled,
this.groupRegistry,
this.backgroundColor,
this.side,
this.innerRadius,
}) : _radioType = _RadioType.material,
useCupertinoCheckmarkStyle = false;
/// Creates an adaptive [Radio] based on whether the target platform is iOS
/// or macOS, following Material design's
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
///
/// On iOS and macOS, this constructor creates a [CupertinoRadio], which has
/// matching functionality and presentation as Material checkboxes, and are the
/// graphics expected on iOS. On other platforms, this creates a Material
/// design [Radio].
///
/// If a [CupertinoRadio] is created, the following parameters are ignored:
/// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius],
/// [materialTapTargetSize], [visualDensity].
///
/// [useCupertinoCheckmarkStyle] is used only if a [CupertinoRadio] is created.
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
const Radio.adaptive({
super.key,
required this.value,
@Deprecated(
'Use a RadioGroup ancestor to manage group value instead. '
'This feature was deprecated after v3.32.0-0.0.pre.',
)
this.groupValue,
@Deprecated(
'Use RadioGroup to handle value change instead. '
'This feature was deprecated after v3.32.0-0.0.pre.',
)
this.onChanged,
this.mouseCursor,
this.toggleable = false,
this.activeColor,
this.fillColor,
this.focusColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
this.autofocus = false,
this.useCupertinoCheckmarkStyle = false,
this.enabled,
this.groupRegistry,
this.backgroundColor,
this.side,
this.innerRadius,
}) : _radioType = _RadioType.adaptive;
/// {@macro flutter.widget.RawRadio.value}
final T value;
/// {@template flutter.material.Radio.groupValue}
/// The currently selected value for a group of radio buttons.
///
/// This radio button is considered selected if its [value] matches the
/// [groupValue].
///
/// This is deprecated, use [RadioGroup] to manage group value instead.
/// {@endtemplate}
@Deprecated(
'Use a RadioGroup ancestor to manage group value instead. '
'This feature was deprecated after v3.32.0-0.0.pre.',
)
final T? groupValue;
/// {@template flutter.material.Radio.onChanged}
/// 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 provided callback will not be invoked if this radio button is already
/// selected and [toggleable] is not set to true.
///
/// If the [toggleable] is set to true, tapping a already selected radio will
/// invoke this callback with `null` as value.
///
/// The callback provided to [onChanged] should update the state of the parent
/// [StatefulWidget] using the [State.setState] method, so that the parent
/// gets rebuilt.
/// {@endtemplate}
///
/// For example:
///
/// ```dart
/// Radio<SingingCharacter>(
/// value: SingingCharacter.lafayette,
/// groupValue: _character,
/// onChanged: (SingingCharacter? newValue) {
/// setState(() {
/// _character = newValue;
/// });
/// },
/// )
/// ```
///
/// This is deprecated, use [RadioGroup] to handle value change instead.
@Deprecated(
'Use RadioGroup to handle value change instead. '
'This feature was deprecated after v3.32.0-0.0.pre.',
)
final ValueChanged<T?>? onChanged;
/// {@macro flutter.widget.RawRadio.mouseCursor}
///
/// If null, then the value of [RadioThemeData.mouseCursor] is used.
/// If that is also null, then [WidgetStateMouseCursor.clickable] is used.
final MouseCursor? mouseCursor;
/// {@macro flutter.widget.RawRadio.toggleable}
///
/// {@tool dartpad}
/// This example shows how to enable deselecting a radio button by setting the
/// [toggleable] attribute.
///
/// ** See code in examples/api/lib/material/radio/radio.toggleable.0.dart **
/// {@end-tool}
final bool toggleable;
/// The color to use when this radio button is selected.
///
/// Defaults to [ColorScheme.secondary].
///
/// If [fillColor] returns a non-null color in the [WidgetState.selected]
/// state, it will be used instead of this color.
final Color? activeColor;
/// {@template flutter.material.radio.fillColor}
/// The color that fills the radio button, in all [WidgetState]s.
///
/// Resolves in the following states:
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
///
/// {@tool snippet}
/// This example resolves the [fillColor] based on the current [WidgetState]
/// of the [Radio], providing a different [Color] when it is
/// [WidgetState.disabled].
///
/// ```dart
/// Radio<int>(
/// value: 1,
/// fillColor: WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
/// if (states.contains(WidgetState.disabled)) {
/// return Colors.orange.withOpacity(.32);
/// }
/// return Colors.orange;
/// })
/// )
/// ```
/// {@end-tool}
/// {@endtemplate}
///
/// If null, then the value of [activeColor] is used in the selected state. If
/// that is also null, then the value of [RadioThemeData.fillColor] is used.
/// If that is also null and [ThemeData.useMaterial3] is false, then
/// [ThemeData.disabledColor] is used in the disabled state, [ColorScheme.secondary]
/// is used in the selected state, and [ThemeData.unselectedWidgetColor] is used in the
/// default state; if [ThemeData.useMaterial3] is true, then [ColorScheme.onSurface]
/// is used in the disabled state, [ColorScheme.primary] is used in the
/// selected state and [ColorScheme.onSurfaceVariant] is used in the default state.
final MaterialStateProperty<Color?>? fillColor;
/// {@template flutter.material.radio.materialTapTargetSize}
/// Configures the minimum size of the tap target.
/// {@endtemplate}
///
/// If null, then the value of [RadioThemeData.materialTapTargetSize] is used.
/// If that is also null, then the value of [ThemeData.materialTapTargetSize]
/// is used.
///
/// See also:
///
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
final MaterialTapTargetSize? materialTapTargetSize;
/// {@template flutter.material.radio.visualDensity}
/// Defines how compact the radio's layout will be.
/// {@endtemplate}
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// If null, then the value of [RadioThemeData.visualDensity] is used. If that
/// is also null, then the value of [ThemeData.visualDensity] is used.
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
/// widgets within a [Theme].
final VisualDensity? visualDensity;
/// The color for the radio's [Material] when it has the input focus.
///
/// If [overlayColor] returns a non-null color in the [WidgetState.focused]
/// state, it will be used instead.
///
/// If null, then the value of [RadioThemeData.overlayColor] is used in the
/// focused state. If that is also null, then the value of
/// [ThemeData.focusColor] is used.
final Color? focusColor;
/// {@template flutter.material.radio.hoverColor}
/// The color for the radio's [Material] when a pointer is hovering over it.
///
/// If [overlayColor] returns a non-null color in the [WidgetState.hovered]
/// state, it will be used instead.
/// {@endtemplate}
///
/// If null, then the value of [RadioThemeData.overlayColor] is used in the
/// hovered state. If that is also null, then the value of
/// [ThemeData.hoverColor] is used.
final Color? hoverColor;
/// {@template flutter.material.radio.overlayColor}
/// The color for the radio's [Material].
///
/// Resolves in the following states:
/// * [WidgetState.pressed].
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// {@endtemplate}
///
/// If null, then the value of [activeColor] with alpha
/// [kRadialReactionAlpha], [focusColor] and [hoverColor] is used in the
/// pressed, focused and hovered state. If that is also null,
/// the value of [RadioThemeData.overlayColor] is used. If that is also null,
/// then in Material 2, the value of [ColorScheme.secondary] with alpha
/// [kRadialReactionAlpha], [ThemeData.focusColor] and [ThemeData.hoverColor]
/// is used in the pressed, focused and hovered state. In Material3, the default
/// values are:
/// * selected
/// * pressed - Theme.colorScheme.onSurface(0.1)
/// * hovered - Theme.colorScheme.primary(0.08)
/// * focused - Theme.colorScheme.primary(0.1)
/// * pressed - Theme.colorScheme.primary(0.1)
/// * hovered - Theme.colorScheme.onSurface(0.08)
/// * focused - Theme.colorScheme.onSurface(0.1)
final MaterialStateProperty<Color?>? overlayColor;
/// {@template flutter.material.radio.splashRadius}
/// The splash radius of the circular [Material] ink response.
/// {@endtemplate}
///
/// If null, then the value of [RadioThemeData.splashRadius] is used. If that
/// is also null, then [kRadialReactionRadius] is used.
final double? splashRadius;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// Controls whether the checkmark style is used in an iOS-style radio.
///
/// Only usable under the [Radio.adaptive] constructor. If set to true, on
/// Apple platforms the radio button will appear as an iOS styled checkmark.
/// Controls the [CupertinoRadio] through [CupertinoRadio.useCheckmarkStyle].
///
/// Defaults to false.
final bool useCupertinoCheckmarkStyle;
/// {@macro flutter.widget.RawRadio.groupRegistry}
///
/// Unless provided, the [BuildContext] will be used to look up the ancestor
/// [RadioGroupRegistry].
final RadioGroupRegistry<T>? groupRegistry;
final _RadioType _radioType;
/// {@template flutter.material.Radio.enabled}
/// Whether this widget is interactive.
///
/// If not provided, this widget will be interactable if one of the following
/// is true:
///
/// * A [onChanged] is provided.
/// * Having a [RadioGroup] with the same type T above this widget.
/// * A [groupRegistry] is provided.
///
/// If this is set to true, one of the above condition must also be true.
/// Otherwise, an assertion error is thrown.
/// {@endtemplate}
final bool? enabled;
/// {@template flutter.material.Radio.backgroundColor}
/// The color of the background of the radio button, in all [WidgetState]s.
///
/// Resolves in the following states:
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
/// {@endtemplate}
///
/// If null, then [RadioThemeData.backgroundColor] of [ThemeData.radioTheme]
/// is used. If that is also null the default value is transparent in all
/// states.
final WidgetStateProperty<Color?>? backgroundColor;
/// {@template flutter.material.Radio.side}
/// The side for the circular border of the radio button, in all
/// [WidgetState]s.
///
/// This property can be a [BorderSide] or a [WidgetStateBorderSide] to leverage
/// widget state resolution.
///
/// Resolves in the following states:
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
/// {@endtemplate}
///
/// If null, then [RadioThemeData.side] of [ThemeData.radioTheme] is used. If
/// that is also null, the default value is a border using the fill color.
final BorderSide? side;
/// {@template flutter.material.Radio.innerRadius}
/// The radius of the inner circle of the radio button, in all [WidgetState]s.
///
/// Resolves in the following states:
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
/// {@endtemplate}
///
/// If null, then [RadioThemeData.innerRadius] of [ThemeData.radioTheme] is
/// used. If that is also null, the default value is `4.5` in all states.
final WidgetStateProperty<double?>? innerRadius;
@override
State<Radio<T>> createState() => _RadioState<T>();
}
class _RadioState<T> extends State<Radio<T>> {
FocusNode? _internalFocusNode;
FocusNode get _focusNode => widget.focusNode ?? (_internalFocusNode ??= FocusNode());
bool get _enabled =>
widget.enabled ??
(widget.onChanged != null ||
widget.groupRegistry != null ||
RadioGroup.maybeOf<T>(context) != null);
_RadioRegistry<T>? _internalRadioRegistry;
RadioGroupRegistry<T> get _effectiveRegistry {
if (widget.groupRegistry != null) {
return widget.groupRegistry!;
}
final RadioGroupRegistry<T>? inheritedRegistry = RadioGroup.maybeOf<T>(context);
if (inheritedRegistry != null) {
return inheritedRegistry;
}
// Handles deprecated API.
return _internalRadioRegistry ??= _RadioRegistry<T>(this);
}
@override
void dispose() {
_internalFocusNode?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(
!(widget.enabled ?? false) ||
widget.onChanged != null ||
widget.groupRegistry != null ||
RadioGroup.maybeOf<T>(context) != null,
'Radio is enabled but has no Radio.onChange or registry above',
);
assert(debugCheckHasMaterial(context));
switch (widget._radioType) {
case _RadioType.material:
break;
case _RadioType.adaptive:
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return CupertinoRadio<T>(
value: widget.value,
groupValue: widget.groupValue,
onChanged: widget.onChanged,
mouseCursor: widget.mouseCursor,
toggleable: widget.toggleable,
activeColor: widget.activeColor,
focusColor: widget.focusColor,
focusNode: _focusNode,
autofocus: widget.autofocus,
useCheckmarkStyle: widget.useCupertinoCheckmarkStyle,
groupRegistry: _effectiveRegistry,
enabled: _enabled,
);
}
}
final RadioThemeData radioTheme = RadioTheme.of(context);
final MaterialStateProperty<MouseCursor> effectiveMouseCursor =
MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ??
radioTheme.mouseCursor?.resolve(states) ??
MaterialStateProperty.resolveAs<MouseCursor>(
MaterialStateMouseCursor.clickable,
states,
);
});
return RawRadio<T>(
value: widget.value,
mouseCursor: effectiveMouseCursor,
toggleable: widget.toggleable,
focusNode: _focusNode,
autofocus: widget.autofocus,
groupRegistry: _effectiveRegistry,
enabled: _enabled,
builder: (BuildContext context, ToggleableStateMixin state) {
return _RadioPaint(
toggleableState: state,
activeColor: widget.activeColor,
fillColor: widget.fillColor,
hoverColor: widget.hoverColor,
focusColor: widget.focusColor,
overlayColor: widget.overlayColor,
splashRadius: widget.splashRadius,
visualDensity: widget.visualDensity,
materialTapTargetSize: widget.materialTapTargetSize,
backgroundColor: widget.backgroundColor,
side: widget.side,
innerRadius: widget.innerRadius,
);
},
);
}
}
/// A registry for deprecated API.
// TODO(chunhtai): Remove this once deprecated API is removed.
class _RadioRegistry<T> extends RadioGroupRegistry<T> {
_RadioRegistry(this.state);
final _RadioState<T> state;
@override
T? get groupValue => state.widget.groupValue;
@override
ValueChanged<T?> get onChanged => state.widget.onChanged!;
@override
void registerClient(RadioClient<T> radio) {}
@override
void unregisterClient(RadioClient<T> radio) {}
}
class _RadioPaint extends StatefulWidget {
const _RadioPaint({
required this.toggleableState,
required this.activeColor,
required this.fillColor,
required this.hoverColor,
required this.focusColor,
required this.overlayColor,
required this.splashRadius,
required this.visualDensity,
required this.materialTapTargetSize,
required this.backgroundColor,
required this.side,
required this.innerRadius,
});
final ToggleableStateMixin toggleableState;
final Color? activeColor;
final MaterialStateProperty<Color?>? fillColor;
final Color? hoverColor;
final Color? focusColor;
final MaterialStateProperty<Color?>? overlayColor;
final double? splashRadius;
final VisualDensity? visualDensity;
final MaterialTapTargetSize? materialTapTargetSize;
final WidgetStateProperty<Color?>? backgroundColor;
final BorderSide? side;
final WidgetStateProperty<double?>? innerRadius;
@override
State<StatefulWidget> createState() => _RadioPaintState();
}
class _RadioPaintState extends State<_RadioPaint> {
final _RadioPainter _painter = _RadioPainter();
@override
void dispose() {
_painter.dispose();
super.dispose();
}
MaterialStateProperty<Color?> get _widgetFillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return widget.activeColor;
}
return null;
});
}
BorderSide? _resolveSide(BorderSide? side, Set<MaterialState> states) {
if (side is WidgetStateProperty) {
return WidgetStateProperty.resolveAs<BorderSide?>(side, states);
}
if (!states.contains(WidgetState.selected)) {
return side;
}
return null;
}
@override
Widget build(BuildContext context) {
final RadioThemeData radioTheme = RadioTheme.of(context);
final RadioThemeData defaults = Theme.of(context).useMaterial3
? _RadioDefaultsM3(context)
: _RadioDefaultsM2(context);
// Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between.
final Set<MaterialState> activeStates = widget.toggleableState.states
..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = widget.toggleableState.states
..remove(MaterialState.selected);
final Color? activeColor =
widget.fillColor?.resolve(activeStates) ??
_widgetFillColor.resolve(activeStates) ??
radioTheme.fillColor?.resolve(activeStates);
final Color effectiveActiveColor = activeColor ?? defaults.fillColor!.resolve(activeStates)!;
final Color? inactiveColor =
widget.fillColor?.resolve(inactiveStates) ??
_widgetFillColor.resolve(inactiveStates) ??
radioTheme.fillColor?.resolve(inactiveStates);
final Color effectiveInactiveColor =
inactiveColor ?? defaults.fillColor!.resolve(inactiveStates)!;
final Color activeBackgroundColor =
widget.backgroundColor?.resolve(activeStates) ??
radioTheme.backgroundColor?.resolve(activeStates) ??
defaults.backgroundColor!.resolve(activeStates)!;
final Color inactiveBackgroundColor =
widget.backgroundColor?.resolve(inactiveStates) ??
radioTheme.backgroundColor?.resolve(inactiveStates) ??
defaults.backgroundColor!.resolve(inactiveStates)!;
final Set<MaterialState> focusedStates = widget.toggleableState.states
..add(MaterialState.focused);
Color effectiveFocusOverlayColor =
widget.overlayColor?.resolve(focusedStates) ??
widget.focusColor ??
radioTheme.overlayColor?.resolve(focusedStates) ??
defaults.overlayColor!.resolve(focusedStates)!;
final Set<MaterialState> hoveredStates = widget.toggleableState.states
..add(MaterialState.hovered);
Color effectiveHoverOverlayColor =
widget.overlayColor?.resolve(hoveredStates) ??
widget.hoverColor ??
radioTheme.overlayColor?.resolve(hoveredStates) ??
defaults.overlayColor!.resolve(hoveredStates)!;
final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed);
final Color effectiveActivePressedOverlayColor =
widget.overlayColor?.resolve(activePressedStates) ??
radioTheme.overlayColor?.resolve(activePressedStates) ??
activeColor?.withAlpha(kRadialReactionAlpha) ??
defaults.overlayColor!.resolve(activePressedStates)!;
final Set<MaterialState> inactivePressedStates = inactiveStates..add(MaterialState.pressed);
final Color effectiveInactivePressedOverlayColor =
widget.overlayColor?.resolve(inactivePressedStates) ??
radioTheme.overlayColor?.resolve(inactivePressedStates) ??
inactiveColor?.withAlpha(kRadialReactionAlpha) ??
defaults.overlayColor!.resolve(inactivePressedStates)!;
if (widget.toggleableState.downPosition != null) {
effectiveHoverOverlayColor = widget.toggleableState.states.contains(MaterialState.selected)
? effectiveActivePressedOverlayColor
: effectiveInactivePressedOverlayColor;
effectiveFocusOverlayColor = widget.toggleableState.states.contains(MaterialState.selected)
? effectiveActivePressedOverlayColor
: effectiveInactivePressedOverlayColor;
}
final MaterialTapTargetSize effectiveMaterialTapTargetSize =
widget.materialTapTargetSize ??
radioTheme.materialTapTargetSize ??
defaults.materialTapTargetSize!;
final VisualDensity effectiveVisualDensity =
widget.visualDensity ?? radioTheme.visualDensity ?? defaults.visualDensity!;
Size size = switch (effectiveMaterialTapTargetSize) {
MaterialTapTargetSize.padded => const Size(
kMinInteractiveDimension,
kMinInteractiveDimension,
),
MaterialTapTargetSize.shrinkWrap => const Size(
kMinInteractiveDimension - 8.0,
kMinInteractiveDimension - 8.0,
),
};
size += effectiveVisualDensity.baseSizeAdjustment;
final BorderSide activeSide =
_resolveSide(widget.side, activeStates) ??
_resolveSide(radioTheme.side, activeStates) ??
BorderSide(
color: effectiveActiveColor,
width: 2.0,
strokeAlign: BorderSide.strokeAlignCenter,
);
final BorderSide inactiveSide =
_resolveSide(widget.side, inactiveStates) ??
_resolveSide(radioTheme.side, inactiveStates) ??
BorderSide(
color: effectiveInactiveColor,
width: 2.0,
strokeAlign: BorderSide.strokeAlignCenter,
);
final double innerRadius =
widget.innerRadius?.resolve(activeStates) ??
radioTheme.innerRadius?.resolve(activeStates) ??
_kInnerRadius;
return CustomPaint(
size: size,
painter: _painter
..position = widget.toggleableState.position
..reaction = widget.toggleableState.reaction
..reactionFocusFade = widget.toggleableState.reactionFocusFade
..reactionHoverFade = widget.toggleableState.reactionHoverFade
..inactiveReactionColor = effectiveInactivePressedOverlayColor
..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor
..splashRadius = widget.splashRadius ?? radioTheme.splashRadius ?? kRadialReactionRadius
..downPosition = widget.toggleableState.downPosition
..isFocused = widget.toggleableState.states.contains(MaterialState.focused)
..isHovered = widget.toggleableState.states.contains(MaterialState.hovered)
..activeColor = effectiveActiveColor
..inactiveColor = effectiveInactiveColor
..activeBackgroundColor = activeBackgroundColor
..inactiveBackgroundColor = inactiveBackgroundColor
..activeSide = activeSide
..inactiveSide = inactiveSide
..innerRadius = innerRadius,
);
}
}
class _RadioPainter extends ToggleablePainter {
Color get inactiveBackgroundColor => _inactiveBackgroundColor!;
Color? _inactiveBackgroundColor;
set inactiveBackgroundColor(Color? value) {
if (_inactiveBackgroundColor == value) {
return;
}
_inactiveBackgroundColor = value;
notifyListeners();
}
Color get activeBackgroundColor => _activeBackgroundColor!;
Color? _activeBackgroundColor;
set activeBackgroundColor(Color? value) {
if (_activeBackgroundColor == value) {
return;
}
_activeBackgroundColor = value;
notifyListeners();
}
BorderSide get inactiveSide => _inactiveSide!;
BorderSide? _inactiveSide;
set inactiveSide(BorderSide? value) {
if (_inactiveSide == value) {
return;
}
_inactiveSide = value;
notifyListeners();
}
BorderSide get activeSide => _activeSide!;
BorderSide? _activeSide;
set activeSide(BorderSide? value) {
if (_activeSide == value) {
return;
}
_activeSide = value;
notifyListeners();
}
double get innerRadius => _innerRadius!;
double? _innerRadius;
set innerRadius(double? value) {
if (_innerRadius == value) {
return;
}
_innerRadius = value;
notifyListeners();
}
@override
void paint(Canvas canvas, Size size) {
paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));
final Rect rect = Offset.zero & size;
final Offset center = rect.center;
final Rect effectiveRect = (center & const Size.square(_kOuterRadius * 2)).translate(
-_kOuterRadius,
-_kOuterRadius,
);
// Background
final Paint backgroundPaint = Paint()
..color = Color.lerp(inactiveBackgroundColor, activeBackgroundColor, position.value)!
..style = PaintingStyle.fill;
canvas.drawCircle(center, _kOuterRadius, backgroundPaint);
// Outer circle
final BorderSide side = BorderSide.lerp(inactiveSide, activeSide, position.value);
CircleBorder(side: side).paint(canvas, effectiveRect);
// Inner circle
if (!position.isDismissed) {
final Paint innerCirclePaint = Paint()
..style = PaintingStyle.fill
..color = Color.lerp(inactiveColor, activeColor, position.value)!;
canvas.drawCircle(center, innerRadius * position.value, innerCirclePaint);
}
}
}
// Hand coded defaults based on Material Design 2.
class _RadioDefaultsM2 extends RadioThemeData {
_RadioDefaultsM2(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _theme.disabledColor;
}
if (states.contains(MaterialState.selected)) {
return _colors.secondary;
}
return _theme.unselectedWidgetColor;
});
}
@override
MaterialStateProperty<Color> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return fillColor.resolve(states).withAlpha(kRadialReactionAlpha);
}
if (states.contains(MaterialState.hovered)) {
return _theme.hoverColor;
}
if (states.contains(MaterialState.focused)) {
return _theme.focusColor;
}
return Colors.transparent;
});
}
@override
MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize;
@override
VisualDensity get visualDensity => _theme.visualDensity;
@override
WidgetStateProperty<Color> get backgroundColor =>
WidgetStateProperty.all<Color>(Colors.transparent);
}
// BEGIN GENERATED TOKEN PROPERTIES - Radio<T>
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
class _RadioDefaultsM3 extends RadioThemeData {
_RadioDefaultsM3(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
if (states.contains(MaterialState.pressed)) {
return _colors.primary;
}
if (states.contains(MaterialState.hovered)) {
return _colors.primary;
}
if (states.contains(MaterialState.focused)) {
return _colors.primary;
}
return _colors.primary;
}
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface;
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface;
}
return _colors.onSurfaceVariant;
});
}
@override
MaterialStateProperty<Color> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.primary.withOpacity(0.1);
}
return Colors.transparent;
}
if (states.contains(MaterialState.pressed)) {
return _colors.primary.withOpacity(0.1);
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface.withOpacity(0.1);
}
return Colors.transparent;
});
}
@override
MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize;
@override
VisualDensity get visualDensity => _theme.visualDensity;
@override
WidgetStateProperty<Color> get backgroundColor =>
WidgetStateProperty.all<Color>(Colors.transparent);
}
// dart format on
// END GENERATED TOKEN PROPERTIES - Radio<T>