1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6
7import 'basic.dart';
8import 'focus_manager.dart';
9import 'framework.dart';
10import 'radio_group.dart';
11import 'ticker_provider.dart';
12import 'toggleable.dart';
13import 'widget_state.dart';
14
15/// Signature for [RawRadio.builder].
16///
17/// The builder can use `state` to determine the state of the radio and build
18/// the visual.
19///
20/// {@macro flutter.widgets.ToggleableStateMixin.buildToggleableWithChild}
21typedef RadioBuilder = Widget Function(BuildContext context, ToggleableStateMixin state);
22
23/// A Radio button that provides basic radio functionalities.
24///
25/// Provide the `builder` to draw UI for radio.
26///
27/// {@macro flutter.widgets.ToggleableStateMixin.buildToggleableWithChild}
28///
29/// This widget allows selection between a number of mutually exclusive values.
30/// When one radio button in a group is selected, the other radio buttons in the
31/// group cease to be selected. The values are of type `T`, the type parameter
32/// of the radio class. Enums are commonly used for this purpose.
33///
34/// {@macro flutter.widget.RawRadio.groupValue}
35///
36/// If [enabled] is false, the radio will not be interactive.
37///
38/// See also:
39///
40/// * [Radio], which uses this widget to build a Material styled radio button.
41/// * [CupertinoRadio], which uses this widget to build a Cupertino styled
42/// radio button.
43class RawRadio<T> extends StatefulWidget {
44 /// Creates a radio button.
45 ///
46 /// If [enabled] is true, the [groupRegistry] must not be null.
47 const RawRadio({
48 super.key,
49 required this.value,
50 required this.mouseCursor,
51 required this.toggleable,
52 required this.focusNode,
53 required this.autofocus,
54 required this.groupRegistry,
55 required this.enabled,
56 required this.builder,
57 }) : assert(!enabled || groupRegistry != null, 'an enabled raw radio must have a registry');
58
59 /// {@template flutter.widget.RawRadio.value}
60 /// The value represented by this radio button.
61 /// {@endtemplate}
62 final T value;
63
64 /// {@template flutter.widget.RawRadio.mouseCursor}
65 /// The cursor for a mouse pointer when it enters or is hovering over the
66 /// widget.
67 ///
68 /// If [mouseCursor] is a [WidgetStateMouseCursor],
69 /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
70 ///
71 /// * [WidgetState.selected].
72 /// * [WidgetState.hovered].
73 /// * [WidgetState.focused].
74 /// * [WidgetState.disabled].
75 /// {@endtemplate}
76 final WidgetStateProperty<MouseCursor> mouseCursor;
77
78 /// {@template flutter.widget.RawRadio.toggleable}
79 /// Set to true if this radio button is allowed to be returned to an
80 /// indeterminate state by selecting it again when selected.
81 ///
82 /// To indicate returning to an indeterminate state, [RadioGroup.onChanged]
83 /// of the [RadioGroup] above the widget tree will be called with null.
84 ///
85 /// If true, [RadioGroup.onChanged] is called with [value] when selected while
86 /// [RadioGroup.groupValue] != [value], and with null when selected again while
87 /// [RadioGroup.groupValue] == [value].
88 ///
89 /// If false, [RadioGroup.onChanged] will be called with [value] when it is
90 /// selected while [RadioGroup.groupValue] != [value], and only by selecting
91 /// another radio button in the group (i.e. changing the value of
92 /// [RadioGroup.groupValue]) can this radio button be unselected.
93 ///
94 /// The default is false.
95 /// {@endtemplate}
96 final bool toggleable;
97
98 /// {@macro flutter.widgets.Focus.focusNode}
99 final FocusNode focusNode;
100
101 /// {@macro flutter.widgets.Focus.autofocus}
102 final bool autofocus;
103
104 /// The builder for the radio button visual.
105 ///
106 /// Use the input `state` to determine the current state of the radio.
107 ///
108 /// {@macro flutter.widgets.ToggleableStateMixin.buildToggleableWithChild}
109 final RadioBuilder builder;
110
111 /// Whether this widget is enabled.
112 final bool enabled;
113
114 /// {@template flutter.widget.RawRadio.groupRegistry}
115 /// The registry this radio registers to.
116 /// {@endtemplate}
117 ///
118 /// {@template flutter.widget.RawRadio.groupValue}
119 /// The radio relies on [groupRegistry] to maintains the state for selection.
120 /// If use in conjunction with a [RadioGroup] widget, use [RadioGroup.maybeOf]
121 /// to get the group registry from the context.
122 /// {@endtemplate}
123 final RadioGroupRegistry<T>? groupRegistry;
124
125 @override
126 State<RawRadio<T>> createState() => _RawRadioState<T>();
127}
128
129class _RawRadioState<T> extends State<RawRadio<T>>
130 with TickerProviderStateMixin, ToggleableStateMixin, RadioClient<T> {
131 @override
132 FocusNode get focusNode => widget.focusNode;
133
134 @override
135 T get radioValue => widget.value;
136
137 @override
138 void initState() {
139 // This has to be before the init state because the [ToggleableStateMixin]
140 // expect the [value] is up-to-date when init its state.
141 registry = widget.groupRegistry;
142 super.initState();
143 }
144
145 /// Handle selection status changed.
146 ///
147 /// if `selected` is false, nothing happens.
148 ///
149 /// if `selected` is true, select this radio. i.e. [Radio.onChanged] is called
150 /// with [Radio.value]. This also updates the group value in [RadioGroup] if it
151 /// is in use.
152 ///
153 /// if `selected` is null, unselect this radio. Same as `selected` is true
154 /// except group value is set to null.
155 void _handleChanged(bool? selected) {
156 assert(registry != null);
157 if (!(selected ?? true)) {
158 return;
159 }
160 if (selected ?? false) {
161 registry!.onChanged(widget.value);
162 } else {
163 registry!.onChanged(null);
164 }
165 }
166
167 @override
168 void didUpdateWidget(RawRadio<T> oldWidget) {
169 super.didUpdateWidget(oldWidget);
170 registry = widget.groupRegistry;
171 animateToValue(); // The registry's group value may have changed
172 }
173
174 @override
175 void dispose() {
176 super.dispose();
177 registry = null;
178 }
179
180 @override
181 ValueChanged<bool?>? get onChanged => registry != null ? _handleChanged : null;
182
183 @override
184 bool get tristate => widget.toggleable;
185
186 @override
187 bool? get value => widget.value == registry?.groupValue;
188
189 @override
190 bool get isInteractive => widget.enabled;
191
192 @override
193 Widget build(BuildContext context) {
194 final bool? accessibilitySelected;
195 switch (defaultTargetPlatform) {
196 case TargetPlatform.android:
197 case TargetPlatform.fuchsia:
198 case TargetPlatform.linux:
199 case TargetPlatform.windows:
200 accessibilitySelected = null;
201 case TargetPlatform.iOS:
202 case TargetPlatform.macOS:
203 accessibilitySelected = value;
204 }
205
206 return Semantics(
207 inMutuallyExclusiveGroup: true,
208 checked: value,
209 selected: accessibilitySelected,
210 child: buildToggleableWithChild(
211 focusNode: focusNode,
212 autofocus: widget.autofocus,
213 mouseCursor: widget.mouseCursor,
214 child: widget.builder(context, this),
215 ),
216 );
217 }
218}
219