@@ -13,6 +13,7 @@ import 'package:flutter/foundation.dart';
13
13
import 'package:flutter/widgets.dart' ;
14
14
15
15
import 'colors.dart' ;
16
+ import 'theme.dart' ;
16
17
17
18
// Examples can assume:
18
19
// late BuildContext context;
@@ -30,6 +31,37 @@ const double _kCupertinoFocusColorOpacity = 0.80;
30
31
const double _kCupertinoFocusColorBrightness = 0.69 ;
31
32
const double _kCupertinoFocusColorSaturation = 0.835 ;
32
33
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
+
33
65
/// A macOS-style radio button.
34
66
///
35
67
/// 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
250
282
}
251
283
}
252
284
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
+
253
322
@override
254
323
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);
259
333
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);
266
335
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 ();
269
341
270
- final Color effectiveFillColor = widget.fillColor ?? CupertinoColors .white;
342
+ final Color effectiveFillColor = _defaultInnerColor.resolve (currentStates);
343
+
344
+ final Color effectiveBorderColor = _defaultBorderColor.resolve (currentStates);
271
345
272
346
final WidgetStateProperty <MouseCursor > effectiveMouseCursor =
273
347
WidgetStateProperty .resolveWith <MouseCursor >((Set <WidgetState > states) {
@@ -303,14 +377,19 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
303
377
onFocusChange: onFocusChange,
304
378
size: _size,
305
379
painter: _painter
380
+ ..position = position
381
+ ..reaction = reaction
306
382
..focusColor = effectiveFocusOverlayColor
307
383
..downPosition = downPosition
308
384
..isFocused = focused
309
- ..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor
385
+ ..activeColor = effectiveActiveColor
310
386
..inactiveColor = effectiveInactiveColor
311
387
..fillColor = effectiveFillColor
312
388
..value = value
313
- ..checkmarkStyle = widget.useCheckmarkStyle,
389
+ ..checkmarkStyle = widget.useCheckmarkStyle
390
+ ..isActive = widget.onChanged != null
391
+ ..borderColor = effectiveBorderColor
392
+ ..brightness = CupertinoTheme .of (context).brightness,
314
393
),
315
394
);
316
395
}
@@ -347,22 +426,65 @@ class _RadioPainter extends ToggleablePainter {
347
426
notifyListeners ();
348
427
}
349
428
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
+
350
477
@override
351
478
void paint (Canvas canvas, Size size) {
352
479
final Offset center = (Offset .zero & size).center;
353
480
354
- final Paint paint = Paint ()
355
- ..color = inactiveColor
356
- ..style = PaintingStyle .fill
357
- ..strokeWidth = 0.1 ;
358
-
359
481
if (checkmarkStyle) {
360
482
if (value ?? false ) {
361
483
final Path path = Path ();
362
484
final Paint checkPaint = Paint ()
363
485
..color = activeColor
364
486
..style = PaintingStyle .stroke
365
- ..strokeWidth = 2
487
+ ..strokeWidth = _kCheckmarkStrokeWidth
366
488
..strokeCap = StrokeCap .round;
367
489
final double width = _size.width;
368
490
final Offset origin = Offset (center.dx - (width/ 2 ), center.dy - (width/ 2 ));
@@ -377,27 +499,57 @@ class _RadioPainter extends ToggleablePainter {
377
499
canvas.drawPath (path, checkPaint);
378
500
}
379
501
} 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
-
387
502
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);
393
545
}
394
546
}
395
-
396
547
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);
401
553
}
402
554
}
403
555
}
0 commit comments