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 {
this.trackColor,
this.thumbColor,
this.applyTheme,
this.focusColor,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(value != null),
assert(dragStartBehavior != null);
......@@ -125,6 +126,11 @@ class CupertinoSwitch extends StatefulWidget {
/// Defaults to [CupertinoColors.white] when null.
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}
/// Whether to apply the ambient [CupertinoThemeData].
///
......@@ -178,8 +184,14 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
late AnimationController _reactionController;
late Animation<double> _reaction;
late bool isFocused;
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
// switch must be animated to the position indicated by the widget's value.
bool needsPositionAnimation = false;
......@@ -188,6 +200,8 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
void initState() {
super.initState();
isFocused = false;
_tap = TapGestureRecognizer()
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
......@@ -253,7 +267,7 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
_reactionController.forward();
}
void _handleTap() {
void _handleTap([Intent? _]) {
if (isInteractive) {
widget.onChanged!(!widget.value);
_emitVibration();
......@@ -322,9 +336,19 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
}
}
void _onShowFocusHighlight(bool showHighlight) {
setState(() { isFocused = showHighlight; });
}
@override
Widget build(BuildContext 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) {
_resumePositionAnimation();
}
......@@ -332,19 +356,29 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
cursor: isInteractive && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
child: Opacity(
opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0,
child: _CupertinoSwitchRenderObjectWidget(
value: widget.value,
activeColor: CupertinoDynamicColor.resolve(
widget.activeColor
?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null)
?? CupertinoColors.systemGreen,
context,
child: FocusableActionDetector(
onShowFocusHighlight: _onShowFocusHighlight,
actions: _actionMap,
enabled: isInteractive,
child: _CupertinoSwitchRenderObjectWidget(
value: widget.value,
activeColor: activeColor,
trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, 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,
textDirection: Directionality.of(context),
isFocused: isFocused,
state: this,
),
trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context),
thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor ?? CupertinoColors.white, context),
onChanged: widget.onChanged,
textDirection: Directionality.of(context),
state: this,
),
),
);
......@@ -367,8 +401,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
required this.activeColor,
required this.trackColor,
required this.thumbColor,
required this.focusColor,
required this.onChanged,
required this.textDirection,
required this.isFocused,
required this.state,
});
......@@ -376,9 +412,11 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Color activeColor;
final Color trackColor;
final Color thumbColor;
final Color focusColor;
final ValueChanged<bool>? onChanged;
final _CupertinoSwitchState state;
final TextDirection textDirection;
final bool isFocused;
@override
_RenderCupertinoSwitch createRenderObject(BuildContext context) {
......@@ -387,8 +425,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
activeColor: activeColor,
trackColor: trackColor,
thumbColor: thumbColor,
focusColor: focusColor,
onChanged: onChanged,
textDirection: textDirection,
isFocused: isFocused,
state: state,
);
}
......@@ -401,8 +441,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
..activeColor = activeColor
..trackColor = trackColor
..thumbColor = thumbColor
..focusColor = focusColor
..onChanged = onChanged
..textDirection = textDirection;
..textDirection = textDirection
..isFocused = isFocused;
}
}
......@@ -426,8 +468,10 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
required Color activeColor,
required Color trackColor,
required Color thumbColor,
required Color focusColor,
ValueChanged<bool>? onChanged,
required TextDirection textDirection,
required bool isFocused,
required _CupertinoSwitchState state,
}) : assert(value != null),
assert(activeColor != null),
......@@ -435,9 +479,11 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
_value = value,
_activeColor = activeColor,
_trackColor = trackColor,
_focusColor = focusColor,
_thumbPainter = CupertinoThumbPainter.switchThumb(color: thumbColor),
_onChanged = onChanged,
_textDirection = textDirection,
_isFocused = isFocused,
_state = state,
super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) {
state.position.addListener(markNeedsPaint);
......@@ -490,6 +536,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
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>? _onChanged;
set onChanged(ValueChanged<bool>? value) {
......@@ -515,6 +572,17 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
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;
@override
......@@ -570,6 +638,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
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 thumbLeft = lerpDouble(
trackRect.left + _kTrackInnerStart - CupertinoThumbPainter.radius,
......
......@@ -48,6 +48,39 @@ void main() {
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 {
final Key switchKey = UniqueKey();
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