@@ -13,6 +13,7 @@ import 'package:flutter/foundation.dart';
1313import 'package:flutter/widgets.dart' ;
1414
1515import 'colors.dart' ;
16+ import 'theme.dart' ;
1617
1718// Examples can assume:
1819// late BuildContext context;
@@ -30,6 +31,37 @@ const double _kCupertinoFocusColorOpacity = 0.80;
3031const double _kCupertinoFocusColorBrightness = 0.69 ;
3132const double _kCupertinoFocusColorSaturation = 0.835 ;
3233
34+ // Eyeballed from a radio on a physical Macbook Pro running macOS version 14.5.
35+ final Color _kDisabledOuterColor = CupertinoColors .white.withOpacity (0.50 );
36+ const Color _kDisabledInnerColor = CupertinoDynamicColor .withBrightness (
37+ color: Color .fromARGB (64 , 0 , 0 , 0 ),
38+ darkColor: Color .fromARGB (64 , 255 , 255 , 255 ),
39+ );
40+ const Color _kDisabledBorderColor = CupertinoDynamicColor .withBrightness (
41+ color: Color .fromARGB (64 , 0 , 0 , 0 ),
42+ darkColor: Color .fromARGB (64 , 0 , 0 , 0 ),
43+ );
44+ const CupertinoDynamicColor _kDefaultBorderColor = CupertinoDynamicColor .withBrightness (
45+ color: Color .fromARGB (255 , 209 , 209 , 214 ),
46+ darkColor: Color .fromARGB (64 , 0 , 0 , 0 ),
47+ );
48+ const CupertinoDynamicColor _kDefaultInnerColor = CupertinoDynamicColor .withBrightness (
49+ color: CupertinoColors .white,
50+ darkColor: Color .fromARGB (255 , 222 , 232 , 248 ),
51+ );
52+ const CupertinoDynamicColor _kDefaultOuterColor = CupertinoDynamicColor .withBrightness (
53+ color: CupertinoColors .activeBlue,
54+ darkColor: Color .fromARGB (255 , 50 , 100 , 215 ),
55+ );
56+ const double _kPressedOverlayOpacity = 0.15 ;
57+ const double _kCheckmarkStrokeWidth = 2.0 ;
58+ const double _kFocusOutlineStrokeWidth = 3.0 ;
59+ const double _kBorderOutlineStrokeWidth = 0.3 ;
60+ // In dark mode, the outer color of a radio is an opacity gradient of the
61+ // background color.
62+ const List <double > _kDarkGradientOpacities = < double > [0.14 , 0.29 ];
63+ const List <double > _kDisabledDarkGradientOpacities = < double > [0.08 , 0.14 ];
64+
3365/// A macOS-style radio button.
3466///
3567/// 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
250282 }
251283 }
252284
285+ WidgetStateProperty <Color > get _defaultOuterColor {
286+ return WidgetStateProperty .resolveWith ((Set <WidgetState > states) {
287+ if (states.contains (WidgetState .disabled)) {
288+ return CupertinoDynamicColor .resolve (_kDisabledOuterColor, context);
289+ }
290+ if (states.contains (WidgetState .selected)) {
291+ return widget.activeColor ?? CupertinoDynamicColor .resolve (_kDefaultOuterColor, context);
292+ }
293+ return widget.inactiveColor ?? CupertinoColors .white;
294+ });
295+ }
296+
297+ WidgetStateProperty <Color > get _defaultInnerColor {
298+ return WidgetStateProperty .resolveWith ((Set <WidgetState > states) {
299+ if (states.contains (WidgetState .disabled) && states.contains (WidgetState .selected)) {
300+ return widget.fillColor ?? CupertinoDynamicColor .resolve (_kDisabledInnerColor, context);
301+ }
302+ if (states.contains (WidgetState .selected)) {
303+ return widget.fillColor ?? CupertinoDynamicColor .resolve (_kDefaultInnerColor, context);
304+ }
305+ return CupertinoColors .white;
306+ });
307+ }
308+
309+ WidgetStateProperty <Color > get _defaultBorderColor {
310+ return WidgetStateProperty .resolveWith ((Set <WidgetState > states) {
311+ if ((states.contains (WidgetState .selected) || states.contains (WidgetState .focused))
312+ && ! states.contains (WidgetState .disabled)) {
313+ return CupertinoColors .transparent;
314+ }
315+ if (states.contains (WidgetState .disabled)) {
316+ return CupertinoDynamicColor .resolve (_kDisabledBorderColor, context);
317+ }
318+ return CupertinoDynamicColor .resolve (_kDefaultBorderColor, context);
319+ });
320+ }
321+
253322 @override
254323 Widget build (BuildContext context) {
255- final Color effectiveActiveColor = widget.activeColor
256- ?? CupertinoColors .activeBlue;
257- final Color effectiveInactiveColor = widget.inactiveColor
258- ?? CupertinoColors .white;
324+ // Colors need to be resolved in selected and non selected states separately.
325+ final Set <WidgetState > activeStates = states..add (WidgetState .selected);
326+ final Set <WidgetState > inactiveStates = states..remove (WidgetState .selected);
327+
328+ // Since the states getter always makes a new set, make a copy to use
329+ // throughout the lifecycle of this build method.
330+ final Set <WidgetState > currentStates = states;
331+
332+ final Color effectiveActiveColor = _defaultOuterColor.resolve (activeStates);
259333
260- final Color effectiveFocusOverlayColor = widget.focusColor
261- ?? HSLColor
262- .fromColor (effectiveActiveColor.withOpacity (_kCupertinoFocusColorOpacity))
263- .withLightness (_kCupertinoFocusColorBrightness)
264- .withSaturation (_kCupertinoFocusColorSaturation)
265- .toColor ();
334+ final Color effectiveInactiveColor = _defaultOuterColor.resolve (inactiveStates);
266335
267- final Color effectiveActivePressedOverlayColor =
268- HSLColor .fromColor (effectiveActiveColor).withLightness (0.45 ).toColor ();
336+ final Color effectiveFocusOverlayColor = widget.focusColor ?? HSLColor
337+ .fromColor (effectiveActiveColor.withOpacity (_kCupertinoFocusColorOpacity))
338+ .withLightness (_kCupertinoFocusColorBrightness)
339+ .withSaturation (_kCupertinoFocusColorSaturation)
340+ .toColor ();
269341
270- final Color effectiveFillColor = widget.fillColor ?? CupertinoColors .white;
342+ final Color effectiveFillColor = _defaultInnerColor.resolve (currentStates);
343+
344+ final Color effectiveBorderColor = _defaultBorderColor.resolve (currentStates);
271345
272346 final WidgetStateProperty <MouseCursor > effectiveMouseCursor =
273347 WidgetStateProperty .resolveWith <MouseCursor >((Set <WidgetState > states) {
@@ -303,14 +377,19 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
303377 onFocusChange: onFocusChange,
304378 size: _size,
305379 painter: _painter
380+ ..position = position
381+ ..reaction = reaction
306382 ..focusColor = effectiveFocusOverlayColor
307383 ..downPosition = downPosition
308384 ..isFocused = focused
309- ..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor
385+ ..activeColor = effectiveActiveColor
310386 ..inactiveColor = effectiveInactiveColor
311387 ..fillColor = effectiveFillColor
312388 ..value = value
313- ..checkmarkStyle = widget.useCheckmarkStyle,
389+ ..checkmarkStyle = widget.useCheckmarkStyle
390+ ..isActive = widget.onChanged != null
391+ ..borderColor = effectiveBorderColor
392+ ..brightness = CupertinoTheme .of (context).brightness,
314393 ),
315394 );
316395 }
@@ -347,22 +426,65 @@ class _RadioPainter extends ToggleablePainter {
347426 notifyListeners ();
348427 }
349428
429+ Brightness ? get brightness => _brightness;
430+ Brightness ? _brightness;
431+ set brightness (Brightness ? value) {
432+ if (_brightness == value) {
433+ return ;
434+ }
435+ _brightness = value;
436+ notifyListeners ();
437+ }
438+
439+ Color get borderColor => _borderColor! ;
440+ Color ? _borderColor;
441+ set borderColor (Color value) {
442+ if (_borderColor == value) {
443+ return ;
444+ }
445+ _borderColor = value;
446+ notifyListeners ();
447+ }
448+
449+ void _drawPressedOverlay (Canvas canvas, Offset center, double radius) {
450+ final Paint pressedPaint = Paint ()
451+ ..color = brightness == Brightness .light
452+ ? CupertinoColors .black.withOpacity (_kPressedOverlayOpacity)
453+ : CupertinoColors .white.withOpacity (_kPressedOverlayOpacity);
454+ canvas.drawCircle (center, radius, pressedPaint);
455+ }
456+
457+ void _drawFillGradient (Canvas canvas, Offset center, double radius, Color topColor, Color bottomColor) {
458+ final LinearGradient fillGradient = LinearGradient (
459+ begin: Alignment .topCenter,
460+ end: Alignment .bottomCenter,
461+ colors: < Color > [topColor, bottomColor],
462+ );
463+ final Rect circleRect = Rect .fromCircle (center: center, radius: radius);
464+ final Paint gradientPaint = Paint ()
465+ ..shader = fillGradient.createShader (circleRect);
466+ canvas.drawPath (Path ()..addOval (circleRect), gradientPaint);
467+ }
468+
469+ void _drawOuterBorder (Canvas canvas, Offset center) {
470+ final Paint borderPaint = Paint ()
471+ ..style = PaintingStyle .stroke
472+ ..color = borderColor
473+ ..strokeWidth = _kBorderOutlineStrokeWidth;
474+ canvas.drawCircle (center, _kOuterRadius, borderPaint);
475+ }
476+
350477 @override
351478 void paint (Canvas canvas, Size size) {
352479 final Offset center = (Offset .zero & size).center;
353480
354- final Paint paint = Paint ()
355- ..color = inactiveColor
356- ..style = PaintingStyle .fill
357- ..strokeWidth = 0.1 ;
358-
359481 if (checkmarkStyle) {
360482 if (value ?? false ) {
361483 final Path path = Path ();
362484 final Paint checkPaint = Paint ()
363485 ..color = activeColor
364486 ..style = PaintingStyle .stroke
365- ..strokeWidth = 2
487+ ..strokeWidth = _kCheckmarkStrokeWidth
366488 ..strokeCap = StrokeCap .round;
367489 final double width = _size.width;
368490 final Offset origin = Offset (center.dx - (width/ 2 ), center.dy - (width/ 2 ));
@@ -377,27 +499,57 @@ class _RadioPainter extends ToggleablePainter {
377499 canvas.drawPath (path, checkPaint);
378500 }
379501 } else {
380- // Outer border
381- canvas.drawCircle (center, _kOuterRadius, paint);
382-
383- paint.style = PaintingStyle .stroke;
384- paint.color = CupertinoColors .inactiveGray;
385- canvas.drawCircle (center, _kOuterRadius, paint);
386-
387502 if (value ?? false ) {
388- paint.style = PaintingStyle .fill;
389- paint.color = activeColor;
390- canvas.drawCircle (center, _kOuterRadius, paint);
391- paint.color = fillColor;
392- canvas.drawCircle (center, _kInnerRadius, paint);
503+ final Paint outerPaint = Paint ()..color = activeColor;
504+ // Draw a gradient in dark mode if the radio is disabled.
505+ if (brightness == Brightness .dark && ! isActive) {
506+ _drawFillGradient (
507+ canvas,
508+ center,
509+ _kOuterRadius,
510+ outerPaint.color.withOpacity (isActive ? _kDarkGradientOpacities[0 ] : _kDisabledDarkGradientOpacities[0 ]),
511+ outerPaint.color.withOpacity (isActive ? _kDarkGradientOpacities[1 ] : _kDisabledDarkGradientOpacities[1 ]),
512+ );
513+ } else {
514+ canvas.drawCircle (center, _kOuterRadius, outerPaint);
515+ }
516+ // The outer circle's opacity changes when the radio is pressed.
517+ if (downPosition != null ) {
518+ _drawPressedOverlay (canvas, center, _kOuterRadius);
519+ }
520+ final Paint innerPaint = Paint ()..color = fillColor;
521+ canvas.drawCircle (center, _kInnerRadius, innerPaint);
522+ // Draw an outer border if the radio is disabled and selected.
523+ if (! isActive) {
524+ _drawOuterBorder (canvas, center);
525+ }
526+ } else {
527+ final Paint paint = Paint ();
528+ paint.color = isActive ? inactiveColor : _kDisabledOuterColor;
529+ if (brightness == Brightness .dark) {
530+ _drawFillGradient (
531+ canvas,
532+ center,
533+ _kOuterRadius,
534+ paint.color.withOpacity (isActive ? _kDarkGradientOpacities[0 ] : _kDisabledDarkGradientOpacities[0 ]),
535+ paint.color.withOpacity (isActive ? _kDarkGradientOpacities[1 ] : _kDisabledDarkGradientOpacities[1 ]),
536+ );
537+ } else {
538+ canvas.drawCircle (center, _kOuterRadius, paint);
539+ }
540+ // The entire circle's opacity changes when the radio is pressed.
541+ if (downPosition != null ) {
542+ _drawPressedOverlay (canvas, center, _kOuterRadius);
543+ }
544+ _drawOuterBorder (canvas, center);
393545 }
394546 }
395-
396547 if (isFocused) {
397- paint.style = PaintingStyle .stroke;
398- paint.color = focusColor;
399- paint.strokeWidth = 3.0 ;
400- canvas.drawCircle (center, _kOuterRadius + 1.5 , paint);
548+ final Paint focusPaint = Paint ()
549+ ..style = PaintingStyle .stroke
550+ ..color = focusColor
551+ ..strokeWidth = _kFocusOutlineStrokeWidth;
552+ canvas.drawCircle (center, _kOuterRadius + _kFocusOutlineStrokeWidth / 2 , focusPaint);
401553 }
402554 }
403555}
0 commit comments