Skip to content

Commit

Permalink
Add focus detector to CupertinoSwitch (#118345)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MitchellGoodwin committed Jan 19, 2023
1 parent b9ab640 commit ea36b3a
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#
# 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)
23 changes: 23 additions & 0 deletions examples/flutter_view/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#
# 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)
23 changes: 23 additions & 0 deletions examples/platform_view/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#
# 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)
108 changes: 94 additions & 14 deletions packages/flutter/lib/src/cupertino/switch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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].
///
Expand Down Expand Up @@ -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;
Expand All @@ -188,6 +200,8 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
void initState() {
super.initState();

isFocused = false;

_tap = TapGestureRecognizer()
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -322,29 +336,49 @@ 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();
}
return MouseRegion(
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,
),
),
);
Expand All @@ -367,18 +401,22 @@ 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,
});

final bool value;
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) {
Expand All @@ -387,8 +425,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
activeColor: activeColor,
trackColor: trackColor,
thumbColor: thumbColor,
focusColor: focusColor,
onChanged: onChanged,
textDirection: textDirection,
isFocused: isFocused,
state: state,
);
}
Expand All @@ -401,8 +441,10 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
..activeColor = activeColor
..trackColor = trackColor
..thumbColor = thumbColor
..focusColor = focusColor
..onChanged = onChanged
..textDirection = textDirection;
..textDirection = textDirection
..isFocused = isFocused;
}
}

Expand All @@ -426,18 +468,22 @@ 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),
assert(state != null),
_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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions packages/flutter/test/cupertino/switch_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit ea36b3a

Please sign in to comment.