Skip to content

Commit ea36b3a

Browse files
Add focus detector to CupertinoSwitch (#118345)
* Add focus detector to CupertinoSwitch * Add comment * Remove whitespace * Add focusColor constructor to CupertinoSwitch * Remove whitespace * Add color type * Remove gap in border * Adjust color and line thickness
1 parent b9ab640 commit ea36b3a

File tree

5 files changed

+196
-14
lines changed

5 files changed

+196
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#
2+
# Generated file, do not edit.
3+
#
4+
5+
list(APPEND FLUTTER_PLUGIN_LIST
6+
)
7+
8+
list(APPEND FLUTTER_FFI_PLUGIN_LIST
9+
)
10+
11+
set(PLUGIN_BUNDLED_LIBRARIES)
12+
13+
foreach(plugin ${FLUTTER_PLUGIN_LIST})
14+
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
15+
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
16+
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
17+
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
18+
endforeach(plugin)
19+
20+
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
21+
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
22+
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
23+
endforeach(ffi_plugin)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#
2+
# Generated file, do not edit.
3+
#
4+
5+
list(APPEND FLUTTER_PLUGIN_LIST
6+
)
7+
8+
list(APPEND FLUTTER_FFI_PLUGIN_LIST
9+
)
10+
11+
set(PLUGIN_BUNDLED_LIBRARIES)
12+
13+
foreach(plugin ${FLUTTER_PLUGIN_LIST})
14+
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
15+
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
16+
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
17+
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
18+
endforeach(plugin)
19+
20+
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
21+
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
22+
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
23+
endforeach(ffi_plugin)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#
2+
# Generated file, do not edit.
3+
#
4+
5+
list(APPEND FLUTTER_PLUGIN_LIST
6+
)
7+
8+
list(APPEND FLUTTER_FFI_PLUGIN_LIST
9+
)
10+
11+
set(PLUGIN_BUNDLED_LIBRARIES)
12+
13+
foreach(plugin ${FLUTTER_PLUGIN_LIST})
14+
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
15+
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
16+
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
17+
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
18+
endforeach(plugin)
19+
20+
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
21+
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
22+
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
23+
endforeach(ffi_plugin)

packages/flutter/lib/src/cupertino/switch.dart

+94-14
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class CupertinoSwitch extends StatefulWidget {
7474
this.trackColor,
7575
this.thumbColor,
7676
this.applyTheme,
77+
this.focusColor,
7778
this.dragStartBehavior = DragStartBehavior.start,
7879
}) : assert(value != null),
7980
assert(dragStartBehavior != null);
@@ -125,6 +126,11 @@ class CupertinoSwitch extends StatefulWidget {
125126
/// Defaults to [CupertinoColors.white] when null.
126127
final Color? thumbColor;
127128

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+
128134
/// {@template flutter.cupertino.CupertinoSwitch.applyTheme}
129135
/// Whether to apply the ambient [CupertinoThemeData].
130136
///
@@ -178,8 +184,14 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
178184
late AnimationController _reactionController;
179185
late Animation<double> _reaction;
180186

187+
late bool isFocused;
188+
181189
bool get isInteractive => widget.onChanged != null;
182190

191+
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
192+
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap),
193+
};
194+
183195
// A non-null boolean value that changes to true at the end of a drag if the
184196
// switch must be animated to the position indicated by the widget's value.
185197
bool needsPositionAnimation = false;
@@ -188,6 +200,8 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
188200
void initState() {
189201
super.initState();
190202

203+
isFocused = false;
204+
191205
_tap = TapGestureRecognizer()
192206
..onTapDown = _handleTapDown
193207
..onTapUp = _handleTapUp
@@ -253,7 +267,7 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
253267
_reactionController.forward();
254268
}
255269

256-
void _handleTap() {
270+
void _handleTap([Intent? _]) {
257271
if (isInteractive) {
258272
widget.onChanged!(!widget.value);
259273
_emitVibration();
@@ -322,29 +336,49 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
322336
}
323337
}
324338

339+
void _onShowFocusHighlight(bool showHighlight) {
340+
setState(() { isFocused = showHighlight; });
341+
}
342+
325343
@override
326344
Widget build(BuildContext context) {
327345
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+
);
328352
if (needsPositionAnimation) {
329353
_resumePositionAnimation();
330354
}
331355
return MouseRegion(
332356
cursor: isInteractive && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
333357
child: Opacity(
334358
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,
342381
),
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,
348382
),
349383
),
350384
);
@@ -367,18 +401,22 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
367401
required this.activeColor,
368402
required this.trackColor,
369403
required this.thumbColor,
404+
required this.focusColor,
370405
required this.onChanged,
371406
required this.textDirection,
407+
required this.isFocused,
372408
required this.state,
373409
});
374410

375411
final bool value;
376412
final Color activeColor;
377413
final Color trackColor;
378414
final Color thumbColor;
415+
final Color focusColor;
379416
final ValueChanged<bool>? onChanged;
380417
final _CupertinoSwitchState state;
381418
final TextDirection textDirection;
419+
final bool isFocused;
382420

383421
@override
384422
_RenderCupertinoSwitch createRenderObject(BuildContext context) {
@@ -387,8 +425,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
387425
activeColor: activeColor,
388426
trackColor: trackColor,
389427
thumbColor: thumbColor,
428+
focusColor: focusColor,
390429
onChanged: onChanged,
391430
textDirection: textDirection,
431+
isFocused: isFocused,
392432
state: state,
393433
);
394434
}
@@ -401,8 +441,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
401441
..activeColor = activeColor
402442
..trackColor = trackColor
403443
..thumbColor = thumbColor
444+
..focusColor = focusColor
404445
..onChanged = onChanged
405-
..textDirection = textDirection;
446+
..textDirection = textDirection
447+
..isFocused = isFocused;
406448
}
407449
}
408450

@@ -426,18 +468,22 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
426468
required Color activeColor,
427469
required Color trackColor,
428470
required Color thumbColor,
471+
required Color focusColor,
429472
ValueChanged<bool>? onChanged,
430473
required TextDirection textDirection,
474+
required bool isFocused,
431475
required _CupertinoSwitchState state,
432476
}) : assert(value != null),
433477
assert(activeColor != null),
434478
assert(state != null),
435479
_value = value,
436480
_activeColor = activeColor,
437481
_trackColor = trackColor,
482+
_focusColor = focusColor,
438483
_thumbPainter = CupertinoThumbPainter.switchThumb(color: thumbColor),
439484
_onChanged = onChanged,
440485
_textDirection = textDirection,
486+
_isFocused = isFocused,
441487
_state = state,
442488
super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) {
443489
state.position.addListener(markNeedsPaint);
@@ -490,6 +536,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
490536
markNeedsPaint();
491537
}
492538

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+
493550
ValueChanged<bool>? get onChanged => _onChanged;
494551
ValueChanged<bool>? _onChanged;
495552
set onChanged(ValueChanged<bool>? value) {
@@ -515,6 +572,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
515572
markNeedsPaint();
516573
}
517574

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+
518586
bool get isInteractive => onChanged != null;
519587

520588
@override
@@ -570,6 +638,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
570638
final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
571639
canvas.drawRRect(trackRRect, paint);
572640

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+
573653
final double currentThumbExtension = CupertinoThumbPainter.extension * currentReactionValue;
574654
final double thumbLeft = lerpDouble(
575655
trackRect.left + _kTrackInnerStart - CupertinoThumbPainter.radius,

packages/flutter/test/cupertino/switch_test.dart

+33
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,39 @@ void main() {
4848
expect(value, isTrue);
4949
});
5050

51+
testWidgets('CupertinoSwitch can be toggled by keyboard shortcuts', (WidgetTester tester) async {
52+
bool value = true;
53+
Widget buildApp({bool enabled = true}) {
54+
return CupertinoApp(
55+
home: CupertinoPageScaffold(
56+
child: Center(
57+
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
58+
return CupertinoSwitch(
59+
value: value,
60+
onChanged: enabled ? (bool newValue) {
61+
setState(() {
62+
value = newValue;
63+
});
64+
} : null,
65+
);
66+
}),
67+
),
68+
),
69+
);
70+
}
71+
await tester.pumpWidget(buildApp());
72+
await tester.pumpAndSettle();
73+
expect(value, isTrue);
74+
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
75+
await tester.pumpAndSettle();
76+
await tester.sendKeyEvent(LogicalKeyboardKey.space);
77+
await tester.pumpAndSettle();
78+
expect(value, isFalse);
79+
await tester.sendKeyEvent(LogicalKeyboardKey.space);
80+
await tester.pumpAndSettle();
81+
expect(value, isTrue);
82+
});
83+
5184
testWidgets('Switch emits light haptic vibration on tap', (WidgetTester tester) async {
5285
final Key switchKey = UniqueKey();
5386
bool value = false;

0 commit comments

Comments
 (0)