Unverified Commit ea36b3a5 authored by Mitchell Goodwin's avatar Mitchell Goodwin Committed by GitHub

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
parent b9ab6404
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
...@@ -74,6 +74,7 @@ class CupertinoSwitch extends StatefulWidget { ...@@ -74,6 +74,7 @@ class CupertinoSwitch extends StatefulWidget {
this.trackColor, this.trackColor,
this.thumbColor, this.thumbColor,
this.applyTheme, this.applyTheme,
this.focusColor,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
}) : assert(value != null), }) : assert(value != null),
assert(dragStartBehavior != null); assert(dragStartBehavior != null);
...@@ -125,6 +126,11 @@ class CupertinoSwitch extends StatefulWidget { ...@@ -125,6 +126,11 @@ class CupertinoSwitch extends StatefulWidget {
/// Defaults to [CupertinoColors.white] when null. /// Defaults to [CupertinoColors.white] when null.
final Color? thumbColor; final Color? thumbColor;
/// The color to use for the focus highlight for keyboard interactions.
///
/// Defaults to a a slightly transparent [activeColor].
final Color? focusColor;
/// {@template flutter.cupertino.CupertinoSwitch.applyTheme} /// {@template flutter.cupertino.CupertinoSwitch.applyTheme}
/// Whether to apply the ambient [CupertinoThemeData]. /// Whether to apply the ambient [CupertinoThemeData].
/// ///
...@@ -178,8 +184,14 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt ...@@ -178,8 +184,14 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
late AnimationController _reactionController; late AnimationController _reactionController;
late Animation<double> _reaction; late Animation<double> _reaction;
late bool isFocused;
bool get isInteractive => widget.onChanged != null; bool get isInteractive => widget.onChanged != null;
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap),
};
// A non-null boolean value that changes to true at the end of a drag if the // A non-null boolean value that changes to true at the end of a drag if the
// switch must be animated to the position indicated by the widget's value. // switch must be animated to the position indicated by the widget's value.
bool needsPositionAnimation = false; bool needsPositionAnimation = false;
...@@ -188,6 +200,8 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt ...@@ -188,6 +200,8 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
void initState() { void initState() {
super.initState(); super.initState();
isFocused = false;
_tap = TapGestureRecognizer() _tap = TapGestureRecognizer()
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTapUp = _handleTapUp ..onTapUp = _handleTapUp
...@@ -253,7 +267,7 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt ...@@ -253,7 +267,7 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
_reactionController.forward(); _reactionController.forward();
} }
void _handleTap() { void _handleTap([Intent? _]) {
if (isInteractive) { if (isInteractive) {
widget.onChanged!(!widget.value); widget.onChanged!(!widget.value);
_emitVibration(); _emitVibration();
...@@ -322,9 +336,19 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt ...@@ -322,9 +336,19 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
} }
} }
void _onShowFocusHighlight(bool showHighlight) {
setState(() { isFocused = showHighlight; });
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final CupertinoThemeData theme = CupertinoTheme.of(context); final CupertinoThemeData theme = CupertinoTheme.of(context);
final Color activeColor = CupertinoDynamicColor.resolve(
widget.activeColor
?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null)
?? CupertinoColors.systemGreen,
context,
);
if (needsPositionAnimation) { if (needsPositionAnimation) {
_resumePositionAnimation(); _resumePositionAnimation();
} }
...@@ -332,21 +356,31 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt ...@@ -332,21 +356,31 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
cursor: isInteractive && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, cursor: isInteractive && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
child: Opacity( child: Opacity(
opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0, opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0,
child: FocusableActionDetector(
onShowFocusHighlight: _onShowFocusHighlight,
actions: _actionMap,
enabled: isInteractive,
child: _CupertinoSwitchRenderObjectWidget( child: _CupertinoSwitchRenderObjectWidget(
value: widget.value, value: widget.value,
activeColor: CupertinoDynamicColor.resolve( activeColor: activeColor,
widget.activeColor
?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null)
?? CupertinoColors.systemGreen,
context,
),
trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context), trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context),
thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor ?? CupertinoColors.white, context), thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor ?? CupertinoColors.white, context),
// Opacity, lightness, and saturation values were aproximated with
// color pickers on the switches in the macOS settings.
focusColor: CupertinoDynamicColor.resolve(
widget.focusColor ??
HSLColor
.fromColor(activeColor.withOpacity(0.80))
.withLightness(0.69).withSaturation(0.835)
.toColor(),
context),
onChanged: widget.onChanged, onChanged: widget.onChanged,
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
isFocused: isFocused,
state: this, state: this,
), ),
), ),
),
); );
} }
...@@ -367,8 +401,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -367,8 +401,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
required this.activeColor, required this.activeColor,
required this.trackColor, required this.trackColor,
required this.thumbColor, required this.thumbColor,
required this.focusColor,
required this.onChanged, required this.onChanged,
required this.textDirection, required this.textDirection,
required this.isFocused,
required this.state, required this.state,
}); });
...@@ -376,9 +412,11 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -376,9 +412,11 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Color activeColor; final Color activeColor;
final Color trackColor; final Color trackColor;
final Color thumbColor; final Color thumbColor;
final Color focusColor;
final ValueChanged<bool>? onChanged; final ValueChanged<bool>? onChanged;
final _CupertinoSwitchState state; final _CupertinoSwitchState state;
final TextDirection textDirection; final TextDirection textDirection;
final bool isFocused;
@override @override
_RenderCupertinoSwitch createRenderObject(BuildContext context) { _RenderCupertinoSwitch createRenderObject(BuildContext context) {
...@@ -387,8 +425,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -387,8 +425,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
activeColor: activeColor, activeColor: activeColor,
trackColor: trackColor, trackColor: trackColor,
thumbColor: thumbColor, thumbColor: thumbColor,
focusColor: focusColor,
onChanged: onChanged, onChanged: onChanged,
textDirection: textDirection, textDirection: textDirection,
isFocused: isFocused,
state: state, state: state,
); );
} }
...@@ -401,8 +441,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -401,8 +441,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
..activeColor = activeColor ..activeColor = activeColor
..trackColor = trackColor ..trackColor = trackColor
..thumbColor = thumbColor ..thumbColor = thumbColor
..focusColor = focusColor
..onChanged = onChanged ..onChanged = onChanged
..textDirection = textDirection; ..textDirection = textDirection
..isFocused = isFocused;
} }
} }
...@@ -426,8 +468,10 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -426,8 +468,10 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
required Color activeColor, required Color activeColor,
required Color trackColor, required Color trackColor,
required Color thumbColor, required Color thumbColor,
required Color focusColor,
ValueChanged<bool>? onChanged, ValueChanged<bool>? onChanged,
required TextDirection textDirection, required TextDirection textDirection,
required bool isFocused,
required _CupertinoSwitchState state, required _CupertinoSwitchState state,
}) : assert(value != null), }) : assert(value != null),
assert(activeColor != null), assert(activeColor != null),
...@@ -435,9 +479,11 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -435,9 +479,11 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
_value = value, _value = value,
_activeColor = activeColor, _activeColor = activeColor,
_trackColor = trackColor, _trackColor = trackColor,
_focusColor = focusColor,
_thumbPainter = CupertinoThumbPainter.switchThumb(color: thumbColor), _thumbPainter = CupertinoThumbPainter.switchThumb(color: thumbColor),
_onChanged = onChanged, _onChanged = onChanged,
_textDirection = textDirection, _textDirection = textDirection,
_isFocused = isFocused,
_state = state, _state = state,
super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) { super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) {
state.position.addListener(markNeedsPaint); state.position.addListener(markNeedsPaint);
...@@ -490,6 +536,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -490,6 +536,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
markNeedsPaint(); markNeedsPaint();
} }
Color get focusColor => _focusColor;
Color _focusColor;
set focusColor(Color value) {
assert(value != null);
if (value == _focusColor) {
return;
}
_focusColor = value;
markNeedsPaint();
}
ValueChanged<bool>? get onChanged => _onChanged; ValueChanged<bool>? get onChanged => _onChanged;
ValueChanged<bool>? _onChanged; ValueChanged<bool>? _onChanged;
set onChanged(ValueChanged<bool>? value) { set onChanged(ValueChanged<bool>? value) {
...@@ -515,6 +572,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -515,6 +572,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
markNeedsPaint(); markNeedsPaint();
} }
bool get isFocused => _isFocused;
bool _isFocused;
set isFocused(bool value) {
assert(value != null);
if(value == _isFocused) {
return;
}
_isFocused = value;
markNeedsPaint();
}
bool get isInteractive => onChanged != null; bool get isInteractive => onChanged != null;
@override @override
...@@ -570,6 +638,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -570,6 +638,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius)); final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
canvas.drawRRect(trackRRect, paint); canvas.drawRRect(trackRRect, paint);
if(_isFocused) {
// Paints a border around the switch in the focus color.
final RRect borderTrackRRect = trackRRect.inflate(1.75);
final Paint borderPaint = Paint()
..color = focusColor
..style = PaintingStyle.stroke
..strokeWidth = 3.5;
canvas.drawRRect(borderTrackRRect, borderPaint);
}
final double currentThumbExtension = CupertinoThumbPainter.extension * currentReactionValue; final double currentThumbExtension = CupertinoThumbPainter.extension * currentReactionValue;
final double thumbLeft = lerpDouble( final double thumbLeft = lerpDouble(
trackRect.left + _kTrackInnerStart - CupertinoThumbPainter.radius, trackRect.left + _kTrackInnerStart - CupertinoThumbPainter.radius,
......
...@@ -48,6 +48,39 @@ void main() { ...@@ -48,6 +48,39 @@ void main() {
expect(value, isTrue); expect(value, isTrue);
}); });
testWidgets('CupertinoSwitch can be toggled by keyboard shortcuts', (WidgetTester tester) async {
bool value = true;
Widget buildApp({bool enabled = true}) {
return CupertinoApp(
home: CupertinoPageScaffold(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return CupertinoSwitch(
value: value,
onChanged: enabled ? (bool newValue) {
setState(() {
value = newValue;
});
} : null,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(value, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(value, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(value, isTrue);
});
testWidgets('Switch emits light haptic vibration on tap', (WidgetTester tester) async { testWidgets('Switch emits light haptic vibration on tap', (WidgetTester tester) async {
final Key switchKey = UniqueKey(); final Key switchKey = UniqueKey();
bool value = false; bool value = false;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment