diff --git a/packages/flutter/lib/src/cupertino/radio.dart b/packages/flutter/lib/src/cupertino/radio.dart index b10c618981c..5222e51fe64 100644 --- a/packages/flutter/lib/src/cupertino/radio.dart +++ b/packages/flutter/lib/src/cupertino/radio.dart @@ -79,6 +79,7 @@ class CupertinoRadio extends StatefulWidget { this.focusColor, this.focusNode, this.autofocus = false, + this.useCheckmarkStyle = false, }); /// The value represented by this radio button. @@ -146,6 +147,12 @@ class CupertinoRadio extends StatefulWidget { /// {@end-tool} final bool toggleable; + /// Controls whether the radio displays in a checkbox style or the default iOS + /// radio style. + /// + /// Defaults to false. + final bool useCheckmarkStyle; + /// The color to use when this radio button is selected. /// /// Defaults to [CupertinoColors.activeBlue]. @@ -263,7 +270,8 @@ class _CupertinoRadioState extends State> with TickerProvid ..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor ..inactiveColor = effectiveInactiveColor ..fillColor = effectiveFillColor - ..value = value, + ..value = value + ..checkmarkStyle = widget.useCheckmarkStyle, ), ); } @@ -290,28 +298,61 @@ class _RadioPainter extends ToggleablePainter { notifyListeners(); } + bool get checkmarkStyle => _checkmarkStyle; + bool _checkmarkStyle = false; + set checkmarkStyle(bool value) { + if (value == _checkmarkStyle) { + return; + } + _checkmarkStyle = value; + notifyListeners(); + } + @override void paint(Canvas canvas, Size size) { final Offset center = (Offset.zero & size).center; - // Outer border final Paint paint = Paint() - ..color = inactiveColor - ..style = PaintingStyle.fill - ..strokeWidth = 0.1; - canvas.drawCircle(center, _kOuterRadius, paint); + ..color = inactiveColor + ..style = PaintingStyle.fill + ..strokeWidth = 0.1; - paint.style = PaintingStyle.stroke; - paint.color = CupertinoColors.inactiveGray; - canvas.drawCircle(center, _kOuterRadius, paint); - - if (value ?? false) { - paint.style = PaintingStyle.fill; - paint.color = activeColor; + if (checkmarkStyle) { + if (value ?? false) { + final Path path = Path(); + final Paint checkPaint = Paint() + ..color = activeColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2 + ..strokeCap = StrokeCap.round; + final double width = _size.width; + final Offset origin = Offset(center.dx - (width/2), center.dy - (width/2)); + final Offset start = Offset(width * 0.25, width * 0.52); + final Offset mid = Offset(width * 0.46, width * 0.75); + final Offset end = Offset(width * 0.85, width * 0.29); + path.moveTo(origin.dx + start.dx, origin.dy + start.dy); + path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy); + canvas.drawPath(path, checkPaint); + path.moveTo(origin.dx + mid.dx, origin.dy + mid.dy); + path.lineTo(origin.dx + end.dx, origin.dy + end.dy); + canvas.drawPath(path, checkPaint); + } + } else { + // Outer border canvas.drawCircle(center, _kOuterRadius, paint); - paint.color = fillColor; - canvas.drawCircle(center, _kInnerRadius, paint); + + paint.style = PaintingStyle.stroke; + paint.color = CupertinoColors.inactiveGray; + canvas.drawCircle(center, _kOuterRadius, paint); + + if (value ?? false) { + paint.style = PaintingStyle.fill; + paint.color = activeColor; + canvas.drawCircle(center, _kOuterRadius, paint); + paint.color = fillColor; + canvas.drawCircle(center, _kInnerRadius, paint); + } } if (isFocused) { diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index 0716763e549..380037181c2 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -96,7 +96,8 @@ class Radio extends StatefulWidget { this.visualDensity, this.focusNode, this.autofocus = false, - }) : _radioType = _RadioType.material; + }) : _radioType = _RadioType.material, + useCupertinoCheckmarkStyle = false; /// Creates an adaptive [Radio] based on whether the target platform is iOS /// or macOS, following Material design's @@ -111,6 +112,8 @@ class Radio extends StatefulWidget { /// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius], /// [materialTapTargetSize], [visualDensity]. /// + /// [useCupertinoCheckmarkStyle] is used only if a [CupertinoRadio] is created. + /// /// The target platform is based on the current [Theme]: [ThemeData.platform]. const Radio.adaptive({ super.key, @@ -129,6 +132,7 @@ class Radio extends StatefulWidget { this.visualDensity, this.focusNode, this.autofocus = false, + this.useCupertinoCheckmarkStyle = false }) : _radioType = _RadioType.adaptive; /// The value represented by this radio button. @@ -345,6 +349,15 @@ class Radio extends StatefulWidget { /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; + /// Controls whether the checkmark style is used in an iOS-style radio. + /// + /// Only usable under the [Radio.adaptive] constructor. If set to true, on + /// Apple platforms the radio button will appear as an iOS styled checkmark. + /// Controls the [CupertinoRadio] through [CupertinoRadio.useCheckmarkStyle]. + /// + /// Defaults to false. + final bool useCupertinoCheckmarkStyle; + final _RadioType _radioType; bool get _selected => value == groupValue; @@ -427,6 +440,7 @@ class _RadioState extends State> with TickerProviderStateMixin, Togg focusColor: widget.focusColor, focusNode: widget.focusNode, autofocus: widget.autofocus, + useCheckmarkStyle: widget.useCupertinoCheckmarkStyle, ); } } diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index 62a80c46d1a..0f354dced4d 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -189,6 +189,7 @@ class RadioListTile extends StatelessWidget { this.onFocusChange, this.enableFeedback, }) : _radioType = _RadioType.material, + useCupertinoCheckmarkStyle = false, assert(!isThreeLine || subtitle != null); /// Creates a combination of a list tile and a platform adaptive radio. @@ -226,6 +227,7 @@ class RadioListTile extends StatelessWidget { this.focusNode, this.onFocusChange, this.enableFeedback, + this.useCupertinoCheckmarkStyle = false, }) : _radioType = _RadioType.adaptive, assert(!isThreeLine || subtitle != null); @@ -435,6 +437,17 @@ class RadioListTile extends StatelessWidget { final _RadioType _radioType; + /// Determines wether or not to use the checkbox style for the [CupertinoRadio] + /// control. + /// + /// Only usable under the [RadioListTile.adaptive] constructor. If set to + /// true, on Apple platforms the radio button will appear as an iOS styled + /// checkmark. Controls the [CupertinoRadio] through + /// [CupertinoRadio.useCheckmarkStyle]. + /// + /// Defaults to false. + final bool useCupertinoCheckmarkStyle; + @override Widget build(BuildContext context) { final Widget control; @@ -468,6 +481,7 @@ class RadioListTile extends StatelessWidget { hoverColor: hoverColor, overlayColor: overlayColor, splashRadius: splashRadius, + useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle, ); } diff --git a/packages/flutter/test/cupertino/radio_test.dart b/packages/flutter/test/cupertino/radio_test.dart index e1dd7565caf..343d97b5259 100644 --- a/packages/flutter/test/cupertino/radio_test.dart +++ b/packages/flutter/test/cupertino/radio_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -350,6 +351,61 @@ void main() { expect(groupValue, equals(2)); }); + testWidgets('Show a checkmark when useCheckmarkStyle is true', (WidgetTester tester) async { + await tester.pumpWidget(CupertinoApp( + home: Center( + child: CupertinoRadio( + value: 1, + groupValue: 1, + onChanged: (int? i) { }, + ), + ), + )); + await tester.pumpAndSettle(); + + // Has no checkmark when useCheckmarkStyle is false + expect( + tester.firstRenderObject(find.byType(CupertinoRadio)), + isNot(paints..path()) + ); + + await tester.pumpWidget(CupertinoApp( + home: Center( + child: CupertinoRadio( + value: 1, + groupValue: 2, + useCheckmarkStyle: true, + onChanged: (int? i) { }, + ), + ), + )); + await tester.pumpAndSettle(); + + // Has no checkmark when group value doesn't match the value + expect( + tester.firstRenderObject(find.byType(CupertinoRadio)), + isNot(paints..path()) + ); + + await tester.pumpWidget(CupertinoApp( + home: Center( + child: CupertinoRadio( + value: 1, + groupValue: 1, + useCheckmarkStyle: true, + onChanged: (int? i) { }, + ), + ), + )); + await tester.pumpAndSettle(); + + // Draws a path to show the checkmark when toggled on + expect( + tester.firstRenderObject(find.byType(CupertinoRadio)), + paints..path() + ); + }); + testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { final Key key = UniqueKey();