// 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 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; /// {@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? groupRegistry; @override State> createState() => _RawRadioState(); } class _RawRadioState extends State> with TickerProviderStateMixin, ToggleableStateMixin, RadioClient { @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 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? 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), ), ); } }