mirror of
https://github.com/flutter/flutter
synced 2024-10-02 14:34:22 +00:00
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:
parent
3f01c7e019
commit
678f40cf04
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Reference in a new issue