Unverified Commit 678f40cf authored by Mitchell Goodwin's avatar Mitchell Goodwin Committed by GitHub

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
parent 3f01c7e0
...@@ -79,6 +79,7 @@ class CupertinoRadio<T> extends StatefulWidget { ...@@ -79,6 +79,7 @@ class CupertinoRadio<T> extends StatefulWidget {
this.focusColor, this.focusColor,
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
this.useCheckmarkStyle = false,
}); });
/// The value represented by this radio button. /// The value represented by this radio button.
...@@ -146,6 +147,12 @@ class CupertinoRadio<T> extends StatefulWidget { ...@@ -146,6 +147,12 @@ class CupertinoRadio<T> extends StatefulWidget {
/// {@end-tool} /// {@end-tool}
final bool toggleable; 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. /// The color to use when this radio button is selected.
/// ///
/// Defaults to [CupertinoColors.activeBlue]. /// Defaults to [CupertinoColors.activeBlue].
...@@ -263,7 +270,8 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid ...@@ -263,7 +270,8 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor ..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor
..inactiveColor = effectiveInactiveColor ..inactiveColor = effectiveInactiveColor
..fillColor = effectiveFillColor ..fillColor = effectiveFillColor
..value = value, ..value = value
..checkmarkStyle = widget.useCheckmarkStyle,
), ),
); );
} }
...@@ -290,16 +298,48 @@ class _RadioPainter extends ToggleablePainter { ...@@ -290,16 +298,48 @@ class _RadioPainter extends ToggleablePainter {
notifyListeners(); notifyListeners();
} }
bool get checkmarkStyle => _checkmarkStyle;
bool _checkmarkStyle = false;
set checkmarkStyle(bool value) {
if (value == _checkmarkStyle) {
return;
}
_checkmarkStyle = value;
notifyListeners();
}
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final Offset center = (Offset.zero & size).center; final Offset center = (Offset.zero & size).center;
// Outer border
final Paint paint = Paint() final Paint paint = Paint()
..color = inactiveColor ..color = inactiveColor
..style = PaintingStyle.fill ..style = PaintingStyle.fill
..strokeWidth = 0.1; ..strokeWidth = 0.1;
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); canvas.drawCircle(center, _kOuterRadius, paint);
paint.style = PaintingStyle.stroke; paint.style = PaintingStyle.stroke;
...@@ -313,6 +353,7 @@ class _RadioPainter extends ToggleablePainter { ...@@ -313,6 +353,7 @@ class _RadioPainter extends ToggleablePainter {
paint.color = fillColor; paint.color = fillColor;
canvas.drawCircle(center, _kInnerRadius, paint); canvas.drawCircle(center, _kInnerRadius, paint);
} }
}
if (isFocused) { if (isFocused) {
paint.style = PaintingStyle.stroke; paint.style = PaintingStyle.stroke;
......
...@@ -96,7 +96,8 @@ class Radio<T> extends StatefulWidget { ...@@ -96,7 +96,8 @@ class Radio<T> extends StatefulWidget {
this.visualDensity, this.visualDensity,
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
}) : _radioType = _RadioType.material; }) : _radioType = _RadioType.material,
useCupertinoCheckmarkStyle = false;
/// Creates an adaptive [Radio] based on whether the target platform is iOS /// Creates an adaptive [Radio] based on whether the target platform is iOS
/// or macOS, following Material design's /// or macOS, following Material design's
...@@ -111,6 +112,8 @@ class Radio<T> extends StatefulWidget { ...@@ -111,6 +112,8 @@ class Radio<T> extends StatefulWidget {
/// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius], /// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius],
/// [materialTapTargetSize], [visualDensity]. /// [materialTapTargetSize], [visualDensity].
/// ///
/// [useCupertinoCheckmarkStyle] is used only if a [CupertinoRadio] is created.
///
/// The target platform is based on the current [Theme]: [ThemeData.platform]. /// The target platform is based on the current [Theme]: [ThemeData.platform].
const Radio.adaptive({ const Radio.adaptive({
super.key, super.key,
...@@ -129,6 +132,7 @@ class Radio<T> extends StatefulWidget { ...@@ -129,6 +132,7 @@ class Radio<T> extends StatefulWidget {
this.visualDensity, this.visualDensity,
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
this.useCupertinoCheckmarkStyle = false
}) : _radioType = _RadioType.adaptive; }) : _radioType = _RadioType.adaptive;
/// The value represented by this radio button. /// The value represented by this radio button.
...@@ -345,6 +349,15 @@ class Radio<T> extends StatefulWidget { ...@@ -345,6 +349,15 @@ class Radio<T> extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus} /// {@macro flutter.widgets.Focus.autofocus}
final bool 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; final _RadioType _radioType;
bool get _selected => value == groupValue; bool get _selected => value == groupValue;
...@@ -427,6 +440,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg ...@@ -427,6 +440,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
focusColor: widget.focusColor, focusColor: widget.focusColor,
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
useCheckmarkStyle: widget.useCupertinoCheckmarkStyle,
); );
} }
} }
......
...@@ -189,6 +189,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -189,6 +189,7 @@ class RadioListTile<T> extends StatelessWidget {
this.onFocusChange, this.onFocusChange,
this.enableFeedback, this.enableFeedback,
}) : _radioType = _RadioType.material, }) : _radioType = _RadioType.material,
useCupertinoCheckmarkStyle = false,
assert(!isThreeLine || subtitle != null); assert(!isThreeLine || subtitle != null);
/// Creates a combination of a list tile and a platform adaptive radio. /// Creates a combination of a list tile and a platform adaptive radio.
...@@ -226,6 +227,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -226,6 +227,7 @@ class RadioListTile<T> extends StatelessWidget {
this.focusNode, this.focusNode,
this.onFocusChange, this.onFocusChange,
this.enableFeedback, this.enableFeedback,
this.useCupertinoCheckmarkStyle = false,
}) : _radioType = _RadioType.adaptive, }) : _radioType = _RadioType.adaptive,
assert(!isThreeLine || subtitle != null); assert(!isThreeLine || subtitle != null);
...@@ -435,6 +437,17 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -435,6 +437,17 @@ class RadioListTile<T> extends StatelessWidget {
final _RadioType _radioType; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget control; final Widget control;
...@@ -468,6 +481,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -468,6 +481,7 @@ class RadioListTile<T> extends StatelessWidget {
hoverColor: hoverColor, hoverColor: hoverColor,
overlayColor: overlayColor, overlayColor: overlayColor,
splashRadius: splashRadius, splashRadius: splashRadius,
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
); );
} }
......
...@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { void main() {
...@@ -350,6 +351,61 @@ void main() { ...@@ -350,6 +351,61 @@ void main() {
expect(groupValue, equals(2)); expect(groupValue, equals(2));
}); });
testWidgets('Show a checkmark when useCheckmarkStyle is true', (WidgetTester tester) async {
await tester.pumpWidget(CupertinoApp(
home: Center(
child: CupertinoRadio<int>(
value: 1,
groupValue: 1,
onChanged: (int? i) { },
),
),
));
await tester.pumpAndSettle();
// Has no checkmark when useCheckmarkStyle is false
expect(
tester.firstRenderObject<RenderBox>(find.byType(CupertinoRadio<int>)),
isNot(paints..path())
);
await tester.pumpWidget(CupertinoApp(
home: Center(
child: CupertinoRadio<int>(
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<RenderBox>(find.byType(CupertinoRadio<int>)),
isNot(paints..path())
);
await tester.pumpWidget(CupertinoApp(
home: Center(
child: CupertinoRadio<int>(
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<RenderBox>(find.byType(CupertinoRadio<int>)),
paints..path()
);
});
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
final Key key = UniqueKey(); final Key key = UniqueKey();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment