Skip to content
This repository was archived by the owner on Feb 10, 2026. It is now read-only.

Commit 7f2149d

Browse files
authored
feat: [MDS-1593] Radio (#463)
Co-authored-by: GittHub-d <birgitt.majas@yolo.com>
1 parent a9b9ed3 commit 7f2149d

2 files changed

Lines changed: 107 additions & 130 deletions

File tree

example/lib/src/storybook/stories/primitives/radio.dart

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class _RadioStoryState extends State<RadioStory> {
2626
Widget build(BuildContext context) {
2727
final activeColorKnob = context.knobs.nullable.options(
2828
label: "activeColor",
29-
description: "MoonColors variants for checked MoonRadio.",
29+
description: "MoonColors variants for selected MoonRadio.",
3030
enabled: false,
3131
initial: 0,
3232
// piccolo
@@ -37,7 +37,7 @@ class _RadioStoryState extends State<RadioStory> {
3737

3838
final inactiveColorKnob = context.knobs.nullable.options(
3939
label: "inactiveColor",
40-
description: "MoonColors variants for unchecked MoonRadio.",
40+
description: "MoonColors variants for unselected MoonRadio.",
4141
enabled: false,
4242
initial: 0,
4343
// piccolo
@@ -82,29 +82,27 @@ class _RadioStoryState extends State<RadioStory> {
8282
const TextDivider(text: "MoonRadio with label"),
8383
...List.generate(
8484
2,
85-
(int index) => MoonMenuItem(
86-
absorbGestures: true,
87-
onTap: isDisabledKnob
88-
? null
89-
: () => setState(
90-
() {
91-
if (isToggleableKnob &&
92-
valueLabel == ChoiceLabel.values[index]) {
93-
valueLabel = null;
94-
} else {
95-
valueLabel = ChoiceLabel.values[index];
96-
}
97-
},
98-
),
99-
label: Text("With label #${index + 1}"),
100-
trailing: MoonRadio(
101-
value: ChoiceLabel.values[index],
102-
groupValue: valueLabel,
103-
toggleable: isToggleableKnob,
104-
tapAreaSizeValue: 0,
105-
onChanged: isDisabledKnob ? null : (_) {},
106-
),
107-
),
85+
(int index) {
86+
final ChoiceLabel value = ChoiceLabel.values[index];
87+
final shouldReset = isToggleableKnob && valueLabel == value;
88+
89+
return MoonMenuItem(
90+
absorbGestures: true,
91+
onTap: isDisabledKnob
92+
? null
93+
: () => setState(
94+
() => valueLabel = shouldReset ? null : value,
95+
),
96+
label: Text("With label #${index + 1}"),
97+
trailing: MoonRadio(
98+
value: value,
99+
groupValue: valueLabel,
100+
toggleable: isToggleableKnob,
101+
tapAreaSizeValue: 0,
102+
onChanged: isDisabledKnob ? null : (_) {},
103+
),
104+
);
105+
},
108106
),
109107
],
110108
),

lib/src/widgets/radio/radio.dart

Lines changed: 84 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import 'package:flutter/material.dart';
22

3+
import 'package:mix/mix.dart';
4+
import 'package:moon_core/moon_core.dart';
5+
36
import 'package:moon_design/src/theme/effects/effects_theme.dart';
7+
import 'package:moon_design/src/theme/effects/focus_effect.dart';
48
import 'package:moon_design/src/theme/theme.dart';
59
import 'package:moon_design/src/theme/tokens/opacities.dart';
610
import 'package:moon_design/src/theme/tokens/tokens.dart';
7-
import 'package:moon_design/src/utils/touch_target_padding.dart';
8-
import 'package:moon_design/src/widgets/common/effects/focus_effect.dart';
9-
import 'package:moon_design/src/widgets/radio/radio_painter.dart';
11+
1012
import 'package:moon_tokens/moon_tokens.dart';
1113

1214
class MoonRadio<T> extends StatefulWidget {
@@ -86,128 +88,105 @@ class MoonRadio<T> extends StatefulWidget {
8688
required this.onChanged,
8789
});
8890

89-
bool get _selected => value == groupValue;
90-
9191
@override
9292
State<MoonRadio<T>> createState() => _RadioState<T>();
9393
}
9494

95-
class _RadioState<T> extends State<MoonRadio<T>>
96-
with TickerProviderStateMixin, ToggleableStateMixin {
97-
final MoonRadioPainter _painter = MoonRadioPainter();
98-
99-
void _handleChanged(bool? selected) {
100-
if (selected == null) {
101-
widget.onChanged!(null);
102-
103-
return;
104-
}
105-
if (selected) {
106-
widget.onChanged!(widget.value);
107-
}
108-
}
95+
class _RadioState<T> extends State<MoonRadio<T>> {
96+
bool get _selected => widget.value == widget.groupValue;
97+
98+
ShapeDecorationWithPremultipliedAlpha _getFocusDecoration(
99+
double width,
100+
Color color,
101+
) =>
102+
ShapeDecorationWithPremultipliedAlpha(
103+
shape: CircleBorder(
104+
side: BorderSide(
105+
width: width,
106+
color: color,
107+
strokeAlign: BorderSide.strokeAlignOutside,
108+
),
109+
),
110+
);
109111

110112
@override
111-
void didUpdateWidget(MoonRadio<T> oldWidget) {
112-
super.didUpdateWidget(oldWidget);
113+
Widget build(BuildContext context) {
114+
const double sizeValue = 16;
115+
const double dotSizeValue = (sizeValue - 1) / 2;
113116

114-
if (widget._selected != oldWidget._selected) animateToValue();
115-
}
117+
final MoonFocusEffect focusEffect =
118+
MoonEffectsTheme(tokens: MoonTokens.light).controlFocusEffect;
116119

117-
@override
118-
void dispose() {
119-
_painter.dispose();
120+
final Color effectiveActiveColor =
121+
widget.activeColor ?? MoonColors.light.piccolo;
120122

121-
super.dispose();
122-
}
123+
final Color effectiveInactiveColor =
124+
widget.inactiveColor ?? MoonColors.light.trunks;
123125

124-
@override
125-
ValueChanged<bool?>? get onChanged =>
126-
widget.onChanged != null ? _handleChanged : null;
126+
final Color effectiveFocusEffectColor = focusEffect.effectColor;
127127

128-
@override
129-
bool get tristate => widget.toggleable;
128+
final double effectiveFocusEffectExtent = focusEffect.effectExtent;
130129

131-
@override
132-
bool? get value => widget._selected;
130+
final Duration effectiveFocusEffectDuration = focusEffect.effectDuration;
133131

134-
@override
135-
Widget build(BuildContext context) {
136-
const Size size = Size(16, 16);
137-
138-
final Color effectiveActiveColor = widget.activeColor ??
139-
context.moonTheme?.radioTheme.colors.activeColor ??
140-
MoonColors.light.piccolo;
141-
142-
final Color effectiveInactiveColor = widget.inactiveColor ??
143-
context.moonTheme?.radioTheme.colors.inactiveColor ??
144-
MoonColors.light.trunks;
145-
146-
final Color effectiveFocusEffectColor =
147-
context.moonEffects?.controlFocusEffect.effectColor ??
148-
MoonEffectsTheme(tokens: MoonTokens.light)
149-
.controlFocusEffect
150-
.effectColor;
151-
152-
final double effectiveFocusEffectExtent =
153-
context.moonEffects?.controlFocusEffect.effectExtent ??
154-
MoonEffectsTheme(tokens: MoonTokens.light)
155-
.controlFocusEffect
156-
.effectExtent;
157-
158-
final Duration effectiveFocusEffectDuration =
159-
context.moonEffects?.controlFocusEffect.effectDuration ??
160-
MoonEffectsTheme(tokens: MoonTokens.light)
161-
.controlFocusEffect
162-
.effectDuration;
163-
164-
final Curve effectiveFocusEffectCurve =
165-
context.moonEffects?.controlFocusEffect.effectCurve ??
166-
MoonEffectsTheme(tokens: MoonTokens.light)
167-
.controlFocusEffect
168-
.effectCurve;
132+
final Curve effectiveFocusEffectCurve = focusEffect.effectCurve;
169133

170134
final double effectiveDisabledOpacityValue =
171135
context.moonOpacities?.disabled ?? MoonOpacities.opacities.disabled;
172136

173-
final WidgetStateProperty<MouseCursor> effectiveMouseCursor =
174-
WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
175-
return WidgetStateMouseCursor.clickable.resolve(states);
176-
});
177-
178-
return Semantics(
179-
label: widget.semanticLabel,
180-
inMutuallyExclusiveGroup: true,
181-
checked: widget._selected,
182-
child: TouchTargetPadding(
183-
minSize: Size(widget.tapAreaSizeValue, widget.tapAreaSizeValue),
184-
child: MoonFocusEffect(
185-
show: states.contains(WidgetState.focused),
186-
effectExtent: effectiveFocusEffectExtent,
187-
childBorderRadius: BorderRadius.circular(8),
188-
effectColor: effectiveFocusEffectColor,
189-
effectCurve: effectiveFocusEffectCurve,
190-
effectDuration: effectiveFocusEffectDuration,
191-
child: RepaintBoundary(
192-
child: AnimatedOpacity(
193-
opacity: states.contains(WidgetState.disabled)
194-
? effectiveDisabledOpacityValue
195-
: 1,
196-
duration: effectiveFocusEffectDuration,
197-
child: buildToggleable(
198-
focusNode: widget.focusNode,
199-
autofocus: widget.autofocus,
200-
mouseCursor: effectiveMouseCursor,
201-
size: size,
202-
painter: _painter
203-
..position = position
204-
..activeColor = effectiveActiveColor
205-
..inactiveColor = effectiveInactiveColor,
206-
),
207-
),
137+
final Style dotStyle = Style(
138+
$box.chain
139+
..width(_selected ? dotSizeValue : 0)
140+
..height(_selected ? dotSizeValue : 0)
141+
..color(effectiveActiveColor)
142+
..shape.circle(),
143+
).animate(duration: effectiveFocusEffectDuration);
144+
145+
final Style baseStyle = Style(
146+
$box.chain
147+
..height(sizeValue)
148+
..width(sizeValue)
149+
..border
150+
.color(_selected ? effectiveActiveColor : effectiveInactiveColor)
151+
..alignment.center()
152+
..shape.circle(),
153+
).animate(duration: effectiveFocusEffectDuration);
154+
155+
final Style effectsStyle = Style(
156+
$box.shapeDecoration.as(_getFocusDecoration(0, Colors.transparent)),
157+
$with.animatedOpacity(
158+
opacity: widget.onChanged == null ? effectiveDisabledOpacityValue : 1,
159+
duration: effectiveFocusEffectDuration,
160+
),
161+
$on.focus(
162+
$box.shapeDecoration.as(
163+
_getFocusDecoration(
164+
effectiveFocusEffectExtent,
165+
effectiveFocusEffectColor,
208166
),
209167
),
210168
),
169+
).animate(
170+
duration: effectiveFocusEffectDuration,
171+
curve: effectiveFocusEffectCurve,
172+
);
173+
174+
return MoonBaseSingleSelectWidget(
175+
value: widget.value,
176+
groupValue: widget.groupValue,
177+
toggleable: widget.toggleable,
178+
focusNode: widget.focusNode,
179+
autofocus: widget.autofocus,
180+
semanticLabel: widget.semanticLabel,
181+
tapAreaSizeValue: widget.tapAreaSizeValue,
182+
style: effectsStyle,
183+
onChanged: widget.onChanged,
184+
child: Box(
185+
style: baseStyle,
186+
child: Box(
187+
style: dotStyle,
188+
),
189+
),
211190
);
212191
}
213192
}

0 commit comments

Comments
 (0)