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
This commit is contained in:
Mitchell Goodwin 2023-05-16 14:54:20 -07:00 committed by GitHub
parent 3f01c7e019
commit 678f40cf04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 141 additions and 16 deletions

View file

@ -79,6 +79,7 @@ class CupertinoRadio<T> 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<T> 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<T> extends State<CupertinoRadio<T>> 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) {

View file

@ -96,7 +96,8 @@ class Radio<T> 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<T> 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<T> 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<T> 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<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
focusColor: widget.focusColor,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
useCheckmarkStyle: widget.useCupertinoCheckmarkStyle,
);
}
}

View file

@ -189,6 +189,7 @@ class RadioListTile<T> 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<T> extends StatelessWidget {
this.focusNode,
this.onFocusChange,
this.enableFeedback,
this.useCupertinoCheckmarkStyle = false,
}) : _radioType = _RadioType.adaptive,
assert(!isThreeLine || subtitle != null);
@ -435,6 +437,17 @@ class RadioListTile<T> 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<T> extends StatelessWidget {
hoverColor: hoverColor,
overlayColor: overlayColor,
splashRadius: splashRadius,
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
);
}

View file

@ -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<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 {
final Key key = UniqueKey();