Skip to content

Commit 678f40c

Browse files
Add checkmark style to CupertinoRadio (#126480)
Fixes: #102813 Adds a checkmark style to the Cupertino Radio. Also allows the Radio.adaptive and RadioListTile.adaptive widgets to control whether they use the checkmark style for their Cupertino widgets or not. This is how it looks in action: https://github.com/flutter/flutter/assets/58190796/b409b270-42dd-404a-9350-d2c3e1d7fa4e
1 parent 3f01c7e commit 678f40c

File tree

4 files changed

+142
-17
lines changed

4 files changed

+142
-17
lines changed

packages/flutter/lib/src/cupertino/radio.dart

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class CupertinoRadio<T> extends StatefulWidget {
7979
this.focusColor,
8080
this.focusNode,
8181
this.autofocus = false,
82+
this.useCheckmarkStyle = false,
8283
});
8384

8485
/// The value represented by this radio button.
@@ -146,6 +147,12 @@ class CupertinoRadio<T> extends StatefulWidget {
146147
/// {@end-tool}
147148
final bool toggleable;
148149

150+
/// Controls whether the radio displays in a checkbox style or the default iOS
151+
/// radio style.
152+
///
153+
/// Defaults to false.
154+
final bool useCheckmarkStyle;
155+
149156
/// The color to use when this radio button is selected.
150157
///
151158
/// Defaults to [CupertinoColors.activeBlue].
@@ -263,7 +270,8 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
263270
..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor
264271
..inactiveColor = effectiveInactiveColor
265272
..fillColor = effectiveFillColor
266-
..value = value,
273+
..value = value
274+
..checkmarkStyle = widget.useCheckmarkStyle,
267275
),
268276
);
269277
}
@@ -290,28 +298,61 @@ class _RadioPainter extends ToggleablePainter {
290298
notifyListeners();
291299
}
292300

301+
bool get checkmarkStyle => _checkmarkStyle;
302+
bool _checkmarkStyle = false;
303+
set checkmarkStyle(bool value) {
304+
if (value == _checkmarkStyle) {
305+
return;
306+
}
307+
_checkmarkStyle = value;
308+
notifyListeners();
309+
}
310+
293311
@override
294312
void paint(Canvas canvas, Size size) {
295313

296314
final Offset center = (Offset.zero & size).center;
297315

298-
// Outer border
299316
final Paint paint = Paint()
300-
..color = inactiveColor
301-
..style = PaintingStyle.fill
302-
..strokeWidth = 0.1;
303-
canvas.drawCircle(center, _kOuterRadius, paint);
304-
305-
paint.style = PaintingStyle.stroke;
306-
paint.color = CupertinoColors.inactiveGray;
307-
canvas.drawCircle(center, _kOuterRadius, paint);
308-
309-
if (value ?? false) {
310-
paint.style = PaintingStyle.fill;
311-
paint.color = activeColor;
317+
..color = inactiveColor
318+
..style = PaintingStyle.fill
319+
..strokeWidth = 0.1;
320+
321+
if (checkmarkStyle) {
322+
if (value ?? false) {
323+
final Path path = Path();
324+
final Paint checkPaint = Paint()
325+
..color = activeColor
326+
..style = PaintingStyle.stroke
327+
..strokeWidth = 2
328+
..strokeCap = StrokeCap.round;
329+
final double width = _size.width;
330+
final Offset origin = Offset(center.dx - (width/2), center.dy - (width/2));
331+
final Offset start = Offset(width * 0.25, width * 0.52);
332+
final Offset mid = Offset(width * 0.46, width * 0.75);
333+
final Offset end = Offset(width * 0.85, width * 0.29);
334+
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
335+
path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
336+
canvas.drawPath(path, checkPaint);
337+
path.moveTo(origin.dx + mid.dx, origin.dy + mid.dy);
338+
path.lineTo(origin.dx + end.dx, origin.dy + end.dy);
339+
canvas.drawPath(path, checkPaint);
340+
}
341+
} else {
342+
// Outer border
312343
canvas.drawCircle(center, _kOuterRadius, paint);
313-
paint.color = fillColor;
314-
canvas.drawCircle(center, _kInnerRadius, paint);
344+
345+
paint.style = PaintingStyle.stroke;
346+
paint.color = CupertinoColors.inactiveGray;
347+
canvas.drawCircle(center, _kOuterRadius, paint);
348+
349+
if (value ?? false) {
350+
paint.style = PaintingStyle.fill;
351+
paint.color = activeColor;
352+
canvas.drawCircle(center, _kOuterRadius, paint);
353+
paint.color = fillColor;
354+
canvas.drawCircle(center, _kInnerRadius, paint);
355+
}
315356
}
316357

317358
if (isFocused) {

packages/flutter/lib/src/material/radio.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ class Radio<T> extends StatefulWidget {
9696
this.visualDensity,
9797
this.focusNode,
9898
this.autofocus = false,
99-
}) : _radioType = _RadioType.material;
99+
}) : _radioType = _RadioType.material,
100+
useCupertinoCheckmarkStyle = false;
100101

101102
/// Creates an adaptive [Radio] based on whether the target platform is iOS
102103
/// or macOS, following Material design's
@@ -111,6 +112,8 @@ class Radio<T> extends StatefulWidget {
111112
/// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius],
112113
/// [materialTapTargetSize], [visualDensity].
113114
///
115+
/// [useCupertinoCheckmarkStyle] is used only if a [CupertinoRadio] is created.
116+
///
114117
/// The target platform is based on the current [Theme]: [ThemeData.platform].
115118
const Radio.adaptive({
116119
super.key,
@@ -129,6 +132,7 @@ class Radio<T> extends StatefulWidget {
129132
this.visualDensity,
130133
this.focusNode,
131134
this.autofocus = false,
135+
this.useCupertinoCheckmarkStyle = false
132136
}) : _radioType = _RadioType.adaptive;
133137

134138
/// The value represented by this radio button.
@@ -345,6 +349,15 @@ class Radio<T> extends StatefulWidget {
345349
/// {@macro flutter.widgets.Focus.autofocus}
346350
final bool autofocus;
347351

352+
/// Controls whether the checkmark style is used in an iOS-style radio.
353+
///
354+
/// Only usable under the [Radio.adaptive] constructor. If set to true, on
355+
/// Apple platforms the radio button will appear as an iOS styled checkmark.
356+
/// Controls the [CupertinoRadio] through [CupertinoRadio.useCheckmarkStyle].
357+
///
358+
/// Defaults to false.
359+
final bool useCupertinoCheckmarkStyle;
360+
348361
final _RadioType _radioType;
349362

350363
bool get _selected => value == groupValue;
@@ -427,6 +440,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
427440
focusColor: widget.focusColor,
428441
focusNode: widget.focusNode,
429442
autofocus: widget.autofocus,
443+
useCheckmarkStyle: widget.useCupertinoCheckmarkStyle,
430444
);
431445
}
432446
}

packages/flutter/lib/src/material/radio_list_tile.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ class RadioListTile<T> extends StatelessWidget {
189189
this.onFocusChange,
190190
this.enableFeedback,
191191
}) : _radioType = _RadioType.material,
192+
useCupertinoCheckmarkStyle = false,
192193
assert(!isThreeLine || subtitle != null);
193194

194195
/// Creates a combination of a list tile and a platform adaptive radio.
@@ -226,6 +227,7 @@ class RadioListTile<T> extends StatelessWidget {
226227
this.focusNode,
227228
this.onFocusChange,
228229
this.enableFeedback,
230+
this.useCupertinoCheckmarkStyle = false,
229231
}) : _radioType = _RadioType.adaptive,
230232
assert(!isThreeLine || subtitle != null);
231233

@@ -435,6 +437,17 @@ class RadioListTile<T> extends StatelessWidget {
435437

436438
final _RadioType _radioType;
437439

440+
/// Determines wether or not to use the checkbox style for the [CupertinoRadio]
441+
/// control.
442+
///
443+
/// Only usable under the [RadioListTile.adaptive] constructor. If set to
444+
/// true, on Apple platforms the radio button will appear as an iOS styled
445+
/// checkmark. Controls the [CupertinoRadio] through
446+
/// [CupertinoRadio.useCheckmarkStyle].
447+
///
448+
/// Defaults to false.
449+
final bool useCupertinoCheckmarkStyle;
450+
438451
@override
439452
Widget build(BuildContext context) {
440453
final Widget control;
@@ -468,6 +481,7 @@ class RadioListTile<T> extends StatelessWidget {
468481
hoverColor: hoverColor,
469482
overlayColor: overlayColor,
470483
splashRadius: splashRadius,
484+
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
471485
);
472486
}
473487

packages/flutter/test/cupertino/radio_test.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
88
import 'package:flutter/services.dart';
99
import 'package:flutter_test/flutter_test.dart';
1010

11+
import '../rendering/mock_canvas.dart';
1112
import '../widgets/semantics_tester.dart';
1213

1314
void main() {
@@ -350,6 +351,61 @@ void main() {
350351
expect(groupValue, equals(2));
351352
});
352353

354+
testWidgets('Show a checkmark when useCheckmarkStyle is true', (WidgetTester tester) async {
355+
await tester.pumpWidget(CupertinoApp(
356+
home: Center(
357+
child: CupertinoRadio<int>(
358+
value: 1,
359+
groupValue: 1,
360+
onChanged: (int? i) { },
361+
),
362+
),
363+
));
364+
await tester.pumpAndSettle();
365+
366+
// Has no checkmark when useCheckmarkStyle is false
367+
expect(
368+
tester.firstRenderObject<RenderBox>(find.byType(CupertinoRadio<int>)),
369+
isNot(paints..path())
370+
);
371+
372+
await tester.pumpWidget(CupertinoApp(
373+
home: Center(
374+
child: CupertinoRadio<int>(
375+
value: 1,
376+
groupValue: 2,
377+
useCheckmarkStyle: true,
378+
onChanged: (int? i) { },
379+
),
380+
),
381+
));
382+
await tester.pumpAndSettle();
383+
384+
// Has no checkmark when group value doesn't match the value
385+
expect(
386+
tester.firstRenderObject<RenderBox>(find.byType(CupertinoRadio<int>)),
387+
isNot(paints..path())
388+
);
389+
390+
await tester.pumpWidget(CupertinoApp(
391+
home: Center(
392+
child: CupertinoRadio<int>(
393+
value: 1,
394+
groupValue: 1,
395+
useCheckmarkStyle: true,
396+
onChanged: (int? i) { },
397+
),
398+
),
399+
));
400+
await tester.pumpAndSettle();
401+
402+
// Draws a path to show the checkmark when toggled on
403+
expect(
404+
tester.firstRenderObject<RenderBox>(find.byType(CupertinoRadio<int>)),
405+
paints..path()
406+
);
407+
});
408+
353409
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
354410
final Key key = UniqueKey();
355411

0 commit comments

Comments
 (0)