mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Add focus nodes, hover, and shortcuts to switches, checkboxes, and radio buttons. (#43213)
In order to enable keyboard navigation to more controls, I've added focus nodes to switches, checkboxes, and radio buttons. In addition, this change enables mouse hover over these controls. - Added tests for focus, hover, and shortcut activation for Checkbox, Radio, and Switch.
This commit is contained in:
parent
320a933d9e
commit
900067207e
|
@ -4,6 +4,8 @@
|
|||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
@ -52,7 +54,7 @@ class Checkbox extends StatefulWidget {
|
|||
/// * [onChanged], which is called when the value of the checkbox should
|
||||
/// change. It can be set to null to disable the checkbox.
|
||||
///
|
||||
/// The value of [tristate] must not be null.
|
||||
/// The values of [tristate] and [autofocus] must not be null.
|
||||
const Checkbox({
|
||||
Key key,
|
||||
@required this.value,
|
||||
|
@ -60,9 +62,14 @@ class Checkbox extends StatefulWidget {
|
|||
@required this.onChanged,
|
||||
this.activeColor,
|
||||
this.checkColor,
|
||||
this.focusColor,
|
||||
this.hoverColor,
|
||||
this.materialTapTargetSize,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : assert(tristate != null),
|
||||
assert(tristate || value != null),
|
||||
assert(autofocus != null),
|
||||
super(key: key);
|
||||
|
||||
/// Whether this checkbox is checked.
|
||||
|
@ -113,10 +120,10 @@ class Checkbox extends StatefulWidget {
|
|||
///
|
||||
/// Checkbox displays a dash when its value is null.
|
||||
///
|
||||
/// When a tri-state checkbox is tapped its [onChanged] callback will be
|
||||
/// applied to true if the current value is null or false, false otherwise.
|
||||
/// Typically tri-state checkboxes are disabled (the onChanged callback is
|
||||
/// null) so they don't respond to taps.
|
||||
/// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged]
|
||||
/// callback will be applied to true if the current value is false, to null if
|
||||
/// value is true, and to false if value is null (i.e. it cycles through false
|
||||
/// => true => null => false when tapped).
|
||||
///
|
||||
/// If tristate is false (the default), [value] must not be null.
|
||||
final bool tristate;
|
||||
|
@ -130,6 +137,18 @@ class Checkbox extends StatefulWidget {
|
|||
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
||||
final MaterialTapTargetSize materialTapTargetSize;
|
||||
|
||||
/// The color for the checkbox's [Material] when it has the input focus.
|
||||
final Color focusColor;
|
||||
|
||||
/// The color for the checkbox's [Material] when a pointer is hovering over it.
|
||||
final Color hoverColor;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// The width of a checkbox widget.
|
||||
static const double width = 18.0;
|
||||
|
||||
|
@ -138,6 +157,68 @@ class Checkbox extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
|
||||
bool get enabled => widget.onChanged != null;
|
||||
Map<LocalKey, ActionFactory> _actionMap;
|
||||
bool _showHighlight = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_actionMap = <LocalKey, ActionFactory>{
|
||||
SelectAction.key: _createAction,
|
||||
if (!kIsWeb) ActivateAction.key: _createAction,
|
||||
};
|
||||
_updateHighlightMode(WidgetsBinding.instance.focusManager.highlightMode);
|
||||
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
|
||||
}
|
||||
|
||||
void _actionHandler(FocusNode node, Intent intent){
|
||||
if (widget.onChanged != null) {
|
||||
switch (widget.value) {
|
||||
case false:
|
||||
widget.onChanged(true);
|
||||
break;
|
||||
case true:
|
||||
widget.onChanged(widget.tristate ? null : false);
|
||||
break;
|
||||
default: // case null:
|
||||
widget.onChanged(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
final RenderObject renderObject = node.context.findRenderObject();
|
||||
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
||||
}
|
||||
|
||||
Action _createAction() {
|
||||
return CallbackAction(
|
||||
SelectAction.key,
|
||||
onInvoke: _actionHandler,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateHighlightMode(FocusHighlightMode mode) {
|
||||
switch (WidgetsBinding.instance.focusManager.highlightMode) {
|
||||
case FocusHighlightMode.touch:
|
||||
_showHighlight = false;
|
||||
break;
|
||||
case FocusHighlightMode.traditional:
|
||||
_showHighlight = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() { _updateHighlightMode(mode); });
|
||||
}
|
||||
|
||||
bool hovering = false;
|
||||
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
|
||||
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
|
@ -152,15 +233,36 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
|
|||
break;
|
||||
}
|
||||
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
|
||||
return _CheckboxRenderObjectWidget(
|
||||
value: widget.value,
|
||||
tristate: widget.tristate,
|
||||
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
||||
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
|
||||
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
|
||||
onChanged: widget.onChanged,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: this,
|
||||
return MouseRegion(
|
||||
onEnter: enabled ? _handleMouseEnter : null,
|
||||
onExit: enabled ? _handleMouseExit : null,
|
||||
child: Actions(
|
||||
actions: _actionMap,
|
||||
child: Focus(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
canRequestFocus: enabled,
|
||||
debugLabel: '${describeIdentity(widget)}(${widget.value})',
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return _CheckboxRenderObjectWidget(
|
||||
value: widget.value,
|
||||
tristate: widget.tristate,
|
||||
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
||||
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
|
||||
inactiveColor: enabled ? themeData.unselectedWidgetColor : themeData.disabledColor,
|
||||
focusColor: widget.focusColor ?? themeData.focusColor,
|
||||
hoverColor: widget.hoverColor ?? themeData.hoverColor,
|
||||
onChanged: widget.onChanged,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: this,
|
||||
hasFocus: enabled && _showHighlight && Focus.of(context).hasFocus,
|
||||
hovering: enabled && _showHighlight && hovering,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -173,9 +275,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
@required this.activeColor,
|
||||
@required this.checkColor,
|
||||
@required this.inactiveColor,
|
||||
@required this.focusColor,
|
||||
@required this.hoverColor,
|
||||
@required this.onChanged,
|
||||
@required this.vsync,
|
||||
@required this.additionalConstraints,
|
||||
@required this.hasFocus,
|
||||
@required this.hovering,
|
||||
}) : assert(tristate != null),
|
||||
assert(tristate || value != null),
|
||||
assert(activeColor != null),
|
||||
|
@ -185,9 +291,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
|
||||
final bool value;
|
||||
final bool tristate;
|
||||
final bool hasFocus;
|
||||
final bool hovering;
|
||||
final Color activeColor;
|
||||
final Color checkColor;
|
||||
final Color inactiveColor;
|
||||
final Color focusColor;
|
||||
final Color hoverColor;
|
||||
final ValueChanged<bool> onChanged;
|
||||
final TickerProvider vsync;
|
||||
final BoxConstraints additionalConstraints;
|
||||
|
@ -199,9 +309,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
activeColor: activeColor,
|
||||
checkColor: checkColor,
|
||||
inactiveColor: inactiveColor,
|
||||
focusColor: focusColor,
|
||||
hoverColor: hoverColor,
|
||||
onChanged: onChanged,
|
||||
vsync: vsync,
|
||||
additionalConstraints: additionalConstraints,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -212,9 +326,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
..activeColor = activeColor
|
||||
..checkColor = checkColor
|
||||
..inactiveColor = inactiveColor
|
||||
..focusColor = focusColor
|
||||
..hoverColor = hoverColor
|
||||
..onChanged = onChanged
|
||||
..additionalConstraints = additionalConstraints
|
||||
..vsync = vsync;
|
||||
..vsync = vsync
|
||||
..hasFocus = hasFocus
|
||||
..hovering = hovering;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,8 +347,12 @@ class _RenderCheckbox extends RenderToggleable {
|
|||
Color activeColor,
|
||||
this.checkColor,
|
||||
Color inactiveColor,
|
||||
Color focusColor,
|
||||
Color hoverColor,
|
||||
BoxConstraints additionalConstraints,
|
||||
ValueChanged<bool> onChanged,
|
||||
bool hasFocus,
|
||||
bool hovering,
|
||||
@required TickerProvider vsync,
|
||||
}) : _oldValue = value,
|
||||
super(
|
||||
|
@ -238,9 +360,13 @@ class _RenderCheckbox extends RenderToggleable {
|
|||
tristate: tristate,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
focusColor: focusColor,
|
||||
hoverColor: hoverColor,
|
||||
onChanged: onChanged,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: vsync,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
);
|
||||
|
||||
bool _oldValue;
|
||||
|
|
|
@ -489,20 +489,25 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
|||
|
||||
bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
|
||||
|
||||
void _handleAction(FocusNode node, Intent intent) {
|
||||
_startSplash(context: node.context);
|
||||
_handleTap(node.context);
|
||||
}
|
||||
|
||||
Action _createAction() {
|
||||
return CallbackAction(
|
||||
ActivateAction.key,
|
||||
onInvoke: (FocusNode node, Intent intent) {
|
||||
_startSplash(context: node.context);
|
||||
_handleTap(node.context);
|
||||
},
|
||||
onInvoke: _handleAction,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_actionMap = <LocalKey, ActionFactory>{ ActivateAction.key: _createAction };
|
||||
_actionMap = <LocalKey, ActionFactory>{
|
||||
SelectAction.key: _createAction,
|
||||
if (!kIsWeb) ActivateAction.key: _createAction,
|
||||
};
|
||||
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
@ -107,8 +109,13 @@ class Radio<T> extends StatefulWidget {
|
|||
@required this.groupValue,
|
||||
@required this.onChanged,
|
||||
this.activeColor,
|
||||
this.focusColor,
|
||||
this.hoverColor,
|
||||
this.materialTapTargetSize,
|
||||
}) : super(key: key);
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : assert(autofocus != null),
|
||||
super(key: key);
|
||||
|
||||
/// The value represented by this radio button.
|
||||
final T value;
|
||||
|
@ -161,15 +168,77 @@ class Radio<T> extends StatefulWidget {
|
|||
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
||||
final MaterialTapTargetSize materialTapTargetSize;
|
||||
|
||||
/// The color for the radio's [Material] when it has the input focus.
|
||||
final Color focusColor;
|
||||
|
||||
/// The color for the radio's [Material] when a pointer is hovering over it.
|
||||
final Color hoverColor;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
_RadioState<T> createState() => _RadioState<T>();
|
||||
}
|
||||
|
||||
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
||||
bool get _enabled => widget.onChanged != null;
|
||||
bool get enabled => widget.onChanged != null;
|
||||
Map<LocalKey, ActionFactory> _actionMap;
|
||||
bool _showHighlight = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_actionMap = <LocalKey, ActionFactory>{
|
||||
SelectAction.key: _createAction,
|
||||
if (!kIsWeb) ActivateAction.key: _createAction,
|
||||
};
|
||||
_updateHighlightMode(WidgetsBinding.instance.focusManager.highlightMode);
|
||||
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
|
||||
}
|
||||
|
||||
void _actionHandler(FocusNode node, Intent intent){
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged(widget.value);
|
||||
}
|
||||
final RenderObject renderObject = node.context.findRenderObject();
|
||||
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
||||
}
|
||||
|
||||
Action _createAction() {
|
||||
return CallbackAction(
|
||||
SelectAction.key,
|
||||
onInvoke: _actionHandler,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateHighlightMode(FocusHighlightMode mode) {
|
||||
switch (WidgetsBinding.instance.focusManager.highlightMode) {
|
||||
case FocusHighlightMode.touch:
|
||||
_showHighlight = false;
|
||||
break;
|
||||
case FocusHighlightMode.traditional:
|
||||
_showHighlight = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() { _updateHighlightMode(mode); });
|
||||
}
|
||||
|
||||
bool hovering = false;
|
||||
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
|
||||
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
|
||||
|
||||
Color _getInactiveColor(ThemeData themeData) {
|
||||
return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
|
||||
return enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
|
||||
}
|
||||
|
||||
void _handleChanged(bool selected) {
|
||||
|
@ -191,13 +260,34 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
|||
break;
|
||||
}
|
||||
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
|
||||
return _RadioRenderObjectWidget(
|
||||
selected: widget.value == widget.groupValue,
|
||||
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
||||
inactiveColor: _getInactiveColor(themeData),
|
||||
onChanged: _enabled ? _handleChanged : null,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: this,
|
||||
return MouseRegion(
|
||||
onEnter: enabled ? _handleMouseEnter : null,
|
||||
onExit: enabled ? _handleMouseExit : null,
|
||||
child: Actions(
|
||||
actions: _actionMap,
|
||||
child: Focus(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
canRequestFocus: enabled,
|
||||
debugLabel: '${describeIdentity(widget)}(${widget.value})',
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return _RadioRenderObjectWidget(
|
||||
selected: widget.value == widget.groupValue,
|
||||
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
||||
inactiveColor: _getInactiveColor(themeData),
|
||||
focusColor: widget.focusColor ?? themeData.focusColor,
|
||||
hoverColor: widget.hoverColor ?? themeData.hoverColor,
|
||||
onChanged: enabled ? _handleChanged : null,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: this,
|
||||
hasFocus: enabled && _showHighlight && Focus.of(context).hasFocus,
|
||||
hovering: enabled && _showHighlight && hovering,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -208,9 +298,13 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
@required this.selected,
|
||||
@required this.activeColor,
|
||||
@required this.inactiveColor,
|
||||
@required this.focusColor,
|
||||
@required this.hoverColor,
|
||||
@required this.additionalConstraints,
|
||||
this.onChanged,
|
||||
@required this.vsync,
|
||||
@required this.hasFocus,
|
||||
@required this.hovering,
|
||||
}) : assert(selected != null),
|
||||
assert(activeColor != null),
|
||||
assert(inactiveColor != null),
|
||||
|
@ -218,8 +312,12 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
super(key: key);
|
||||
|
||||
final bool selected;
|
||||
final bool hasFocus;
|
||||
final bool hovering;
|
||||
final Color inactiveColor;
|
||||
final Color activeColor;
|
||||
final Color focusColor;
|
||||
final Color hoverColor;
|
||||
final ValueChanged<bool> onChanged;
|
||||
final TickerProvider vsync;
|
||||
final BoxConstraints additionalConstraints;
|
||||
|
@ -229,9 +327,13 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
value: selected,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
focusColor: focusColor,
|
||||
hoverColor: hoverColor,
|
||||
onChanged: onChanged,
|
||||
vsync: vsync,
|
||||
additionalConstraints: additionalConstraints,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -240,9 +342,13 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
..value = selected
|
||||
..activeColor = activeColor
|
||||
..inactiveColor = inactiveColor
|
||||
..focusColor = focusColor
|
||||
..hoverColor = hoverColor
|
||||
..onChanged = onChanged
|
||||
..additionalConstraints = additionalConstraints
|
||||
..vsync = vsync;
|
||||
..vsync = vsync
|
||||
..hasFocus = hasFocus
|
||||
..hovering = hovering;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,17 +357,25 @@ class _RenderRadio extends RenderToggleable {
|
|||
bool value,
|
||||
Color activeColor,
|
||||
Color inactiveColor,
|
||||
Color focusColor,
|
||||
Color hoverColor,
|
||||
ValueChanged<bool> onChanged,
|
||||
BoxConstraints additionalConstraints,
|
||||
@required TickerProvider vsync,
|
||||
bool hasFocus,
|
||||
bool hovering,
|
||||
}) : super(
|
||||
value: value,
|
||||
tristate: false,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
focusColor: focusColor,
|
||||
hoverColor: hoverColor,
|
||||
onChanged: onChanged,
|
||||
additionalConstraints: additionalConstraints,
|
||||
vsync: vsync,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
@ -74,9 +74,13 @@ class Switch extends StatefulWidget {
|
|||
this.inactiveThumbImage,
|
||||
this.materialTapTargetSize,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
}) : _switchType = _SwitchType.material,
|
||||
assert(dragStartBehavior != null),
|
||||
super(key: key);
|
||||
this.focusColor,
|
||||
this.hoverColor,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : _switchType = _SwitchType.material,
|
||||
assert(dragStartBehavior != null),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a [CupertinoSwitch] if the target platform is iOS, creates a
|
||||
/// material design switch otherwise.
|
||||
|
@ -98,8 +102,13 @@ class Switch extends StatefulWidget {
|
|||
this.inactiveThumbImage,
|
||||
this.materialTapTargetSize,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
}) : _switchType = _SwitchType.adaptive,
|
||||
super(key: key);
|
||||
this.focusColor,
|
||||
this.hoverColor,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : assert(autofocus != null),
|
||||
_switchType = _SwitchType.adaptive,
|
||||
super(key: key);
|
||||
|
||||
/// Whether this switch is on or off.
|
||||
///
|
||||
|
@ -180,6 +189,18 @@ class Switch extends StatefulWidget {
|
|||
/// {@macro flutter.cupertino.switch.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// The color for the button's [Material] when it has the input focus.
|
||||
final Color focusColor;
|
||||
|
||||
/// The color for the button's [Material] when a pointer is hovering over it.
|
||||
final Color hoverColor;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
_SwitchState createState() => _SwitchState();
|
||||
|
||||
|
@ -192,6 +213,53 @@ class Switch extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
||||
Map<LocalKey, ActionFactory> _actionMap;
|
||||
bool _showHighlight = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_actionMap = <LocalKey, ActionFactory>{
|
||||
SelectAction.key: _createAction,
|
||||
if (!kIsWeb) ActivateAction.key: _createAction,
|
||||
};
|
||||
_updateHighlightMode(WidgetsBinding.instance.focusManager.highlightMode);
|
||||
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
|
||||
}
|
||||
|
||||
void _actionHandler(FocusNode node, Intent intent){
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged(!widget.value);
|
||||
}
|
||||
final RenderObject renderObject = node.context.findRenderObject();
|
||||
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
||||
}
|
||||
|
||||
Action _createAction() {
|
||||
return CallbackAction(
|
||||
SelectAction.key,
|
||||
onInvoke: _actionHandler,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateHighlightMode(FocusHighlightMode mode) {
|
||||
switch (WidgetsBinding.instance.focusManager.highlightMode) {
|
||||
case FocusHighlightMode.touch:
|
||||
_showHighlight = false;
|
||||
break;
|
||||
case FocusHighlightMode.traditional:
|
||||
_showHighlight = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() { _updateHighlightMode(mode); });
|
||||
}
|
||||
|
||||
Size getSwitchSize(ThemeData theme) {
|
||||
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
|
||||
case MaterialTapTargetSize.padded:
|
||||
|
@ -205,6 +273,12 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|||
return null;
|
||||
}
|
||||
|
||||
bool get enabled => widget.onChanged != null;
|
||||
|
||||
bool hovering = false;
|
||||
void _handleMouseEnter(PointerEnterEvent event) => setState(() { hovering = true; });
|
||||
void _handleMouseExit(PointerExitEvent event) => setState(() { hovering = false; });
|
||||
|
||||
Widget buildMaterialSwitch(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
@ -212,10 +286,12 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|||
|
||||
final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor;
|
||||
final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80);
|
||||
final Color hoverColor = widget.hoverColor ?? theme.hoverColor;
|
||||
final Color focusColor = widget.focusColor ?? theme.focusColor;
|
||||
|
||||
Color inactiveThumbColor;
|
||||
Color inactiveTrackColor;
|
||||
if (widget.onChanged != null) {
|
||||
if (enabled) {
|
||||
const Color black32 = Color(0x52000000); // Black with 32% opacity
|
||||
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
|
||||
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
|
||||
|
@ -224,33 +300,59 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|||
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
|
||||
}
|
||||
|
||||
return _SwitchRenderObjectWidget(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
value: widget.value,
|
||||
activeColor: activeThumbColor,
|
||||
inactiveColor: inactiveThumbColor,
|
||||
activeThumbImage: widget.activeThumbImage,
|
||||
inactiveThumbImage: widget.inactiveThumbImage,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
configuration: createLocalImageConfiguration(context),
|
||||
onChanged: widget.onChanged,
|
||||
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
|
||||
vsync: this,
|
||||
return MouseRegion(
|
||||
onEnter: enabled ? _handleMouseEnter : null,
|
||||
onExit: enabled ? _handleMouseExit : null,
|
||||
child: Actions(
|
||||
actions: _actionMap,
|
||||
child: Focus(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
canRequestFocus: enabled,
|
||||
debugLabel: '${describeIdentity(widget)}({$widget.value})',
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool hasFocus = Focus.of(context).hasFocus;
|
||||
return _SwitchRenderObjectWidget(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
value: widget.value,
|
||||
activeColor: activeThumbColor,
|
||||
inactiveColor: inactiveThumbColor,
|
||||
hoverColor: hoverColor,
|
||||
focusColor: focusColor,
|
||||
activeThumbImage: widget.activeThumbImage,
|
||||
inactiveThumbImage: widget.inactiveThumbImage,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
configuration: createLocalImageConfiguration(context),
|
||||
onChanged: widget.onChanged,
|
||||
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
|
||||
hasFocus: enabled && _showHighlight && hasFocus,
|
||||
hovering: enabled && _showHighlight && hovering,
|
||||
vsync: this,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCupertinoSwitch(BuildContext context) {
|
||||
final Size size = getSwitchSize(Theme.of(context));
|
||||
return Container(
|
||||
width: size.width, // Same size as the Material switch.
|
||||
height: size.height,
|
||||
alignment: Alignment.center,
|
||||
child: CupertinoSwitch(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
value: widget.value,
|
||||
onChanged: widget.onChanged,
|
||||
activeColor: widget.activeColor,
|
||||
return Focus(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
child: Container(
|
||||
width: size.width, // Same size as the Material switch.
|
||||
height: size.height,
|
||||
alignment: Alignment.center,
|
||||
child: CupertinoSwitch(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
value: widget.value,
|
||||
onChanged: widget.onChanged,
|
||||
activeColor: widget.activeColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -284,6 +386,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
this.value,
|
||||
this.activeColor,
|
||||
this.inactiveColor,
|
||||
this.hoverColor,
|
||||
this.focusColor,
|
||||
this.activeThumbImage,
|
||||
this.inactiveThumbImage,
|
||||
this.activeTrackColor,
|
||||
|
@ -293,11 +397,15 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
this.vsync,
|
||||
this.additionalConstraints,
|
||||
this.dragStartBehavior,
|
||||
this.hasFocus,
|
||||
this.hovering,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool value;
|
||||
final Color activeColor;
|
||||
final Color inactiveColor;
|
||||
final Color hoverColor;
|
||||
final Color focusColor;
|
||||
final ImageProvider activeThumbImage;
|
||||
final ImageProvider inactiveThumbImage;
|
||||
final Color activeTrackColor;
|
||||
|
@ -307,6 +415,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
final TickerProvider vsync;
|
||||
final BoxConstraints additionalConstraints;
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
final bool hasFocus;
|
||||
final bool hovering;
|
||||
|
||||
@override
|
||||
_RenderSwitch createRenderObject(BuildContext context) {
|
||||
|
@ -315,6 +425,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
value: value,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
hoverColor: hoverColor,
|
||||
focusColor: focusColor,
|
||||
activeThumbImage: activeThumbImage,
|
||||
inactiveThumbImage: inactiveThumbImage,
|
||||
activeTrackColor: activeTrackColor,
|
||||
|
@ -323,6 +435,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
onChanged: onChanged,
|
||||
textDirection: Directionality.of(context),
|
||||
additionalConstraints: additionalConstraints,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
vsync: vsync,
|
||||
);
|
||||
}
|
||||
|
@ -333,6 +447,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
..value = value
|
||||
..activeColor = activeColor
|
||||
..inactiveColor = inactiveColor
|
||||
..hoverColor = hoverColor
|
||||
..focusColor = focusColor
|
||||
..activeThumbImage = activeThumbImage
|
||||
..inactiveThumbImage = inactiveThumbImage
|
||||
..activeTrackColor = activeTrackColor
|
||||
|
@ -342,6 +458,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
..textDirection = Directionality.of(context)
|
||||
..additionalConstraints = additionalConstraints
|
||||
..dragStartBehavior = dragStartBehavior
|
||||
..hasFocus = hasFocus
|
||||
..hovering = hovering
|
||||
..vsync = vsync;
|
||||
}
|
||||
}
|
||||
|
@ -351,6 +469,8 @@ class _RenderSwitch extends RenderToggleable {
|
|||
bool value,
|
||||
Color activeColor,
|
||||
Color inactiveColor,
|
||||
Color hoverColor,
|
||||
Color focusColor,
|
||||
ImageProvider activeThumbImage,
|
||||
ImageProvider inactiveThumbImage,
|
||||
Color activeTrackColor,
|
||||
|
@ -359,8 +479,10 @@ class _RenderSwitch extends RenderToggleable {
|
|||
BoxConstraints additionalConstraints,
|
||||
@required TextDirection textDirection,
|
||||
ValueChanged<bool> onChanged,
|
||||
@required TickerProvider vsync,
|
||||
DragStartBehavior dragStartBehavior,
|
||||
bool hasFocus,
|
||||
bool hovering,
|
||||
@required TickerProvider vsync,
|
||||
}) : assert(textDirection != null),
|
||||
_activeThumbImage = activeThumbImage,
|
||||
_inactiveThumbImage = inactiveThumbImage,
|
||||
|
@ -373,8 +495,12 @@ class _RenderSwitch extends RenderToggleable {
|
|||
tristate: false,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
hoverColor: hoverColor,
|
||||
focusColor: focusColor,
|
||||
onChanged: onChanged,
|
||||
additionalConstraints: additionalConstraints,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
vsync: vsync,
|
||||
) {
|
||||
_drag = HorizontalDragGestureRecognizer()
|
||||
|
|
|
@ -10,9 +10,15 @@ import 'package:flutter/scheduler.dart';
|
|||
|
||||
import 'constants.dart';
|
||||
|
||||
// Duration of the animation that moves the toggle from one state to another.
|
||||
const Duration _kToggleDuration = Duration(milliseconds: 200);
|
||||
|
||||
// Radius of the radial reaction over time.
|
||||
final Animatable<double> _kRadialReactionRadiusTween = Tween<double>(begin: 0.0, end: kRadialReactionRadius);
|
||||
|
||||
// Duration of the fade animation for the reaction when focus and hover occur.
|
||||
const Duration _kReactionFadeDuration = Duration(milliseconds: 50);
|
||||
|
||||
/// A base class for material style toggleable controls with toggle animations.
|
||||
///
|
||||
/// This class handles storing the current value, dispatching ValueChanged on a
|
||||
|
@ -28,9 +34,13 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
bool tristate = false,
|
||||
@required Color activeColor,
|
||||
@required Color inactiveColor,
|
||||
Color hoverColor,
|
||||
Color focusColor,
|
||||
ValueChanged<bool> onChanged,
|
||||
BoxConstraints additionalConstraints,
|
||||
@required TickerProvider vsync,
|
||||
bool hasFocus = false,
|
||||
bool hovering = false,
|
||||
}) : assert(tristate != null),
|
||||
assert(tristate || value != null),
|
||||
assert(activeColor != null),
|
||||
|
@ -40,7 +50,11 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
_tristate = tristate,
|
||||
_activeColor = activeColor,
|
||||
_inactiveColor = inactiveColor,
|
||||
_hoverColor = hoverColor ?? activeColor.withAlpha(kRadialReactionAlpha),
|
||||
_focusColor = focusColor ?? activeColor.withAlpha(kRadialReactionAlpha),
|
||||
_onChanged = onChanged,
|
||||
_hasFocus = hasFocus,
|
||||
_hovering = hovering,
|
||||
_vsync = vsync,
|
||||
super(additionalConstraints: additionalConstraints) {
|
||||
_tap = TapGestureRecognizer()
|
||||
|
@ -66,6 +80,24 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
parent: _reactionController,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
)..addListener(markNeedsPaint);
|
||||
_reactionHoverFadeController = AnimationController(
|
||||
duration: _kReactionFadeDuration,
|
||||
value: hovering || hasFocus ? 1.0 : 0.0,
|
||||
vsync: vsync,
|
||||
);
|
||||
_reactionHoverFade = CurvedAnimation(
|
||||
parent: _reactionHoverFadeController,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
)..addListener(markNeedsPaint);
|
||||
_reactionFocusFadeController = AnimationController(
|
||||
duration: _kReactionFadeDuration,
|
||||
value: hovering || hasFocus ? 1.0 : 0.0,
|
||||
vsync: vsync,
|
||||
);
|
||||
_reactionFocusFade = CurvedAnimation(
|
||||
parent: _reactionFocusFadeController,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
)..addListener(markNeedsPaint);
|
||||
}
|
||||
|
||||
/// Used by subclasses to manipulate the visual value of the control.
|
||||
|
@ -102,6 +134,66 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
AnimationController _reactionController;
|
||||
Animation<double> _reaction;
|
||||
|
||||
/// Used by subclasses to control the radial reaction's opacity animation for
|
||||
/// [hasFocus] changes.
|
||||
///
|
||||
/// Some controls have a radial ink reaction to focus. This animation
|
||||
/// controller can be used to start or stop these ink reaction fade-ins and
|
||||
/// fade-outs.
|
||||
///
|
||||
/// Subclasses should call [paintRadialReaction] to actually paint the radial
|
||||
/// reaction.
|
||||
@protected
|
||||
AnimationController get reactionFocusFadeController => _reactionFocusFadeController;
|
||||
AnimationController _reactionFocusFadeController;
|
||||
Animation<double> _reactionFocusFade;
|
||||
|
||||
/// Used by subclasses to control the radial reaction's opacity animation for
|
||||
/// [hovering] changes.
|
||||
///
|
||||
/// Some controls have a radial ink reaction to pointer hover. This animation
|
||||
/// controller can be used to start or stop these ink reaction fade-ins and
|
||||
/// fade-outs.
|
||||
///
|
||||
/// Subclasses should call [paintRadialReaction] to actually paint the radial
|
||||
/// reaction.
|
||||
@protected
|
||||
AnimationController get reactionHoverFadeController => _reactionHoverFadeController;
|
||||
AnimationController _reactionHoverFadeController;
|
||||
Animation<double> _reactionHoverFade;
|
||||
|
||||
/// True if this toggleable has the input focus.
|
||||
bool get hasFocus => _hasFocus;
|
||||
bool _hasFocus;
|
||||
set hasFocus(bool value) {
|
||||
assert(value != null);
|
||||
if (value == _hasFocus)
|
||||
return;
|
||||
_hasFocus = value;
|
||||
if (_hasFocus) {
|
||||
_reactionFocusFadeController.forward();
|
||||
} else {
|
||||
_reactionFocusFadeController.reverse();
|
||||
}
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// True if this toggleable is being hovered over by a pointer.
|
||||
bool get hovering => _hovering;
|
||||
bool _hovering;
|
||||
set hovering(bool value) {
|
||||
assert(value != null);
|
||||
if (value == _hovering)
|
||||
return;
|
||||
_hovering = value;
|
||||
if (_hovering) {
|
||||
_reactionHoverFadeController.forward();
|
||||
} else {
|
||||
_reactionHoverFadeController.reverse();
|
||||
}
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// The [TickerProvider] for the [AnimationController]s that run the animations.
|
||||
TickerProvider get vsync => _vsync;
|
||||
TickerProvider _vsync;
|
||||
|
@ -192,6 +284,54 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// The color that should be used for the reaction when [hovering] is true.
|
||||
///
|
||||
/// Used when the toggleable needs to change the reaction color/transparency,
|
||||
/// when it is being hovered over.
|
||||
///
|
||||
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
|
||||
Color get hoverColor => _hoverColor;
|
||||
Color _hoverColor;
|
||||
set hoverColor(Color value) {
|
||||
assert(value != null);
|
||||
if (value == _hoverColor)
|
||||
return;
|
||||
_hoverColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// The color that should be used for the reaction when [hasFocus] is true.
|
||||
///
|
||||
/// Used when the toggleable needs to change the reaction color/transparency,
|
||||
/// when it has focus.
|
||||
///
|
||||
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
|
||||
Color get focusColor => _focusColor;
|
||||
Color _focusColor;
|
||||
set focusColor(Color value) {
|
||||
assert(value != null);
|
||||
if (value == _focusColor)
|
||||
return;
|
||||
_focusColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// The color that should be used for the reaction when drawn.
|
||||
///
|
||||
/// Used when the toggleable needs to change the reaction color/transparency
|
||||
/// that is displayed when the toggleable is toggled by a tap.
|
||||
///
|
||||
/// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
|
||||
Color get reactionColor => _reactionColor;
|
||||
Color _reactionColor;
|
||||
set reactionColor(Color value) {
|
||||
assert(value != null);
|
||||
if (value == _reactionColor)
|
||||
return;
|
||||
_reactionColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// Called when the control changes value.
|
||||
///
|
||||
/// If the control is tapped, [onChanged] is called immediately with the new
|
||||
|
@ -323,12 +463,18 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
/// point at which the user interacted with the control, which is handled
|
||||
/// automatically).
|
||||
void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
|
||||
if (!_reaction.isDismissed) {
|
||||
// TODO(abarth): We should have a different reaction color when position is zero.
|
||||
final Paint reactionPaint = Paint()..color = activeColor.withAlpha(kRadialReactionAlpha);
|
||||
if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) {
|
||||
final Paint reactionPaint = Paint()
|
||||
..color = Color.lerp(
|
||||
Color.lerp(activeColor.withAlpha(kRadialReactionAlpha), hoverColor, _reactionHoverFade.value),
|
||||
focusColor,
|
||||
_reactionFocusFade.value,
|
||||
);
|
||||
final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value);
|
||||
final double radius = _kRadialReactionRadiusTween.evaluate(_reaction);
|
||||
canvas.drawCircle(center + offset, radius, reactionPaint);
|
||||
final double reactionRadius = hasFocus || hovering
|
||||
? kRadialReactionRadius
|
||||
: _kRadialReactionRadiusTween.evaluate(_reaction);
|
||||
canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -376,8 +376,8 @@ class DoNothingAction extends Action {
|
|||
/// An action that invokes the currently focused control.
|
||||
///
|
||||
/// This is an abstract class that serves as a base class for actions that
|
||||
/// activate a control. It is bound to [LogicalKeyboardKey.enter] in the default
|
||||
/// keyboard map in [WidgetsApp].
|
||||
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter] in
|
||||
/// the default keyboard map in [WidgetsApp].
|
||||
abstract class ActivateAction extends Action {
|
||||
/// Creates a [ActivateAction] with a fixed [key];
|
||||
const ActivateAction() : super(key);
|
||||
|
@ -385,3 +385,16 @@ abstract class ActivateAction extends Action {
|
|||
/// The [LocalKey] that uniquely identifies this action.
|
||||
static const LocalKey key = ValueKey<Type>(ActivateAction);
|
||||
}
|
||||
|
||||
/// An action that selects the currently focused control.
|
||||
///
|
||||
/// This is an abstract class that serves as a base class for actions that
|
||||
/// select something, like a checkbox or a radio button. By default, it is bound
|
||||
/// to [LogicalKeyboardKey.space] in the default keyboard map in [WidgetsApp].
|
||||
abstract class SelectAction extends Action {
|
||||
/// Creates a [SelectAction] with a fixed [key];
|
||||
const SelectAction() : super(key);
|
||||
|
||||
/// The [LocalKey] that uniquely identifies this action.
|
||||
static const LocalKey key = ValueKey<Type>(SelectAction);
|
||||
}
|
||||
|
|
|
@ -1046,6 +1046,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
|
||||
};
|
||||
|
||||
final Map<LocalKey, ActionFactory> _actionMap = <LocalKey, ActionFactory>{
|
||||
|
|
|
@ -897,8 +897,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
|
||||
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
|
||||
properties.add(FlagProperty('hasFocus', value: hasFocus, ifTrue: 'FOCUSED', defaultValue: false));
|
||||
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
|
||||
properties.add(FlagProperty('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false));
|
||||
properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -67,11 +67,12 @@ void main() {
|
|||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(Material(
|
||||
|
@ -81,12 +82,13 @@ void main() {
|
|||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isChecked: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(const Material(
|
||||
|
@ -96,9 +98,10 @@ void main() {
|
|||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isFocusable: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(const Material(
|
||||
|
@ -108,7 +111,7 @@ void main() {
|
|||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isChecked: true,
|
||||
|
@ -130,13 +133,14 @@ void main() {
|
|||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
||||
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||
label: 'foo',
|
||||
textDirection: TextDirection.ltr,
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
));
|
||||
handle.dispose();
|
||||
});
|
||||
|
@ -202,6 +206,7 @@ void main() {
|
|||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
), hasLength(1));
|
||||
|
@ -222,6 +227,7 @@ void main() {
|
|||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isChecked,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
), hasLength(1));
|
||||
|
@ -241,6 +247,7 @@ void main() {
|
|||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
), hasLength(1));
|
||||
|
@ -274,7 +281,7 @@ void main() {
|
|||
);
|
||||
|
||||
await tester.tap(find.byType(Checkbox));
|
||||
final RenderObject object = tester.firstRenderObject(find.byType(Checkbox));
|
||||
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
|
||||
|
||||
expect(checkboxValue, true);
|
||||
expect(semanticEvent, <String, dynamic>{
|
||||
|
@ -304,7 +311,9 @@ void main() {
|
|||
}
|
||||
|
||||
RenderToggleable getCheckboxRenderer() {
|
||||
return tester.renderObject<RenderToggleable>(find.byType(Checkbox));
|
||||
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
|
||||
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
|
||||
}));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildFrame(false));
|
||||
|
@ -356,7 +365,9 @@ void main() {
|
|||
}
|
||||
|
||||
RenderToggleable getCheckboxRenderer() {
|
||||
return tester.renderObject<RenderToggleable>(find.byType(Checkbox));
|
||||
return tester.renderObject<RenderToggleable>(find.byWidgetPredicate((Widget widget) {
|
||||
return widget.runtimeType.toString() == '_CheckboxRenderObjectWidget';
|
||||
}));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildFrame(checkColor: const Color(0xFFFFFFFF)));
|
||||
|
@ -376,4 +387,181 @@ void main() {
|
|||
expect(getCheckboxRenderer(), paints..rrect(color: const Color(0xFF000000))); // paints's color is 0xFF000000 (params)
|
||||
});
|
||||
|
||||
testWidgets('Checkbox is focusable and has correct focus color', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox');
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool value = true;
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: enabled ? (bool newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
} : null,
|
||||
focusColor: Colors.orange[500],
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Checkbox))),
|
||||
paints
|
||||
..circle(color: Colors.orange[500])
|
||||
..rrect(
|
||||
color: const Color(0xff1e88e5),
|
||||
rrect: RRect.fromLTRBR(
|
||||
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
|
||||
..path(color: Colors.white),
|
||||
);
|
||||
|
||||
// Check the false value.
|
||||
value = false;
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Checkbox))),
|
||||
paints
|
||||
..circle(color: Colors.orange[500])
|
||||
..drrect(
|
||||
color: const Color(0x8a000000),
|
||||
outer: RRect.fromLTRBR(
|
||||
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)),
|
||||
inner: RRect.fromLTRBR(393.0,
|
||||
293.0, 407.0, 307.0, const Radius.circular(-1.0))),
|
||||
);
|
||||
|
||||
// Check what happens when disabled.
|
||||
value = false;
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isFalse);
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Checkbox))),
|
||||
paints
|
||||
..drrect(
|
||||
color: const Color(0x61000000),
|
||||
outer: RRect.fromLTRBR(
|
||||
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)),
|
||||
inner: RRect.fromLTRBR(393.0,
|
||||
293.0, 407.0, 307.0, const Radius.circular(-1.0))),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Checkbox can be hovered and has correct hover color', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool value = true;
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: enabled ? (bool newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
} : null,
|
||||
hoverColor: Colors.orange[500],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Checkbox))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0xff1e88e5),
|
||||
rrect: RRect.fromLTRBR(
|
||||
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
|
||||
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
|
||||
);
|
||||
|
||||
// Start hovering
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Checkbox))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0xff1e88e5),
|
||||
rrect: RRect.fromLTRBR(
|
||||
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
|
||||
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
|
||||
);
|
||||
|
||||
// Check what happens when disabled.
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Checkbox))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0x61000000),
|
||||
rrect: RRect.fromLTRBR(
|
||||
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
|
||||
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Checkbox can be toggled by keyboard shortcuts', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool value = true;
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: enabled ? (bool newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
} : null,
|
||||
focusColor: Colors.orange[500],
|
||||
autofocus: true,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isFalse);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isTrue);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isFalse);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isTrue);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -131,6 +132,7 @@ void main() {
|
|||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.tap,
|
||||
|
@ -157,6 +159,7 @@ void main() {
|
|||
SemanticsFlag.isChecked,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.tap,
|
||||
|
@ -181,6 +184,7 @@ void main() {
|
|||
SemanticsFlag.isInMutuallyExclusiveGroup,
|
||||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -232,7 +236,7 @@ void main() {
|
|||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
final RenderObject object = tester.firstRenderObject(find.byKey(key));
|
||||
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
|
||||
|
||||
expect(radioValue, 1);
|
||||
expect(semanticEvent, <String, dynamic>{
|
||||
|
@ -282,5 +286,227 @@ void main() {
|
|||
),
|
||||
);
|
||||
}, skip: isBrowser);
|
||||
|
||||
testWidgets('Radio is focusable and has correct focus color', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
int groupValue = 0;
|
||||
const Key radioKey = Key('radio');
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.white,
|
||||
child: Radio<int>(
|
||||
key: radioKey,
|
||||
value: 0,
|
||||
onChanged: enabled ? (int newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
} : null,
|
||||
focusColor: Colors.orange[500],
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
groupValue: groupValue,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
|
||||
..circle(color: Colors.orange[500])
|
||||
..circle(color: const Color(0xff1e88e5))
|
||||
..circle(color: const Color(0xff1e88e5)),
|
||||
);
|
||||
|
||||
// Check when the radio isn't selected.
|
||||
groupValue = 1;
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
|
||||
..circle(color: Colors.orange[500])
|
||||
..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0)
|
||||
);
|
||||
|
||||
// Check when the radio is selected, but disabled.
|
||||
groupValue = 0;
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isFalse);
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
|
||||
..circle(color: const Color(0x61000000))
|
||||
..circle(color: const Color(0x61000000)),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio can be hovered and has correct focus color', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
int groupValue = 0;
|
||||
const Key radioKey = Key('radio');
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.white,
|
||||
child: Radio<int>(
|
||||
key: radioKey,
|
||||
value: 0,
|
||||
onChanged: enabled ? (int newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
} : null,
|
||||
hoverColor: Colors.orange[500],
|
||||
groupValue: groupValue,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
|
||||
..circle(color: const Color(0xff1e88e5))
|
||||
..circle(color: const Color(0xff1e88e5)),
|
||||
);
|
||||
|
||||
// Start hovering
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(tester.getCenter(find.byKey(radioKey)));
|
||||
|
||||
// Check when the radio isn't selected.
|
||||
groupValue = 1;
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
|
||||
..circle(color: Colors.orange[500])
|
||||
..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0)
|
||||
);
|
||||
|
||||
// Check when the radio is selected, but disabled.
|
||||
groupValue = 0;
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0))
|
||||
..circle(color: const Color(0x61000000))
|
||||
..circle(color: const Color(0x61000000)),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio can be toggled by keyboard shortcuts', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
int groupValue = 1;
|
||||
const Key radioKey0 = Key('radio0');
|
||||
const Key radioKey1 = Key('radio1');
|
||||
final FocusNode focusNode1 = FocusNode(debugLabel: 'radio1');
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Radio<int>(
|
||||
key: radioKey0,
|
||||
value: 0,
|
||||
onChanged: enabled ? (int newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
} : null,
|
||||
hoverColor: Colors.orange[500],
|
||||
groupValue: groupValue,
|
||||
autofocus: true,
|
||||
),
|
||||
Radio<int>(
|
||||
key: radioKey1,
|
||||
value: 1,
|
||||
onChanged: enabled ? (int newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
} : null,
|
||||
hoverColor: Colors.orange[500],
|
||||
groupValue: groupValue,
|
||||
focusNode: focusNode1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(groupValue, equals(0));
|
||||
|
||||
focusNode1.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(groupValue, equals(1));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
@ -47,6 +48,7 @@ void main() {
|
|||
Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
|
||||
},
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
|
@ -74,6 +76,12 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
|
||||
expect(pressed, isTrue);
|
||||
|
||||
pressed = false;
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(pressed, kIsWeb ? isFalse : isTrue);
|
||||
});
|
||||
|
||||
testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async {
|
||||
|
|
|
@ -518,7 +518,7 @@ void main() {
|
|||
),
|
||||
);
|
||||
await tester.tap(find.byType(Switch));
|
||||
final RenderObject object = tester.firstRenderObject(find.byType(Switch));
|
||||
final RenderObject object = tester.firstRenderObject(find.byType(Focus));
|
||||
|
||||
expect(value, true);
|
||||
expect(semanticEvent, <String, dynamic>{
|
||||
|
@ -623,4 +623,198 @@ void main() {
|
|||
|
||||
});
|
||||
|
||||
testWidgets('Switch is focusable and has correct focus color', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool value = true;
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: enabled ? (bool newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
} : null,
|
||||
focusColor: Colors.orange[500],
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Switch))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0x801e88e5),
|
||||
rrect: RRect.fromLTRBR(
|
||||
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
|
||||
..circle(color: Colors.orange[500])
|
||||
..circle(color: const Color(0x33000000))
|
||||
..circle(color: const Color(0x24000000))
|
||||
..circle(color: const Color(0x1f000000))
|
||||
..circle(color: const Color(0xff1e88e5)),
|
||||
);
|
||||
|
||||
// Check the false value.
|
||||
value = false;
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Switch))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0x52000000),
|
||||
rrect: RRect.fromLTRBR(
|
||||
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
|
||||
..circle(color: Colors.orange[500])
|
||||
..circle(color: const Color(0x33000000))
|
||||
..circle(color: const Color(0x24000000))
|
||||
..circle(color: const Color(0x1f000000))
|
||||
..circle(color: const Color(0xfffafafa)),
|
||||
);
|
||||
|
||||
// Check what happens when disabled.
|
||||
value = false;
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isFalse);
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Switch))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0x1f000000),
|
||||
rrect: RRect.fromLTRBR(
|
||||
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
|
||||
..circle(color: const Color(0x33000000))
|
||||
..circle(color: const Color(0x24000000))
|
||||
..circle(color: const Color(0x1f000000))
|
||||
..circle(color: const Color(0xffbdbdbd)),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Switch can be hovered and has correct hover color', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool value = true;
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: enabled ? (bool newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
} : null,
|
||||
hoverColor: Colors.orange[500],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Switch))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0x801e88e5),
|
||||
rrect: RRect.fromLTRBR(
|
||||
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
|
||||
..circle(color: const Color(0x33000000))
|
||||
..circle(color: const Color(0x24000000))
|
||||
..circle(color: const Color(0x1f000000))
|
||||
..circle(color: const Color(0xff1e88e5)),
|
||||
);
|
||||
|
||||
// Start hovering
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Switch))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0x801e88e5),
|
||||
rrect: RRect.fromLTRBR(
|
||||
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
|
||||
..circle(color: Colors.orange[500])
|
||||
..circle(color: const Color(0x33000000))
|
||||
..circle(color: const Color(0x24000000))
|
||||
..circle(color: const Color(0x1f000000))
|
||||
..circle(color: const Color(0xff1e88e5)),
|
||||
);
|
||||
|
||||
// Check what happens when disabled.
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byType(Switch))),
|
||||
paints
|
||||
..rrect(
|
||||
color: const Color(0x1f000000),
|
||||
rrect: RRect.fromLTRBR(
|
||||
383.5, 293.0, 416.5, 307.0, const Radius.circular(7.0)))
|
||||
..circle(color: const Color(0x33000000))
|
||||
..circle(color: const Color(0x24000000))
|
||||
..circle(color: const Color(0x1f000000))
|
||||
..circle(color: const Color(0xffbdbdbd)),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Switch can be toggled by keyboard shortcuts', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool value = true;
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: enabled ? (bool newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
} : null,
|
||||
focusColor: Colors.orange[500],
|
||||
autofocus: true,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isFalse);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isTrue);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isFalse);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isTrue);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -66,9 +66,12 @@ void main() {
|
|||
FocusNode(
|
||||
debugLabel: 'Label',
|
||||
).debugFillProperties(builder);
|
||||
final List<String> description = builder.properties.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode n) => n.toString()).toList();
|
||||
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
|
||||
expect(description, <String>[
|
||||
'debugLabel: "Label"',
|
||||
'context: null',
|
||||
'canRequestFocus: true',
|
||||
'hasFocus: false',
|
||||
'hasPrimaryFocus: false'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -621,9 +624,12 @@ void main() {
|
|||
FocusScopeNode(
|
||||
debugLabel: 'Scope Label',
|
||||
).debugFillProperties(builder);
|
||||
final List<String> description = builder.properties.where((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode n) => n.toString()).toList();
|
||||
final List<String> description = builder.properties.map((DiagnosticsNode n) => n.toString()).toList();
|
||||
expect(description, <String>[
|
||||
'debugLabel: "Scope Label"',
|
||||
'context: null',
|
||||
'canRequestFocus: true',
|
||||
'hasFocus: false',
|
||||
'hasPrimaryFocus: false'
|
||||
]);
|
||||
});
|
||||
testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
|
||||
|
@ -663,43 +669,36 @@ void main() {
|
|||
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← [root]\n'
|
||||
' │\n'
|
||||
' └─rootScope: FocusScopeNode#00000(Root Focus Scope)\n'
|
||||
' │ FOCUSED\n'
|
||||
' │ debugLabel: "Root Focus Scope"\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusScopeNode#00000\n'
|
||||
' │\n'
|
||||
' ├─Child 1: FocusScopeNode#00000(Scope 1)\n'
|
||||
' │ │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ │ debugLabel: "Scope 1"\n'
|
||||
' │ │\n'
|
||||
' │ └─Child 1: FocusNode#00000(Parent 1)\n'
|
||||
' │ │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ │ debugLabel: "Parent 1"\n'
|
||||
' │ │\n'
|
||||
' │ ├─Child 1: FocusNode#00000(Child 1)\n'
|
||||
' │ │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ │ debugLabel: "Child 1"\n'
|
||||
' │ │\n'
|
||||
' │ └─Child 2: FocusNode#00000\n'
|
||||
' │ context: Container-[GlobalKey#00000]\n'
|
||||
' │\n'
|
||||
' └─Child 2: FocusScopeNode#00000\n'
|
||||
' │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ FOCUSED\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusNode#00000(Child 4)\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusNode#00000(Parent 2)\n'
|
||||
' │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ FOCUSED\n'
|
||||
' │ debugLabel: "Parent 2"\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │\n'
|
||||
' ├─Child 1: FocusNode#00000(Child 3)\n'
|
||||
' │ context: Container-[GlobalKey#00000]\n'
|
||||
' │ debugLabel: "Child 3"\n'
|
||||
' │\n'
|
||||
' └─Child 2: FocusNode#00000(Child 4)\n'
|
||||
' context: Container-[GlobalKey#00000]\n'
|
||||
' FOCUSED\n'
|
||||
' debugLabel: "Child 4"\n'
|
||||
' PRIMARY FOCUS\n'
|
||||
));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -229,34 +229,29 @@ void main() {
|
|||
parentFocusScope.toStringDeep(),
|
||||
equalsIgnoringHashCodes('FocusScopeNode#00000(Parent Scope Node)\n'
|
||||
' │ context: FocusScope\n'
|
||||
' │ FOCUSED\n'
|
||||
' │ debugLabel: "Parent Scope Node"\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusNode#00000(Child)\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusNode#00000(Child)\n'
|
||||
' context: Focus\n'
|
||||
' FOCUSED\n'
|
||||
' debugLabel: "Child"\n'),
|
||||
' PRIMARY FOCUS\n'),
|
||||
);
|
||||
|
||||
expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep);
|
||||
expect(
|
||||
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
|
||||
equalsIgnoringHashCodes('FocusScopeNode#00000(Root Focus Scope)\n'
|
||||
' │ FOCUSED\n'
|
||||
' │ debugLabel: "Root Focus Scope"\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node)\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusScopeNode#00000(Parent Scope Node)\n'
|
||||
' │ context: FocusScope\n'
|
||||
' │ FOCUSED\n'
|
||||
' │ debugLabel: "Parent Scope Node"\n'
|
||||
' │ IN FOCUS PATH\n'
|
||||
' │ focusedChildren: FocusNode#00000(Child)\n'
|
||||
' │\n'
|
||||
' └─Child 1: FocusNode#00000(Child)\n'
|
||||
' context: Focus\n'
|
||||
' FOCUSED\n'
|
||||
' debugLabel: "Child"\n'),
|
||||
' PRIMARY FOCUS\n'),
|
||||
);
|
||||
|
||||
// Add the child focus scope to the focus tree.
|
||||
|
|
Loading…
Reference in a new issue