Improve CupertinoRadio
fidelity (#149703)
Adds the following: * Darkens when pressed in light mode * Lightens when pressed in dark mode. * Tests that confirm `CupertinoRadio` is focusable and has correct focus colors * Tests that confirm `CupertinoRadio` uses correct default active/inactive/fill colors * Same look in disabled vs. enabled states as native macOS: | Native macOS | Flutter Before | Flutter After | --- | --- | --- | | <img width="50" alt="radio native" src="https://github.com/flutter/flutter/assets/77553258/27c8c27e-f0dc-4ad7-a8c2-361ae8b437bb"> | <img width="50" alt="flutter radio before" src="https://github.com/flutter/flutter/assets/77553258/580d9c4b-0f0d-457e-851f-73450738ee16"> | <img width="50" alt="flutter radio after" src="https://github.com/flutter/flutter/assets/77553258/da6ae21b-87f8-45d8-a2d2-da70ff4853a1"> | * Same look of an unselected radio button in dark mode as native macOS: | Native light mode | Flutter before light mode | Flutter after light mode | Native dark mode | Flutter before dark mode | Flutter after dark mode --- | --- | --- | --- | --- | --- | | <img width="23" alt="native radio light" src="https://github.com/flutter/flutter/assets/77553258/b52fc18b-e10d-4205-b10b-1536fbbf1ca0"> | <img width="23" alt="flutter radio after light" src="https://github.com/flutter/flutter/assets/77553258/54294523-8254-479c-b668-77927a8295f1"> | <img width="23" alt="flutter radio light" src="https://github.com/flutter/flutter/assets/77553258/8472deee-e5ce-4d39-9207-d788ad7f34f4"> | <img width="23" alt="native radio dark" src="https://github.com/flutter/flutter/assets/77553258/44143099-6ab4-4fb8-8a94-ebb1386022c9"> | <img width="23" alt="flutter radio before dark" src="https://github.com/flutter/flutter/assets/77553258/3411d9fb-fc7f-4b20-86a5-34fda167d5b9"> | <img width="23" alt="flutter radio dark" src="https://github.com/flutter/flutter/assets/77553258/39ea3649-142e-43ad-9681-24e1216e0987"> | ## Light mode (with focus highlight) | Native light mode | Flutter before light mode | Flutter after light mode | --- | --- | --- | | <img width="70" alt="native radio light mode" src="https://github.com/user-attachments/assets/914b9f1f-5819-4c5b-8739-8498a72b337f"> | <img width="70" alt="radio flutter focus before" src="https://github.com/user-attachments/assets/3129fca3-3310-4b2b-bcf3-98aa8f049911"> | <img width="70" alt="radio flutter focus after" src="https://github.com/user-attachments/assets/7a2089d9-b2b5-4ff0-9db9-444455301146"> | ## Dark mode | Native dark mode | Flutter before dark mode | Flutter after dark mode | --- | --- | --- | | <img width="70" alt="native radio dark mode" src="https://github.com/user-attachments/assets/4da3c055-ce89-4f37-8fcd-d4cbbc4031a0"> | <img width="70" alt="flutter before radio dark mode" src="https://github.com/user-attachments/assets/36b5f36a-f1d9-4c32-8493-3533a749cf5d"> | <img width="70" alt="flutter radio dark mode after" src="https://github.com/user-attachments/assets/28828e01-bb2f-4217-9756-2766be3919a6"> | ## Disabled light mode | Native | Flutter before | Flutter after | --- | --- | --- | | <img width="120" alt="light disabled radio native" src="https://github.com/user-attachments/assets/bf6d2561-5dcf-4882-afac-6b639fa949b0"> | <img width="120" alt="Screenshot 2024-07-30 at 3 13 30 PM" src="https://github.com/user-attachments/assets/3efc978c-fa58-44e8-877a-ea29778ea384"> | <img width="120" alt="light disabled radio flutter after" src="https://github.com/user-attachments/assets/b2c2e30a-cb8d-40d0-aa6f-75a98caa4829"> | ## Disabled dark mode | Native | Flutter before | Flutter after | --- | --- | --- | | <img width="120" alt="dark disabled radio native" src="https://github.com/user-attachments/assets/feedccc7-9802-4b0c-8038-c9eb771b0eb0"> | <img width="120" alt="Screenshot 2024-07-30 at 3 13 30 PM" src="https://github.com/user-attachments/assets/6d2f03f7-7216-4850-8c4f-f79ae05bb9da"> | <img width="136" alt="dark disabled radio flutter after" src="https://github.com/user-attachments/assets/5e03d4fc-4b8e-4518-b429-6bb58f6d988d"> | `CupertinoRadio` is missing a tristate/mixed state, but [Apple's latest HIG specs discourages its use](https://developer.apple.com/design/human-interface-guidelines/toggles#Radio-buttons). Fixes https://github.com/flutter/flutter/issues/151994 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [Data Driven Fixes]: https://github.com/flutter/flutter/wiki/Data-driven-Fixes --------- Co-authored-by: Kate Lovett <katelovett@google.com>
This commit is contained in:
parent
9893ec99d0
commit
6b73de27bd
@ -13,6 +13,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
@ -30,6 +31,37 @@ const double _kCupertinoFocusColorOpacity = 0.80;
|
||||
const double _kCupertinoFocusColorBrightness = 0.69;
|
||||
const double _kCupertinoFocusColorSaturation = 0.835;
|
||||
|
||||
// Eyeballed from a radio on a physical Macbook Pro running macOS version 14.5.
|
||||
final Color _kDisabledOuterColor = CupertinoColors.white.withOpacity(0.50);
|
||||
const Color _kDisabledInnerColor = CupertinoDynamicColor.withBrightness(
|
||||
color: Color.fromARGB(64, 0, 0, 0),
|
||||
darkColor: Color.fromARGB(64, 255, 255, 255),
|
||||
);
|
||||
const Color _kDisabledBorderColor = CupertinoDynamicColor.withBrightness(
|
||||
color: Color.fromARGB(64, 0, 0, 0),
|
||||
darkColor: Color.fromARGB(64, 0, 0, 0),
|
||||
);
|
||||
const CupertinoDynamicColor _kDefaultBorderColor = CupertinoDynamicColor.withBrightness(
|
||||
color: Color.fromARGB(255, 209, 209, 214),
|
||||
darkColor: Color.fromARGB(64, 0, 0, 0),
|
||||
);
|
||||
const CupertinoDynamicColor _kDefaultInnerColor = CupertinoDynamicColor.withBrightness(
|
||||
color: CupertinoColors.white,
|
||||
darkColor: Color.fromARGB(255, 222, 232, 248),
|
||||
);
|
||||
const CupertinoDynamicColor _kDefaultOuterColor = CupertinoDynamicColor.withBrightness(
|
||||
color: CupertinoColors.activeBlue,
|
||||
darkColor: Color.fromARGB(255, 50, 100, 215),
|
||||
);
|
||||
const double _kPressedOverlayOpacity = 0.15;
|
||||
const double _kCheckmarkStrokeWidth = 2.0;
|
||||
const double _kFocusOutlineStrokeWidth = 3.0;
|
||||
const double _kBorderOutlineStrokeWidth = 0.3;
|
||||
// In dark mode, the outer color of a radio is an opacity gradient of the
|
||||
// background color.
|
||||
const List<double> _kDarkGradientOpacities = <double>[0.14, 0.29];
|
||||
const List<double> _kDisabledDarkGradientOpacities = <double>[0.08, 0.14];
|
||||
|
||||
/// A macOS-style radio button.
|
||||
///
|
||||
/// Used to select between a number of mutually exclusive values. When one radio
|
||||
@ -250,24 +282,66 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
|
||||
}
|
||||
}
|
||||
|
||||
WidgetStateProperty<Color> get _defaultOuterColor {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return CupertinoDynamicColor.resolve(_kDisabledOuterColor, context);
|
||||
}
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return widget.activeColor ?? CupertinoDynamicColor.resolve(_kDefaultOuterColor, context);
|
||||
}
|
||||
return widget.inactiveColor ?? CupertinoColors.white;
|
||||
});
|
||||
}
|
||||
|
||||
WidgetStateProperty<Color> get _defaultInnerColor {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled) && states.contains(WidgetState.selected)) {
|
||||
return widget.fillColor ?? CupertinoDynamicColor.resolve(_kDisabledInnerColor, context);
|
||||
}
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return widget.fillColor ?? CupertinoDynamicColor.resolve(_kDefaultInnerColor, context);
|
||||
}
|
||||
return CupertinoColors.white;
|
||||
});
|
||||
}
|
||||
|
||||
WidgetStateProperty<Color> get _defaultBorderColor {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if ((states.contains(WidgetState.selected) || states.contains(WidgetState.focused))
|
||||
&& !states.contains(WidgetState.disabled)) {
|
||||
return CupertinoColors.transparent;
|
||||
}
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return CupertinoDynamicColor.resolve(_kDisabledBorderColor, context);
|
||||
}
|
||||
return CupertinoDynamicColor.resolve(_kDefaultBorderColor, context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color effectiveActiveColor = widget.activeColor
|
||||
?? CupertinoColors.activeBlue;
|
||||
final Color effectiveInactiveColor = widget.inactiveColor
|
||||
?? CupertinoColors.white;
|
||||
// Colors need to be resolved in selected and non selected states separately.
|
||||
final Set<WidgetState> activeStates = states..add(WidgetState.selected);
|
||||
final Set<WidgetState> inactiveStates = states..remove(WidgetState.selected);
|
||||
|
||||
final Color effectiveFocusOverlayColor = widget.focusColor
|
||||
?? HSLColor
|
||||
.fromColor(effectiveActiveColor.withOpacity(_kCupertinoFocusColorOpacity))
|
||||
.withLightness(_kCupertinoFocusColorBrightness)
|
||||
.withSaturation(_kCupertinoFocusColorSaturation)
|
||||
.toColor();
|
||||
// Since the states getter always makes a new set, make a copy to use
|
||||
// throughout the lifecycle of this build method.
|
||||
final Set<WidgetState> currentStates = states;
|
||||
|
||||
final Color effectiveActivePressedOverlayColor =
|
||||
HSLColor.fromColor(effectiveActiveColor).withLightness(0.45).toColor();
|
||||
final Color effectiveActiveColor = _defaultOuterColor.resolve(activeStates);
|
||||
|
||||
final Color effectiveFillColor = widget.fillColor ?? CupertinoColors.white;
|
||||
final Color effectiveInactiveColor = _defaultOuterColor.resolve(inactiveStates);
|
||||
|
||||
final Color effectiveFocusOverlayColor = widget.focusColor ?? HSLColor
|
||||
.fromColor(effectiveActiveColor.withOpacity(_kCupertinoFocusColorOpacity))
|
||||
.withLightness(_kCupertinoFocusColorBrightness)
|
||||
.withSaturation(_kCupertinoFocusColorSaturation)
|
||||
.toColor();
|
||||
|
||||
final Color effectiveFillColor = _defaultInnerColor.resolve(currentStates);
|
||||
|
||||
final Color effectiveBorderColor = _defaultBorderColor.resolve(currentStates);
|
||||
|
||||
final WidgetStateProperty<MouseCursor> effectiveMouseCursor =
|
||||
WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
|
||||
@ -303,14 +377,19 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
|
||||
onFocusChange: onFocusChange,
|
||||
size: _size,
|
||||
painter: _painter
|
||||
..position = position
|
||||
..reaction = reaction
|
||||
..focusColor = effectiveFocusOverlayColor
|
||||
..downPosition = downPosition
|
||||
..isFocused = focused
|
||||
..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor
|
||||
..activeColor = effectiveActiveColor
|
||||
..inactiveColor = effectiveInactiveColor
|
||||
..fillColor = effectiveFillColor
|
||||
..value = value
|
||||
..checkmarkStyle = widget.useCheckmarkStyle,
|
||||
..checkmarkStyle = widget.useCheckmarkStyle
|
||||
..isActive = widget.onChanged != null
|
||||
..borderColor = effectiveBorderColor
|
||||
..brightness = CupertinoTheme.of(context).brightness,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -347,22 +426,65 @@ class _RadioPainter extends ToggleablePainter {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Brightness? get brightness => _brightness;
|
||||
Brightness? _brightness;
|
||||
set brightness(Brightness? value) {
|
||||
if (_brightness == value) {
|
||||
return;
|
||||
}
|
||||
_brightness = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Color get borderColor => _borderColor!;
|
||||
Color? _borderColor;
|
||||
set borderColor(Color value) {
|
||||
if (_borderColor == value) {
|
||||
return;
|
||||
}
|
||||
_borderColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _drawPressedOverlay(Canvas canvas, Offset center, double radius) {
|
||||
final Paint pressedPaint = Paint()
|
||||
..color = brightness == Brightness.light
|
||||
? CupertinoColors.black.withOpacity(_kPressedOverlayOpacity)
|
||||
: CupertinoColors.white.withOpacity(_kPressedOverlayOpacity);
|
||||
canvas.drawCircle(center, radius, pressedPaint);
|
||||
}
|
||||
|
||||
void _drawFillGradient(Canvas canvas, Offset center, double radius, Color topColor, Color bottomColor) {
|
||||
final LinearGradient fillGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[topColor, bottomColor],
|
||||
);
|
||||
final Rect circleRect = Rect.fromCircle(center: center, radius: radius);
|
||||
final Paint gradientPaint = Paint()
|
||||
..shader = fillGradient.createShader(circleRect);
|
||||
canvas.drawPath(Path()..addOval(circleRect), gradientPaint);
|
||||
}
|
||||
|
||||
void _drawOuterBorder(Canvas canvas, Offset center) {
|
||||
final Paint borderPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..color = borderColor
|
||||
..strokeWidth = _kBorderOutlineStrokeWidth;
|
||||
canvas.drawCircle(center, _kOuterRadius, borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Offset center = (Offset.zero & size).center;
|
||||
|
||||
final Paint paint = Paint()
|
||||
..color = inactiveColor
|
||||
..style = PaintingStyle.fill
|
||||
..strokeWidth = 0.1;
|
||||
|
||||
if (checkmarkStyle) {
|
||||
if (value ?? false) {
|
||||
final Path path = Path();
|
||||
final Paint checkPaint = Paint()
|
||||
..color = activeColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2
|
||||
..strokeWidth = _kCheckmarkStrokeWidth
|
||||
..strokeCap = StrokeCap.round;
|
||||
final double width = _size.width;
|
||||
final Offset origin = Offset(center.dx - (width/2), center.dy - (width/2));
|
||||
@ -377,27 +499,57 @@ class _RadioPainter extends ToggleablePainter {
|
||||
canvas.drawPath(path, checkPaint);
|
||||
}
|
||||
} else {
|
||||
// Outer border
|
||||
canvas.drawCircle(center, _kOuterRadius, 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);
|
||||
final Paint outerPaint = Paint()..color = activeColor;
|
||||
// Draw a gradient in dark mode if the radio is disabled.
|
||||
if (brightness == Brightness.dark && !isActive) {
|
||||
_drawFillGradient(
|
||||
canvas,
|
||||
center,
|
||||
_kOuterRadius,
|
||||
outerPaint.color.withOpacity(isActive ? _kDarkGradientOpacities[0] : _kDisabledDarkGradientOpacities[0]),
|
||||
outerPaint.color.withOpacity(isActive ? _kDarkGradientOpacities[1] : _kDisabledDarkGradientOpacities[1]),
|
||||
);
|
||||
} else {
|
||||
canvas.drawCircle(center, _kOuterRadius, outerPaint);
|
||||
}
|
||||
// The outer circle's opacity changes when the radio is pressed.
|
||||
if (downPosition != null) {
|
||||
_drawPressedOverlay(canvas, center, _kOuterRadius);
|
||||
}
|
||||
final Paint innerPaint = Paint()..color = fillColor;
|
||||
canvas.drawCircle(center, _kInnerRadius, innerPaint);
|
||||
// Draw an outer border if the radio is disabled and selected.
|
||||
if (!isActive) {
|
||||
_drawOuterBorder(canvas, center);
|
||||
}
|
||||
} else {
|
||||
final Paint paint = Paint();
|
||||
paint.color = isActive ? inactiveColor : _kDisabledOuterColor;
|
||||
if (brightness == Brightness.dark) {
|
||||
_drawFillGradient(
|
||||
canvas,
|
||||
center,
|
||||
_kOuterRadius,
|
||||
paint.color.withOpacity(isActive ? _kDarkGradientOpacities[0] : _kDisabledDarkGradientOpacities[0]),
|
||||
paint.color.withOpacity(isActive ? _kDarkGradientOpacities[1] : _kDisabledDarkGradientOpacities[1]),
|
||||
);
|
||||
} else {
|
||||
canvas.drawCircle(center, _kOuterRadius, paint);
|
||||
}
|
||||
// The entire circle's opacity changes when the radio is pressed.
|
||||
if (downPosition != null) {
|
||||
_drawPressedOverlay(canvas, center, _kOuterRadius);
|
||||
}
|
||||
_drawOuterBorder(canvas, center);
|
||||
}
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.color = focusColor;
|
||||
paint.strokeWidth = 3.0;
|
||||
canvas.drawCircle(center, _kOuterRadius + 1.5, paint);
|
||||
final Paint focusPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..color = focusColor
|
||||
..strokeWidth = _kFocusOutlineStrokeWidth;
|
||||
canvas.drawCircle(center, _kOuterRadius + _kFocusOutlineStrokeWidth / 2, focusPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// reduced-test-set:
|
||||
// This file is run as part of a reduced test set in CI on Mac and Windows
|
||||
// machines.
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -434,6 +440,350 @@ void main() {
|
||||
await gesture.up();
|
||||
});
|
||||
|
||||
testWidgets('Radio has correct default active/inactive/fill/border colors in light mode', (WidgetTester tester) async {
|
||||
Widget buildRadio({required int value, required int groupValue}) {
|
||||
return CupertinoApp(
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
child: CupertinoRadio<int>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: (int? i) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 1));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.light_theme.selected.png'),
|
||||
);
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 2));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.light_theme.unselected.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio has correct default active/inactive/fill/border colors in dark mode', (WidgetTester tester) async {
|
||||
Widget buildRadio({required int value, required int groupValue, bool enabled = true}) {
|
||||
return CupertinoApp(
|
||||
theme: const CupertinoThemeData(brightness: Brightness.dark),
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
child: CupertinoRadio<int>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? (int? i) { } : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 1));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.dark_theme.selected.png'),
|
||||
);
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 2));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.dark_theme.unselected.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Disabled radio has correct default active/inactive/fill/border colors in light mode', (WidgetTester tester) async {
|
||||
Widget buildRadio({required int value, required int groupValue}) {
|
||||
return CupertinoApp(
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
child: CupertinoRadio<int>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 1));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.disabled_light_theme.selected.png'),
|
||||
);
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 2));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.disabled_light_theme.unselected.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Disabled radio has correct default active/inactive/fill/border colors in dark mode', (WidgetTester tester) async {
|
||||
Widget buildRadio({required int value, required int groupValue}) {
|
||||
return CupertinoApp(
|
||||
theme: const CupertinoThemeData(brightness: Brightness.dark),
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
child: CupertinoRadio<int>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 1));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.disabled_dark_theme.selected.png'),
|
||||
);
|
||||
await tester.pumpWidget(buildRadio(value: 1, groupValue: 2));
|
||||
await expectLater(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
matchesGoldenFile('radio.disabled_dark_theme.unselected.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio can set inactive/active/fill colors', (WidgetTester tester) async {
|
||||
const Color inactiveBorderColor = Color(0xffd1d1d6);
|
||||
const Color activeColor = Color(0x0000000A);
|
||||
const Color fillColor = Color(0x0000000B);
|
||||
const Color inactiveColor = Color(0x0000000C);
|
||||
const double innerRadius = 2.975;
|
||||
const double outerRadius = 7.0;
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: (int? i) { },
|
||||
activeColor: activeColor,
|
||||
fillColor: fillColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: inactiveColor)
|
||||
..circle(radius: outerRadius, style: PaintingStyle.stroke, color: inactiveBorderColor),
|
||||
reason: 'Unselected radio button should use inactive and border colors',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 1,
|
||||
onChanged: (int? i) { },
|
||||
activeColor: activeColor,
|
||||
fillColor: fillColor,
|
||||
inactiveColor: inactiveColor,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: activeColor)
|
||||
..circle(radius: innerRadius, style: PaintingStyle.fill, color: fillColor),
|
||||
reason: 'Selected radio button should use active and fill colors',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio is slightly darkened when pressed in light mode', (WidgetTester tester) async {
|
||||
const Color activeInnerColor = Color(0xffffffff);
|
||||
const Color activeOuterColor = Color(0xff007aff);
|
||||
const Color inactiveBorderColor = Color(0xffd1d1d6);
|
||||
const Color inactiveOuterColor = Color(0xffffffff);
|
||||
const double innerRadius = 2.975;
|
||||
const double outerRadius = 7.0;
|
||||
const Color pressedShadowColor = Color(0x26ffffff);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: (int? i) { },
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.byType(CupertinoRadio<int>)));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: inactiveOuterColor)
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: pressedShadowColor)
|
||||
..circle(radius: outerRadius, style: PaintingStyle.stroke, color: inactiveBorderColor),
|
||||
reason: 'Unselected pressed radio button is slightly darkened',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 2,
|
||||
groupValue: 2,
|
||||
onChanged: (int? i) { },
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byType(CupertinoRadio<int>)));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: activeOuterColor)
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: pressedShadowColor)
|
||||
..circle(radius: innerRadius, style: PaintingStyle.fill, color: activeInnerColor),
|
||||
reason: 'Selected pressed radio button is slightly darkened',
|
||||
);
|
||||
|
||||
// Finish gestures to release resources.
|
||||
await gesture1.up();
|
||||
await gesture2.up();
|
||||
await tester.pump();
|
||||
});
|
||||
|
||||
testWidgets('Radio is slightly lightened when pressed in dark mode', (WidgetTester tester) async {
|
||||
const Color activeInnerColor = Color(0xffffffff);
|
||||
const Color activeOuterColor = Color(0xff007aff);
|
||||
const Color inactiveBorderColor = Color(0x40000000);
|
||||
const double innerRadius = 2.975;
|
||||
const double outerRadius = 7.0;
|
||||
const Color pressedShadowColor = Color(0x26ffffff);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
theme: const CupertinoThemeData(brightness: Brightness.dark),
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: (int? i) { },
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.byType(CupertinoRadio<int>)));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..path()
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: pressedShadowColor)
|
||||
..circle(radius: outerRadius, style: PaintingStyle.stroke, color: inactiveBorderColor),
|
||||
reason: 'Unselected pressed radio button is slightly lightened',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 2,
|
||||
groupValue: 2,
|
||||
onChanged: (int? i) { },
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byType(CupertinoRadio<int>)));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: activeOuterColor)
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: pressedShadowColor)
|
||||
..circle(radius: innerRadius, style: PaintingStyle.fill, color: activeInnerColor),
|
||||
reason: 'Selected pressed radio button is slightly lightened',
|
||||
);
|
||||
|
||||
// Finish gestures to release resources.
|
||||
await gesture1.up();
|
||||
await gesture2.up();
|
||||
await tester.pump();
|
||||
});
|
||||
|
||||
testWidgets('Radio is focusable and has correct focus colors', (WidgetTester tester) async {
|
||||
const Color activeInnerColor = Color(0xffffffff);
|
||||
const Color activeOuterColor = Color(0xff007aff);
|
||||
const Color defaultFocusColor = Color(0xcc6eadf2);
|
||||
const double innerRadius = 2.975;
|
||||
const double outerRadius = 7.0;
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
final FocusNode node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 1,
|
||||
onChanged: (int? i) { },
|
||||
focusNode: node,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
expect(node.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: activeOuterColor)
|
||||
..circle(radius: innerRadius, style: PaintingStyle.fill, color: activeInnerColor)
|
||||
..circle(strokeWidth: 3.0, style: PaintingStyle.stroke, color: defaultFocusColor),
|
||||
reason: 'Radio is focusable and shows the default focus color',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio can configure a focus color', (WidgetTester tester) async {
|
||||
const Color activeInnerColor = Color(0xffffffff);
|
||||
const Color activeOuterColor = Color(0xff007aff);
|
||||
const Color focusColor = Color(0x0000000A);
|
||||
const double innerRadius = 2.975;
|
||||
const double outerRadius = 7.0;
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
final FocusNode node = FocusNode();
|
||||
addTearDown(node.dispose);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 1,
|
||||
onChanged: (int? i) { },
|
||||
focusColor: focusColor,
|
||||
focusNode: node,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
expect(node.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
find.byType(CupertinoRadio<int>),
|
||||
paints
|
||||
..circle(radius: outerRadius, style: PaintingStyle.fill, color: activeOuterColor)
|
||||
..circle(radius: innerRadius, style: PaintingStyle.fill, color: activeInnerColor)
|
||||
..circle(strokeWidth: 3.0, style: PaintingStyle.stroke, color: focusColor),
|
||||
reason: 'Radio configures the color of the focus outline',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio configures mouse cursor', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
|
Loading…
x
Reference in New Issue
Block a user