blob: 27439fddd630c4ccff7ce8c1e1c71d8050b96f7f [file] [log] [blame]
// 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 'basic.dart';
import 'focus_manager.dart';
import 'framework.dart';
import 'localizations.dart';
import 'radio_group.dart';
import 'ticker_provider.dart';
import 'toggleable.dart';
import 'widget_state.dart';
/// Signature for [RawRadio.builder].
///
/// The builder can use `state` to determine the state of the radio and build
/// the visual.
///
/// {@macro flutter.widgets.ToggleableStateMixin.buildToggleableWithChild}
typedef RadioBuilder = Widget Function(BuildContext context, ToggleableStateMixin state);
/// A Radio button that provides basic radio functionalities.
///
/// Provide the `builder` to draw UI for radio.
///
/// {@macro flutter.widgets.ToggleableStateMixin.buildToggleableWithChild}
///
/// This widget allows selection 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.
///
/// {@macro flutter.widget.RawRadio.groupValue}
///
/// If [enabled] is false, the radio will not be interactive.
///
/// See also:
///
/// * [Radio], which uses this widget to build a Material styled radio button.
/// * [CupertinoRadio], which uses this widget to build a Cupertino styled
/// radio button.
class RawRadio<T> extends StatefulWidget {
/// Creates a radio button.
///
/// If [enabled] is true, the [groupRegistry] must not be null.
const RawRadio({
super.key,
required this.value,
required this.mouseCursor,
required this.toggleable,
required this.focusNode,
required this.autofocus,
required this.groupRegistry,
required this.enabled,
required this.builder,
}) : assert(!enabled || groupRegistry != null, 'an enabled raw radio must have a registry');
/// {@template flutter.widget.RawRadio.value}
/// The value represented by this radio button.
/// {@endtemplate}
final T value;
/// {@template flutter.widget.RawRadio.mouseCursor}
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [WidgetStateMouseCursor],
/// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
///
/// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
/// {@endtemplate}
final WidgetStateProperty<MouseCursor> mouseCursor;
/// {@template flutter.widget.RawRadio.toggleable}
/// 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, [RadioGroup.onChanged]
/// of the [RadioGroup] above the widget tree will be called with null.
///
/// If true, [RadioGroup.onChanged] is called with [value] when selected while
/// [RadioGroup.groupValue] != [value], and with null when selected again while
/// [RadioGroup.groupValue] == [value].
///
/// If false, [RadioGroup.onChanged] will be called with [value] when it is
/// selected while [RadioGroup.groupValue] != [value], and only by selecting
/// another radio button in the group (i.e. changing the value of
/// [RadioGroup.groupValue]) can this radio button be unselected.
///
/// The default is false.
/// {@endtemplate}
final bool toggleable;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// The builder for the radio button visual.
///
/// Use the input `state` to determine the current state of the radio.
///
/// {@macro flutter.widgets.ToggleableStateMixin.buildToggleableWithChild}
final RadioBuilder builder;
/// Whether this widget is enabled.
final bool enabled;
/// {@template flutter.widget.RawRadio.groupRegistry}
/// The registry this radio registers to.
/// {@endtemplate}
///
/// {@template flutter.widget.RawRadio.groupValue}
/// The radio relies on [groupRegistry] to maintains the state for selection.
/// If use in conjunction with a [RadioGroup] widget, use [RadioGroup.maybeOf]
/// to get the group registry from the context.
/// {@endtemplate}
final RadioGroupRegistry<T>? groupRegistry;
@override
State<RawRadio<T>> createState() => _RawRadioState<T>();
}
class _RawRadioState<T> extends State<RawRadio<T>>
with TickerProviderStateMixin, ToggleableStateMixin, RadioClient<T> {
@override
FocusNode get focusNode => widget.focusNode;
@override
bool get enabled => isInteractive;
@override
T get radioValue => widget.value;
@override
void initState() {
// This has to be before the init state because the [ToggleableStateMixin]
// expect the [value] is up-to-date when init its state.
registry = widget.groupRegistry;
super.initState();
}
/// Handle selection status changed.
///
/// if `selected` is false, nothing happens.
///
/// if `selected` is true, select this radio. i.e. [Radio.onChanged] is called
/// with [Radio.value]. This also updates the group value in [RadioGroup] if it
/// is in use.
///
/// if `selected` is null, unselect this radio. Same as `selected` is true
/// except group value is set to null.
void _handleChanged(bool? selected) {
assert(registry != null);
if (!(selected ?? true)) {
return;
}
if (selected ?? false) {
registry!.onChanged(widget.value);
} else {
registry!.onChanged(null);
}
}
@override
void didUpdateWidget(RawRadio<T> oldWidget) {
super.didUpdateWidget(oldWidget);
registry = widget.groupRegistry;
animateToValue(); // The registry's group value may have changed
}
@override
void dispose() {
super.dispose();
registry = null;
}
@override
ValueChanged<bool?>? get onChanged => registry != null ? _handleChanged : null;
@override
bool get tristate => widget.toggleable;
@override
bool? get value => widget.value == registry?.groupValue;
@override
bool get isInteractive => widget.enabled;
@override
Widget build(BuildContext context) {
final bool? accessibilitySelected;
String? semanticsHint;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
accessibilitySelected = null;
semanticsHint = null;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
accessibilitySelected = value;
// Only provide hint for unselected radio buttons to avoid duplication
// of the selected state announcement.
// Selected state is already announced by iOS via the 'selected' property.
if (!(value ?? false)) {
final WidgetsLocalizations localizations = WidgetsLocalizations.of(context);
semanticsHint = localizations.radioButtonUnselectedLabel;
}
}
return Semantics(
inMutuallyExclusiveGroup: true,
checked: value,
selected: accessibilitySelected,
hint: semanticsHint,
child: buildToggleableWithChild(
focusNode: focusNode,
autofocus: widget.autofocus,
mouseCursor: widget.mouseCursor,
child: widget.builder(context, this),
),
);
}
}