| 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 | |
| 5 | import 'package:flutter/foundation.dart'; |
| 6 | |
| 7 | import 'basic.dart'; |
| 8 | import 'focus_manager.dart'; |
| 9 | import 'framework.dart'; |
| 10 | import 'radio_group.dart'; |
| 11 | import 'ticker_provider.dart'; |
| 12 | import 'toggleable.dart'; |
| 13 | import '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} |
| 21 | typedef 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. |
| 43 | class 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 | |
| 129 | class _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 | |