@@ -74,6 +74,7 @@ class CupertinoSwitch extends StatefulWidget {
74
74
this .trackColor,
75
75
this .thumbColor,
76
76
this .applyTheme,
77
+ this .focusColor,
77
78
this .dragStartBehavior = DragStartBehavior .start,
78
79
}) : assert (value != null ),
79
80
assert (dragStartBehavior != null );
@@ -125,6 +126,11 @@ class CupertinoSwitch extends StatefulWidget {
125
126
/// Defaults to [CupertinoColors.white] when null.
126
127
final Color ? thumbColor;
127
128
129
+ /// The color to use for the focus highlight for keyboard interactions.
130
+ ///
131
+ /// Defaults to a a slightly transparent [activeColor] .
132
+ final Color ? focusColor;
133
+
128
134
/// {@template flutter.cupertino.CupertinoSwitch.applyTheme}
129
135
/// Whether to apply the ambient [CupertinoThemeData] .
130
136
///
@@ -178,8 +184,14 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
178
184
late AnimationController _reactionController;
179
185
late Animation <double > _reaction;
180
186
187
+ late bool isFocused;
188
+
181
189
bool get isInteractive => widget.onChanged != null ;
182
190
191
+ late final Map <Type , Action <Intent >> _actionMap = < Type , Action <Intent >> {
192
+ ActivateIntent : CallbackAction <ActivateIntent >(onInvoke: _handleTap),
193
+ };
194
+
183
195
// A non-null boolean value that changes to true at the end of a drag if the
184
196
// switch must be animated to the position indicated by the widget's value.
185
197
bool needsPositionAnimation = false ;
@@ -188,6 +200,8 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
188
200
void initState () {
189
201
super .initState ();
190
202
203
+ isFocused = false ;
204
+
191
205
_tap = TapGestureRecognizer ()
192
206
..onTapDown = _handleTapDown
193
207
..onTapUp = _handleTapUp
@@ -253,7 +267,7 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
253
267
_reactionController.forward ();
254
268
}
255
269
256
- void _handleTap () {
270
+ void _handleTap ([ Intent ? _] ) {
257
271
if (isInteractive) {
258
272
widget.onChanged !(! widget.value);
259
273
_emitVibration ();
@@ -322,29 +336,49 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
322
336
}
323
337
}
324
338
339
+ void _onShowFocusHighlight (bool showHighlight) {
340
+ setState (() { isFocused = showHighlight; });
341
+ }
342
+
325
343
@override
326
344
Widget build (BuildContext context) {
327
345
final CupertinoThemeData theme = CupertinoTheme .of (context);
346
+ final Color activeColor = CupertinoDynamicColor .resolve (
347
+ widget.activeColor
348
+ ?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null )
349
+ ?? CupertinoColors .systemGreen,
350
+ context,
351
+ );
328
352
if (needsPositionAnimation) {
329
353
_resumePositionAnimation ();
330
354
}
331
355
return MouseRegion (
332
356
cursor: isInteractive && kIsWeb ? SystemMouseCursors .click : MouseCursor .defer,
333
357
child: Opacity (
334
358
opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0 ,
335
- child: _CupertinoSwitchRenderObjectWidget (
336
- value: widget.value,
337
- activeColor: CupertinoDynamicColor .resolve (
338
- widget.activeColor
339
- ?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null )
340
- ?? CupertinoColors .systemGreen,
341
- context,
359
+ child: FocusableActionDetector (
360
+ onShowFocusHighlight: _onShowFocusHighlight,
361
+ actions: _actionMap,
362
+ enabled: isInteractive,
363
+ child: _CupertinoSwitchRenderObjectWidget (
364
+ value: widget.value,
365
+ activeColor: activeColor,
366
+ trackColor: CupertinoDynamicColor .resolve (widget.trackColor ?? CupertinoColors .secondarySystemFill, context),
367
+ thumbColor: CupertinoDynamicColor .resolve (widget.thumbColor ?? CupertinoColors .white, context),
368
+ // Opacity, lightness, and saturation values were aproximated with
369
+ // color pickers on the switches in the macOS settings.
370
+ focusColor: CupertinoDynamicColor .resolve (
371
+ widget.focusColor ??
372
+ HSLColor
373
+ .fromColor (activeColor.withOpacity (0.80 ))
374
+ .withLightness (0.69 ).withSaturation (0.835 )
375
+ .toColor (),
376
+ context),
377
+ onChanged: widget.onChanged,
378
+ textDirection: Directionality .of (context),
379
+ isFocused: isFocused,
380
+ state: this ,
342
381
),
343
- trackColor: CupertinoDynamicColor .resolve (widget.trackColor ?? CupertinoColors .secondarySystemFill, context),
344
- thumbColor: CupertinoDynamicColor .resolve (widget.thumbColor ?? CupertinoColors .white, context),
345
- onChanged: widget.onChanged,
346
- textDirection: Directionality .of (context),
347
- state: this ,
348
382
),
349
383
),
350
384
);
@@ -367,18 +401,22 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
367
401
required this .activeColor,
368
402
required this .trackColor,
369
403
required this .thumbColor,
404
+ required this .focusColor,
370
405
required this .onChanged,
371
406
required this .textDirection,
407
+ required this .isFocused,
372
408
required this .state,
373
409
});
374
410
375
411
final bool value;
376
412
final Color activeColor;
377
413
final Color trackColor;
378
414
final Color thumbColor;
415
+ final Color focusColor;
379
416
final ValueChanged <bool >? onChanged;
380
417
final _CupertinoSwitchState state;
381
418
final TextDirection textDirection;
419
+ final bool isFocused;
382
420
383
421
@override
384
422
_RenderCupertinoSwitch createRenderObject (BuildContext context) {
@@ -387,8 +425,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
387
425
activeColor: activeColor,
388
426
trackColor: trackColor,
389
427
thumbColor: thumbColor,
428
+ focusColor: focusColor,
390
429
onChanged: onChanged,
391
430
textDirection: textDirection,
431
+ isFocused: isFocused,
392
432
state: state,
393
433
);
394
434
}
@@ -401,8 +441,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
401
441
..activeColor = activeColor
402
442
..trackColor = trackColor
403
443
..thumbColor = thumbColor
444
+ ..focusColor = focusColor
404
445
..onChanged = onChanged
405
- ..textDirection = textDirection;
446
+ ..textDirection = textDirection
447
+ ..isFocused = isFocused;
406
448
}
407
449
}
408
450
@@ -426,18 +468,22 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
426
468
required Color activeColor,
427
469
required Color trackColor,
428
470
required Color thumbColor,
471
+ required Color focusColor,
429
472
ValueChanged <bool >? onChanged,
430
473
required TextDirection textDirection,
474
+ required bool isFocused,
431
475
required _CupertinoSwitchState state,
432
476
}) : assert (value != null ),
433
477
assert (activeColor != null ),
434
478
assert (state != null ),
435
479
_value = value,
436
480
_activeColor = activeColor,
437
481
_trackColor = trackColor,
482
+ _focusColor = focusColor,
438
483
_thumbPainter = CupertinoThumbPainter .switchThumb (color: thumbColor),
439
484
_onChanged = onChanged,
440
485
_textDirection = textDirection,
486
+ _isFocused = isFocused,
441
487
_state = state,
442
488
super (additionalConstraints: const BoxConstraints .tightFor (width: _kSwitchWidth, height: _kSwitchHeight)) {
443
489
state.position.addListener (markNeedsPaint);
@@ -490,6 +536,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
490
536
markNeedsPaint ();
491
537
}
492
538
539
+ Color get focusColor => _focusColor;
540
+ Color _focusColor;
541
+ set focusColor (Color value) {
542
+ assert (value != null );
543
+ if (value == _focusColor) {
544
+ return ;
545
+ }
546
+ _focusColor = value;
547
+ markNeedsPaint ();
548
+ }
549
+
493
550
ValueChanged <bool >? get onChanged => _onChanged;
494
551
ValueChanged <bool >? _onChanged;
495
552
set onChanged (ValueChanged <bool >? value) {
@@ -515,6 +572,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
515
572
markNeedsPaint ();
516
573
}
517
574
575
+ bool get isFocused => _isFocused;
576
+ bool _isFocused;
577
+ set isFocused (bool value) {
578
+ assert (value != null );
579
+ if (value == _isFocused) {
580
+ return ;
581
+ }
582
+ _isFocused = value;
583
+ markNeedsPaint ();
584
+ }
585
+
518
586
bool get isInteractive => onChanged != null ;
519
587
520
588
@override
@@ -570,6 +638,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
570
638
final RRect trackRRect = RRect .fromRectAndRadius (trackRect, const Radius .circular (_kTrackRadius));
571
639
canvas.drawRRect (trackRRect, paint);
572
640
641
+ if (_isFocused) {
642
+ // Paints a border around the switch in the focus color.
643
+ final RRect borderTrackRRect = trackRRect.inflate (1.75 );
644
+
645
+ final Paint borderPaint = Paint ()
646
+ ..color = focusColor
647
+ ..style = PaintingStyle .stroke
648
+ ..strokeWidth = 3.5 ;
649
+
650
+ canvas.drawRRect (borderTrackRRect, borderPaint);
651
+ }
652
+
573
653
final double currentThumbExtension = CupertinoThumbPainter .extension * currentReactionValue;
574
654
final double thumbLeft = lerpDouble (
575
655
trackRect.left + _kTrackInnerStart - CupertinoThumbPainter .radius,
0 commit comments