Re-Land: Add focus nodes, hover, and shortcuts to switches, checkboxes, and radio buttons. (#43384)
This re-lands the change that adds focus nodes, hover, and shortcuts to switches, checkboxes, and radio buttons. (#43213) No changes from original, except for finding the right RenderBox in dev/integration_tests/android_semantics_testing/test_driver/main_test.dart.
This commit is contained in:
parent
b142c9bbdb
commit
8017f63b08
@ -198,8 +198,16 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Checkbox has correct Android semantics', () async {
|
test('Checkbox has correct Android semantics', () async {
|
||||||
|
Future<AndroidSemanticsNode> getCheckboxSemantics(String key) async {
|
||||||
|
return getSemantics(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byValueKey(key),
|
||||||
|
matching: find.byType('_CheckboxRenderObjectWidget'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(checkboxKeyValue)),
|
await getCheckboxSemantics(checkboxKeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.checkBox,
|
className: AndroidClassName.checkBox,
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
@ -216,7 +224,7 @@ void main() {
|
|||||||
await driver.tap(find.byValueKey(checkboxKeyValue));
|
await driver.tap(find.byValueKey(checkboxKeyValue));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(checkboxKeyValue)),
|
await getCheckboxSemantics(checkboxKeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.checkBox,
|
className: AndroidClassName.checkBox,
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
@ -230,7 +238,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(disabledCheckboxKeyValue)),
|
await getCheckboxSemantics(disabledCheckboxKeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.checkBox,
|
className: AndroidClassName.checkBox,
|
||||||
isCheckable: true,
|
isCheckable: true,
|
||||||
@ -242,8 +250,16 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('Radio has correct Android semantics', () async {
|
test('Radio has correct Android semantics', () async {
|
||||||
|
Future<AndroidSemanticsNode> getRadioSemantics(String key) async {
|
||||||
|
return getSemantics(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byValueKey(key),
|
||||||
|
matching: find.byType('_RadioRenderObjectWidget'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(radio2KeyValue)),
|
await getRadioSemantics(radio2KeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.radio,
|
className: AndroidClassName.radio,
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
@ -260,7 +276,7 @@ void main() {
|
|||||||
await driver.tap(find.byValueKey(radio2KeyValue));
|
await driver.tap(find.byValueKey(radio2KeyValue));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(radio2KeyValue)),
|
await getRadioSemantics(radio2KeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.radio,
|
className: AndroidClassName.radio,
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
@ -275,8 +291,16 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('Switch has correct Android semantics', () async {
|
test('Switch has correct Android semantics', () async {
|
||||||
|
Future<AndroidSemanticsNode> getSwitchSemantics(String key) async {
|
||||||
|
return getSemantics(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byValueKey(key),
|
||||||
|
matching: find.byType('_SwitchRenderObjectWidget'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(switchKeyValue)),
|
await getSwitchSemantics(switchKeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.toggleSwitch,
|
className: AndroidClassName.toggleSwitch,
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
@ -293,7 +317,7 @@ void main() {
|
|||||||
await driver.tap(find.byValueKey(switchKeyValue));
|
await driver.tap(find.byValueKey(switchKeyValue));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(switchKeyValue)),
|
await getSwitchSemantics(switchKeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.toggleSwitch,
|
className: AndroidClassName.toggleSwitch,
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
@ -310,8 +334,16 @@ void main() {
|
|||||||
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/20820.
|
// Regression test for https://github.com/flutter/flutter/issues/20820.
|
||||||
test('Switch can be labeled', () async {
|
test('Switch can be labeled', () async {
|
||||||
|
Future<AndroidSemanticsNode> getSwitchSemantics(String key) async {
|
||||||
|
return getSemantics(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byValueKey(key),
|
||||||
|
matching: find.byType('_SwitchRenderObjectWidget'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(find.byValueKey(labeledSwitchKeyValue)),
|
await getSwitchSemantics(labeledSwitchKeyValue),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
className: AndroidClassName.toggleSwitch,
|
className: AndroidClassName.toggleSwitch,
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.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
|
/// * [onChanged], which is called when the value of the checkbox should
|
||||||
/// change. It can be set to null to disable the checkbox.
|
/// 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({
|
const Checkbox({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.value,
|
@required this.value,
|
||||||
@ -60,9 +62,14 @@ class Checkbox extends StatefulWidget {
|
|||||||
@required this.onChanged,
|
@required this.onChanged,
|
||||||
this.activeColor,
|
this.activeColor,
|
||||||
this.checkColor,
|
this.checkColor,
|
||||||
|
this.focusColor,
|
||||||
|
this.hoverColor,
|
||||||
this.materialTapTargetSize,
|
this.materialTapTargetSize,
|
||||||
|
this.focusNode,
|
||||||
|
this.autofocus = false,
|
||||||
}) : assert(tristate != null),
|
}) : assert(tristate != null),
|
||||||
assert(tristate || value != null),
|
assert(tristate || value != null),
|
||||||
|
assert(autofocus != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Whether this checkbox is checked.
|
/// Whether this checkbox is checked.
|
||||||
@ -113,10 +120,10 @@ class Checkbox extends StatefulWidget {
|
|||||||
///
|
///
|
||||||
/// Checkbox displays a dash when its value is null.
|
/// Checkbox displays a dash when its value is null.
|
||||||
///
|
///
|
||||||
/// When a tri-state checkbox is tapped its [onChanged] callback will be
|
/// When a tri-state checkbox ([tristate] is true) is tapped, its [onChanged]
|
||||||
/// applied to true if the current value is null or false, false otherwise.
|
/// callback will be applied to true if the current value is false, to null if
|
||||||
/// Typically tri-state checkboxes are disabled (the onChanged callback is
|
/// value is true, and to false if value is null (i.e. it cycles through false
|
||||||
/// null) so they don't respond to taps.
|
/// => true => null => false when tapped).
|
||||||
///
|
///
|
||||||
/// If tristate is false (the default), [value] must not be null.
|
/// If tristate is false (the default), [value] must not be null.
|
||||||
final bool tristate;
|
final bool tristate;
|
||||||
@ -130,6 +137,18 @@ class Checkbox extends StatefulWidget {
|
|||||||
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
||||||
final MaterialTapTargetSize materialTapTargetSize;
|
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.
|
/// The width of a checkbox widget.
|
||||||
static const double width = 18.0;
|
static const double width = 18.0;
|
||||||
|
|
||||||
@ -138,6 +157,68 @@ class Checkbox extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(debugCheckHasMaterial(context));
|
assert(debugCheckHasMaterial(context));
|
||||||
@ -152,15 +233,36 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
|
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
|
||||||
|
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(
|
return _CheckboxRenderObjectWidget(
|
||||||
value: widget.value,
|
value: widget.value,
|
||||||
tristate: widget.tristate,
|
tristate: widget.tristate,
|
||||||
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
||||||
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
|
checkColor: widget.checkColor ?? const Color(0xFFFFFFFF),
|
||||||
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
|
inactiveColor: enabled ? themeData.unselectedWidgetColor : themeData.disabledColor,
|
||||||
|
focusColor: widget.focusColor ?? themeData.focusColor,
|
||||||
|
hoverColor: widget.hoverColor ?? themeData.hoverColor,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
vsync: this,
|
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.activeColor,
|
||||||
@required this.checkColor,
|
@required this.checkColor,
|
||||||
@required this.inactiveColor,
|
@required this.inactiveColor,
|
||||||
|
@required this.focusColor,
|
||||||
|
@required this.hoverColor,
|
||||||
@required this.onChanged,
|
@required this.onChanged,
|
||||||
@required this.vsync,
|
@required this.vsync,
|
||||||
@required this.additionalConstraints,
|
@required this.additionalConstraints,
|
||||||
|
@required this.hasFocus,
|
||||||
|
@required this.hovering,
|
||||||
}) : assert(tristate != null),
|
}) : assert(tristate != null),
|
||||||
assert(tristate || value != null),
|
assert(tristate || value != null),
|
||||||
assert(activeColor != null),
|
assert(activeColor != null),
|
||||||
@ -185,9 +291,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
|
|
||||||
final bool value;
|
final bool value;
|
||||||
final bool tristate;
|
final bool tristate;
|
||||||
|
final bool hasFocus;
|
||||||
|
final bool hovering;
|
||||||
final Color activeColor;
|
final Color activeColor;
|
||||||
final Color checkColor;
|
final Color checkColor;
|
||||||
final Color inactiveColor;
|
final Color inactiveColor;
|
||||||
|
final Color focusColor;
|
||||||
|
final Color hoverColor;
|
||||||
final ValueChanged<bool> onChanged;
|
final ValueChanged<bool> onChanged;
|
||||||
final TickerProvider vsync;
|
final TickerProvider vsync;
|
||||||
final BoxConstraints additionalConstraints;
|
final BoxConstraints additionalConstraints;
|
||||||
@ -199,9 +309,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
checkColor: checkColor,
|
checkColor: checkColor,
|
||||||
inactiveColor: inactiveColor,
|
inactiveColor: inactiveColor,
|
||||||
|
focusColor: focusColor,
|
||||||
|
hoverColor: hoverColor,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
|
hasFocus: hasFocus,
|
||||||
|
hovering: hovering,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -212,9 +326,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
..activeColor = activeColor
|
..activeColor = activeColor
|
||||||
..checkColor = checkColor
|
..checkColor = checkColor
|
||||||
..inactiveColor = inactiveColor
|
..inactiveColor = inactiveColor
|
||||||
|
..focusColor = focusColor
|
||||||
|
..hoverColor = hoverColor
|
||||||
..onChanged = onChanged
|
..onChanged = onChanged
|
||||||
..additionalConstraints = additionalConstraints
|
..additionalConstraints = additionalConstraints
|
||||||
..vsync = vsync;
|
..vsync = vsync
|
||||||
|
..hasFocus = hasFocus
|
||||||
|
..hovering = hovering;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,8 +347,12 @@ class _RenderCheckbox extends RenderToggleable {
|
|||||||
Color activeColor,
|
Color activeColor,
|
||||||
this.checkColor,
|
this.checkColor,
|
||||||
Color inactiveColor,
|
Color inactiveColor,
|
||||||
|
Color focusColor,
|
||||||
|
Color hoverColor,
|
||||||
BoxConstraints additionalConstraints,
|
BoxConstraints additionalConstraints,
|
||||||
ValueChanged<bool> onChanged,
|
ValueChanged<bool> onChanged,
|
||||||
|
bool hasFocus,
|
||||||
|
bool hovering,
|
||||||
@required TickerProvider vsync,
|
@required TickerProvider vsync,
|
||||||
}) : _oldValue = value,
|
}) : _oldValue = value,
|
||||||
super(
|
super(
|
||||||
@ -238,9 +360,13 @@ class _RenderCheckbox extends RenderToggleable {
|
|||||||
tristate: tristate,
|
tristate: tristate,
|
||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
inactiveColor: inactiveColor,
|
inactiveColor: inactiveColor,
|
||||||
|
focusColor: focusColor,
|
||||||
|
hoverColor: hoverColor,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
|
hasFocus: hasFocus,
|
||||||
|
hovering: hovering,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool _oldValue;
|
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;
|
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() {
|
Action _createAction() {
|
||||||
return CallbackAction(
|
return CallbackAction(
|
||||||
ActivateAction.key,
|
ActivateAction.key,
|
||||||
onInvoke: (FocusNode node, Intent intent) {
|
onInvoke: _handleAction,
|
||||||
_startSplash(context: node.context);
|
|
||||||
_handleTap(node.context);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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);
|
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
@ -107,8 +109,13 @@ class Radio<T> extends StatefulWidget {
|
|||||||
@required this.groupValue,
|
@required this.groupValue,
|
||||||
@required this.onChanged,
|
@required this.onChanged,
|
||||||
this.activeColor,
|
this.activeColor,
|
||||||
|
this.focusColor,
|
||||||
|
this.hoverColor,
|
||||||
this.materialTapTargetSize,
|
this.materialTapTargetSize,
|
||||||
}) : super(key: key);
|
this.focusNode,
|
||||||
|
this.autofocus = false,
|
||||||
|
}) : assert(autofocus != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
/// The value represented by this radio button.
|
/// The value represented by this radio button.
|
||||||
final T value;
|
final T value;
|
||||||
@ -161,15 +168,77 @@ class Radio<T> extends StatefulWidget {
|
|||||||
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
||||||
final MaterialTapTargetSize materialTapTargetSize;
|
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
|
@override
|
||||||
_RadioState<T> createState() => _RadioState<T>();
|
_RadioState<T> createState() => _RadioState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
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) {
|
Color _getInactiveColor(ThemeData themeData) {
|
||||||
return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
|
return enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleChanged(bool selected) {
|
void _handleChanged(bool selected) {
|
||||||
@ -191,13 +260,34 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
|
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
|
||||||
|
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(
|
return _RadioRenderObjectWidget(
|
||||||
selected: widget.value == widget.groupValue,
|
selected: widget.value == widget.groupValue,
|
||||||
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
|
||||||
inactiveColor: _getInactiveColor(themeData),
|
inactiveColor: _getInactiveColor(themeData),
|
||||||
onChanged: _enabled ? _handleChanged : null,
|
focusColor: widget.focusColor ?? themeData.focusColor,
|
||||||
|
hoverColor: widget.hoverColor ?? themeData.hoverColor,
|
||||||
|
onChanged: enabled ? _handleChanged : null,
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
vsync: this,
|
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.selected,
|
||||||
@required this.activeColor,
|
@required this.activeColor,
|
||||||
@required this.inactiveColor,
|
@required this.inactiveColor,
|
||||||
|
@required this.focusColor,
|
||||||
|
@required this.hoverColor,
|
||||||
@required this.additionalConstraints,
|
@required this.additionalConstraints,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
@required this.vsync,
|
@required this.vsync,
|
||||||
|
@required this.hasFocus,
|
||||||
|
@required this.hovering,
|
||||||
}) : assert(selected != null),
|
}) : assert(selected != null),
|
||||||
assert(activeColor != null),
|
assert(activeColor != null),
|
||||||
assert(inactiveColor != null),
|
assert(inactiveColor != null),
|
||||||
@ -218,8 +312,12 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
final bool hasFocus;
|
||||||
|
final bool hovering;
|
||||||
final Color inactiveColor;
|
final Color inactiveColor;
|
||||||
final Color activeColor;
|
final Color activeColor;
|
||||||
|
final Color focusColor;
|
||||||
|
final Color hoverColor;
|
||||||
final ValueChanged<bool> onChanged;
|
final ValueChanged<bool> onChanged;
|
||||||
final TickerProvider vsync;
|
final TickerProvider vsync;
|
||||||
final BoxConstraints additionalConstraints;
|
final BoxConstraints additionalConstraints;
|
||||||
@ -229,9 +327,13 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
value: selected,
|
value: selected,
|
||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
inactiveColor: inactiveColor,
|
inactiveColor: inactiveColor,
|
||||||
|
focusColor: focusColor,
|
||||||
|
hoverColor: hoverColor,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
|
hasFocus: hasFocus,
|
||||||
|
hovering: hovering,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -240,9 +342,13 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
..value = selected
|
..value = selected
|
||||||
..activeColor = activeColor
|
..activeColor = activeColor
|
||||||
..inactiveColor = inactiveColor
|
..inactiveColor = inactiveColor
|
||||||
|
..focusColor = focusColor
|
||||||
|
..hoverColor = hoverColor
|
||||||
..onChanged = onChanged
|
..onChanged = onChanged
|
||||||
..additionalConstraints = additionalConstraints
|
..additionalConstraints = additionalConstraints
|
||||||
..vsync = vsync;
|
..vsync = vsync
|
||||||
|
..hasFocus = hasFocus
|
||||||
|
..hovering = hovering;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,17 +357,25 @@ class _RenderRadio extends RenderToggleable {
|
|||||||
bool value,
|
bool value,
|
||||||
Color activeColor,
|
Color activeColor,
|
||||||
Color inactiveColor,
|
Color inactiveColor,
|
||||||
|
Color focusColor,
|
||||||
|
Color hoverColor,
|
||||||
ValueChanged<bool> onChanged,
|
ValueChanged<bool> onChanged,
|
||||||
BoxConstraints additionalConstraints,
|
BoxConstraints additionalConstraints,
|
||||||
@required TickerProvider vsync,
|
@required TickerProvider vsync,
|
||||||
|
bool hasFocus,
|
||||||
|
bool hovering,
|
||||||
}) : super(
|
}) : super(
|
||||||
value: value,
|
value: value,
|
||||||
tristate: false,
|
tristate: false,
|
||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
inactiveColor: inactiveColor,
|
inactiveColor: inactiveColor,
|
||||||
|
focusColor: focusColor,
|
||||||
|
hoverColor: hoverColor,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
|
hasFocus: hasFocus,
|
||||||
|
hovering: hovering,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -74,6 +74,10 @@ class Switch extends StatefulWidget {
|
|||||||
this.inactiveThumbImage,
|
this.inactiveThumbImage,
|
||||||
this.materialTapTargetSize,
|
this.materialTapTargetSize,
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.focusColor,
|
||||||
|
this.hoverColor,
|
||||||
|
this.focusNode,
|
||||||
|
this.autofocus = false,
|
||||||
}) : _switchType = _SwitchType.material,
|
}) : _switchType = _SwitchType.material,
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@ -98,7 +102,12 @@ class Switch extends StatefulWidget {
|
|||||||
this.inactiveThumbImage,
|
this.inactiveThumbImage,
|
||||||
this.materialTapTargetSize,
|
this.materialTapTargetSize,
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
}) : _switchType = _SwitchType.adaptive,
|
this.focusColor,
|
||||||
|
this.hoverColor,
|
||||||
|
this.focusNode,
|
||||||
|
this.autofocus = false,
|
||||||
|
}) : assert(autofocus != null),
|
||||||
|
_switchType = _SwitchType.adaptive,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Whether this switch is on or off.
|
/// Whether this switch is on or off.
|
||||||
@ -180,6 +189,18 @@ class Switch extends StatefulWidget {
|
|||||||
/// {@macro flutter.cupertino.switch.dragStartBehavior}
|
/// {@macro flutter.cupertino.switch.dragStartBehavior}
|
||||||
final DragStartBehavior 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
|
@override
|
||||||
_SwitchState createState() => _SwitchState();
|
_SwitchState createState() => _SwitchState();
|
||||||
|
|
||||||
@ -192,6 +213,53 @@ class Switch extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
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) {
|
Size getSwitchSize(ThemeData theme) {
|
||||||
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
|
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
|
||||||
case MaterialTapTargetSize.padded:
|
case MaterialTapTargetSize.padded:
|
||||||
@ -205,6 +273,12 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|||||||
return null;
|
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) {
|
Widget buildMaterialSwitch(BuildContext context) {
|
||||||
assert(debugCheckHasMaterial(context));
|
assert(debugCheckHasMaterial(context));
|
||||||
final ThemeData theme = Theme.of(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 activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor;
|
||||||
final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80);
|
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 inactiveThumbColor;
|
||||||
Color inactiveTrackColor;
|
Color inactiveTrackColor;
|
||||||
if (widget.onChanged != null) {
|
if (enabled) {
|
||||||
const Color black32 = Color(0x52000000); // Black with 32% opacity
|
const Color black32 = Color(0x52000000); // Black with 32% opacity
|
||||||
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
|
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
|
||||||
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
|
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
|
||||||
@ -224,11 +300,26 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|||||||
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
|
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
return _SwitchRenderObjectWidget(
|
||||||
dragStartBehavior: widget.dragStartBehavior,
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
value: widget.value,
|
value: widget.value,
|
||||||
activeColor: activeThumbColor,
|
activeColor: activeThumbColor,
|
||||||
inactiveColor: inactiveThumbColor,
|
inactiveColor: inactiveThumbColor,
|
||||||
|
hoverColor: hoverColor,
|
||||||
|
focusColor: focusColor,
|
||||||
activeThumbImage: widget.activeThumbImage,
|
activeThumbImage: widget.activeThumbImage,
|
||||||
inactiveThumbImage: widget.inactiveThumbImage,
|
inactiveThumbImage: widget.inactiveThumbImage,
|
||||||
activeTrackColor: activeTrackColor,
|
activeTrackColor: activeTrackColor,
|
||||||
@ -236,13 +327,23 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|||||||
configuration: createLocalImageConfiguration(context),
|
configuration: createLocalImageConfiguration(context),
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
|
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
|
||||||
|
hasFocus: enabled && _showHighlight && hasFocus,
|
||||||
|
hovering: enabled && _showHighlight && hovering,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildCupertinoSwitch(BuildContext context) {
|
Widget buildCupertinoSwitch(BuildContext context) {
|
||||||
final Size size = getSwitchSize(Theme.of(context));
|
final Size size = getSwitchSize(Theme.of(context));
|
||||||
return Container(
|
return Focus(
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
child: Container(
|
||||||
width: size.width, // Same size as the Material switch.
|
width: size.width, // Same size as the Material switch.
|
||||||
height: size.height,
|
height: size.height,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@ -252,6 +353,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
activeColor: widget.activeColor,
|
activeColor: widget.activeColor,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,6 +386,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
this.value,
|
this.value,
|
||||||
this.activeColor,
|
this.activeColor,
|
||||||
this.inactiveColor,
|
this.inactiveColor,
|
||||||
|
this.hoverColor,
|
||||||
|
this.focusColor,
|
||||||
this.activeThumbImage,
|
this.activeThumbImage,
|
||||||
this.inactiveThumbImage,
|
this.inactiveThumbImage,
|
||||||
this.activeTrackColor,
|
this.activeTrackColor,
|
||||||
@ -293,11 +397,15 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
this.vsync,
|
this.vsync,
|
||||||
this.additionalConstraints,
|
this.additionalConstraints,
|
||||||
this.dragStartBehavior,
|
this.dragStartBehavior,
|
||||||
|
this.hasFocus,
|
||||||
|
this.hovering,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final bool value;
|
final bool value;
|
||||||
final Color activeColor;
|
final Color activeColor;
|
||||||
final Color inactiveColor;
|
final Color inactiveColor;
|
||||||
|
final Color hoverColor;
|
||||||
|
final Color focusColor;
|
||||||
final ImageProvider activeThumbImage;
|
final ImageProvider activeThumbImage;
|
||||||
final ImageProvider inactiveThumbImage;
|
final ImageProvider inactiveThumbImage;
|
||||||
final Color activeTrackColor;
|
final Color activeTrackColor;
|
||||||
@ -307,6 +415,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
final TickerProvider vsync;
|
final TickerProvider vsync;
|
||||||
final BoxConstraints additionalConstraints;
|
final BoxConstraints additionalConstraints;
|
||||||
final DragStartBehavior dragStartBehavior;
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
final bool hasFocus;
|
||||||
|
final bool hovering;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_RenderSwitch createRenderObject(BuildContext context) {
|
_RenderSwitch createRenderObject(BuildContext context) {
|
||||||
@ -315,6 +425,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
value: value,
|
value: value,
|
||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
inactiveColor: inactiveColor,
|
inactiveColor: inactiveColor,
|
||||||
|
hoverColor: hoverColor,
|
||||||
|
focusColor: focusColor,
|
||||||
activeThumbImage: activeThumbImage,
|
activeThumbImage: activeThumbImage,
|
||||||
inactiveThumbImage: inactiveThumbImage,
|
inactiveThumbImage: inactiveThumbImage,
|
||||||
activeTrackColor: activeTrackColor,
|
activeTrackColor: activeTrackColor,
|
||||||
@ -323,6 +435,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
|
hasFocus: hasFocus,
|
||||||
|
hovering: hovering,
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -333,6 +447,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
..value = value
|
..value = value
|
||||||
..activeColor = activeColor
|
..activeColor = activeColor
|
||||||
..inactiveColor = inactiveColor
|
..inactiveColor = inactiveColor
|
||||||
|
..hoverColor = hoverColor
|
||||||
|
..focusColor = focusColor
|
||||||
..activeThumbImage = activeThumbImage
|
..activeThumbImage = activeThumbImage
|
||||||
..inactiveThumbImage = inactiveThumbImage
|
..inactiveThumbImage = inactiveThumbImage
|
||||||
..activeTrackColor = activeTrackColor
|
..activeTrackColor = activeTrackColor
|
||||||
@ -342,6 +458,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
..textDirection = Directionality.of(context)
|
..textDirection = Directionality.of(context)
|
||||||
..additionalConstraints = additionalConstraints
|
..additionalConstraints = additionalConstraints
|
||||||
..dragStartBehavior = dragStartBehavior
|
..dragStartBehavior = dragStartBehavior
|
||||||
|
..hasFocus = hasFocus
|
||||||
|
..hovering = hovering
|
||||||
..vsync = vsync;
|
..vsync = vsync;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,6 +469,8 @@ class _RenderSwitch extends RenderToggleable {
|
|||||||
bool value,
|
bool value,
|
||||||
Color activeColor,
|
Color activeColor,
|
||||||
Color inactiveColor,
|
Color inactiveColor,
|
||||||
|
Color hoverColor,
|
||||||
|
Color focusColor,
|
||||||
ImageProvider activeThumbImage,
|
ImageProvider activeThumbImage,
|
||||||
ImageProvider inactiveThumbImage,
|
ImageProvider inactiveThumbImage,
|
||||||
Color activeTrackColor,
|
Color activeTrackColor,
|
||||||
@ -359,8 +479,10 @@ class _RenderSwitch extends RenderToggleable {
|
|||||||
BoxConstraints additionalConstraints,
|
BoxConstraints additionalConstraints,
|
||||||
@required TextDirection textDirection,
|
@required TextDirection textDirection,
|
||||||
ValueChanged<bool> onChanged,
|
ValueChanged<bool> onChanged,
|
||||||
@required TickerProvider vsync,
|
|
||||||
DragStartBehavior dragStartBehavior,
|
DragStartBehavior dragStartBehavior,
|
||||||
|
bool hasFocus,
|
||||||
|
bool hovering,
|
||||||
|
@required TickerProvider vsync,
|
||||||
}) : assert(textDirection != null),
|
}) : assert(textDirection != null),
|
||||||
_activeThumbImage = activeThumbImage,
|
_activeThumbImage = activeThumbImage,
|
||||||
_inactiveThumbImage = inactiveThumbImage,
|
_inactiveThumbImage = inactiveThumbImage,
|
||||||
@ -373,8 +495,12 @@ class _RenderSwitch extends RenderToggleable {
|
|||||||
tristate: false,
|
tristate: false,
|
||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
inactiveColor: inactiveColor,
|
inactiveColor: inactiveColor,
|
||||||
|
hoverColor: hoverColor,
|
||||||
|
focusColor: focusColor,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
|
hasFocus: hasFocus,
|
||||||
|
hovering: hovering,
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
) {
|
) {
|
||||||
_drag = HorizontalDragGestureRecognizer()
|
_drag = HorizontalDragGestureRecognizer()
|
||||||
|
@ -10,9 +10,15 @@ import 'package:flutter/scheduler.dart';
|
|||||||
|
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
|
|
||||||
|
// Duration of the animation that moves the toggle from one state to another.
|
||||||
const Duration _kToggleDuration = Duration(milliseconds: 200);
|
const Duration _kToggleDuration = Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
// Radius of the radial reaction over time.
|
||||||
final Animatable<double> _kRadialReactionRadiusTween = Tween<double>(begin: 0.0, end: kRadialReactionRadius);
|
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.
|
/// A base class for material style toggleable controls with toggle animations.
|
||||||
///
|
///
|
||||||
/// This class handles storing the current value, dispatching ValueChanged on a
|
/// This class handles storing the current value, dispatching ValueChanged on a
|
||||||
@ -28,9 +34,13 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||||||
bool tristate = false,
|
bool tristate = false,
|
||||||
@required Color activeColor,
|
@required Color activeColor,
|
||||||
@required Color inactiveColor,
|
@required Color inactiveColor,
|
||||||
|
Color hoverColor,
|
||||||
|
Color focusColor,
|
||||||
ValueChanged<bool> onChanged,
|
ValueChanged<bool> onChanged,
|
||||||
BoxConstraints additionalConstraints,
|
BoxConstraints additionalConstraints,
|
||||||
@required TickerProvider vsync,
|
@required TickerProvider vsync,
|
||||||
|
bool hasFocus = false,
|
||||||
|
bool hovering = false,
|
||||||
}) : assert(tristate != null),
|
}) : assert(tristate != null),
|
||||||
assert(tristate || value != null),
|
assert(tristate || value != null),
|
||||||
assert(activeColor != null),
|
assert(activeColor != null),
|
||||||
@ -40,7 +50,11 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||||||
_tristate = tristate,
|
_tristate = tristate,
|
||||||
_activeColor = activeColor,
|
_activeColor = activeColor,
|
||||||
_inactiveColor = inactiveColor,
|
_inactiveColor = inactiveColor,
|
||||||
|
_hoverColor = hoverColor ?? activeColor.withAlpha(kRadialReactionAlpha),
|
||||||
|
_focusColor = focusColor ?? activeColor.withAlpha(kRadialReactionAlpha),
|
||||||
_onChanged = onChanged,
|
_onChanged = onChanged,
|
||||||
|
_hasFocus = hasFocus,
|
||||||
|
_hovering = hovering,
|
||||||
_vsync = vsync,
|
_vsync = vsync,
|
||||||
super(additionalConstraints: additionalConstraints) {
|
super(additionalConstraints: additionalConstraints) {
|
||||||
_tap = TapGestureRecognizer()
|
_tap = TapGestureRecognizer()
|
||||||
@ -66,6 +80,24 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||||||
parent: _reactionController,
|
parent: _reactionController,
|
||||||
curve: Curves.fastOutSlowIn,
|
curve: Curves.fastOutSlowIn,
|
||||||
)..addListener(markNeedsPaint);
|
)..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.
|
/// Used by subclasses to manipulate the visual value of the control.
|
||||||
@ -102,6 +134,66 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||||||
AnimationController _reactionController;
|
AnimationController _reactionController;
|
||||||
Animation<double> _reaction;
|
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.
|
/// The [TickerProvider] for the [AnimationController]s that run the animations.
|
||||||
TickerProvider get vsync => _vsync;
|
TickerProvider get vsync => _vsync;
|
||||||
TickerProvider _vsync;
|
TickerProvider _vsync;
|
||||||
@ -192,6 +284,54 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||||||
markNeedsPaint();
|
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.
|
/// Called when the control changes value.
|
||||||
///
|
///
|
||||||
/// If the control is tapped, [onChanged] is called immediately with the new
|
/// 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
|
/// point at which the user interacted with the control, which is handled
|
||||||
/// automatically).
|
/// automatically).
|
||||||
void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
|
void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
|
||||||
if (!_reaction.isDismissed) {
|
if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) {
|
||||||
// TODO(abarth): We should have a different reaction color when position is zero.
|
final Paint reactionPaint = Paint()
|
||||||
final Paint reactionPaint = Paint()..color = activeColor.withAlpha(kRadialReactionAlpha);
|
..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 Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value);
|
||||||
final double radius = _kRadialReactionRadiusTween.evaluate(_reaction);
|
final double reactionRadius = hasFocus || hovering
|
||||||
canvas.drawCircle(center + offset, radius, reactionPaint);
|
? 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.
|
/// An action that invokes the currently focused control.
|
||||||
///
|
///
|
||||||
/// This is an abstract class that serves as a base class for actions that
|
/// 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
|
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter] in
|
||||||
/// keyboard map in [WidgetsApp].
|
/// the default keyboard map in [WidgetsApp].
|
||||||
abstract class ActivateAction extends Action {
|
abstract class ActivateAction extends Action {
|
||||||
/// Creates a [ActivateAction] with a fixed [key];
|
/// Creates a [ActivateAction] with a fixed [key];
|
||||||
const ActivateAction() : super(key);
|
const ActivateAction() : super(key);
|
||||||
@ -385,3 +385,16 @@ abstract class ActivateAction extends Action {
|
|||||||
/// The [LocalKey] that uniquely identifies this action.
|
/// The [LocalKey] that uniquely identifies this action.
|
||||||
static const LocalKey key = ValueKey<Type>(ActivateAction);
|
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.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
|
||||||
};
|
};
|
||||||
|
|
||||||
final Map<LocalKey, ActionFactory> _actionMap = <LocalKey, ActionFactory>{
|
final Map<LocalKey, ActionFactory> _actionMap = <LocalKey, ActionFactory>{
|
||||||
|
@ -897,8 +897,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
|
|||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
|
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
|
||||||
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
|
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
|
||||||
properties.add(FlagProperty('hasFocus', value: hasFocus, ifTrue: 'FOCUSED', defaultValue: false));
|
properties.add(FlagProperty('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false));
|
||||||
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
|
properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -67,11 +67,12 @@ void main() {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
|
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics(
|
||||||
hasCheckedState: true,
|
hasCheckedState: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
hasTapAction: true,
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
await tester.pumpWidget(Material(
|
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,
|
hasCheckedState: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
hasTapAction: true,
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
await tester.pumpWidget(const Material(
|
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,
|
hasCheckedState: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
|
isFocusable: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
await tester.pumpWidget(const Material(
|
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,
|
hasCheckedState: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
isChecked: 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',
|
label: 'foo',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
hasCheckedState: true,
|
hasCheckedState: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
hasTapAction: true,
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
));
|
));
|
||||||
handle.dispose();
|
handle.dispose();
|
||||||
});
|
});
|
||||||
@ -202,6 +206,7 @@ void main() {
|
|||||||
SemanticsFlag.hasCheckedState,
|
SemanticsFlag.hasCheckedState,
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
), hasLength(1));
|
), hasLength(1));
|
||||||
@ -222,6 +227,7 @@ void main() {
|
|||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
SemanticsFlag.isChecked,
|
SemanticsFlag.isChecked,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
), hasLength(1));
|
), hasLength(1));
|
||||||
@ -241,6 +247,7 @@ void main() {
|
|||||||
SemanticsFlag.hasCheckedState,
|
SemanticsFlag.hasCheckedState,
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
), hasLength(1));
|
), hasLength(1));
|
||||||
@ -274,7 +281,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await tester.tap(find.byType(Checkbox));
|
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(checkboxValue, true);
|
||||||
expect(semanticEvent, <String, dynamic>{
|
expect(semanticEvent, <String, dynamic>{
|
||||||
@ -304,7 +311,9 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RenderToggleable getCheckboxRenderer() {
|
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));
|
await tester.pumpWidget(buildFrame(false));
|
||||||
@ -356,7 +365,9 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RenderToggleable getCheckboxRenderer() {
|
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)));
|
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)
|
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_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../rendering/mock_canvas.dart';
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -131,6 +132,7 @@ void main() {
|
|||||||
SemanticsFlag.hasCheckedState,
|
SemanticsFlag.hasCheckedState,
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.tap,
|
SemanticsAction.tap,
|
||||||
@ -157,6 +159,7 @@ void main() {
|
|||||||
SemanticsFlag.isChecked,
|
SemanticsFlag.isChecked,
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.tap,
|
SemanticsAction.tap,
|
||||||
@ -181,6 +184,7 @@ void main() {
|
|||||||
SemanticsFlag.isInMutuallyExclusiveGroup,
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
||||||
SemanticsFlag.hasCheckedState,
|
SemanticsFlag.hasCheckedState,
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -232,7 +236,7 @@ void main() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
await tester.tap(find.byKey(key));
|
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(radioValue, 1);
|
||||||
expect(semanticEvent, <String, dynamic>{
|
expect(semanticEvent, <String, dynamic>{
|
||||||
@ -279,5 +283,227 @@ void main() {
|
|||||||
matchesGoldenFile('radio.ink_ripple.png'),
|
matchesGoldenFile('radio.ink_ripple.png'),
|
||||||
);
|
);
|
||||||
}, skip: isBrowser);
|
}, 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
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@ -47,6 +48,7 @@ void main() {
|
|||||||
Shortcuts(
|
Shortcuts(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
|
||||||
},
|
},
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
@ -74,6 +76,12 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(pressed, isTrue);
|
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 {
|
testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async {
|
||||||
|
@ -518,7 +518,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.tap(find.byType(Switch));
|
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(value, true);
|
||||||
expect(semanticEvent, <String, dynamic>{
|
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(
|
FocusNode(
|
||||||
debugLabel: 'Label',
|
debugLabel: 'Label',
|
||||||
).debugFillProperties(builder);
|
).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>[
|
expect(description, <String>[
|
||||||
'debugLabel: "Label"',
|
'context: null',
|
||||||
|
'canRequestFocus: true',
|
||||||
|
'hasFocus: false',
|
||||||
|
'hasPrimaryFocus: false'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -621,9 +624,12 @@ void main() {
|
|||||||
FocusScopeNode(
|
FocusScopeNode(
|
||||||
debugLabel: 'Scope Label',
|
debugLabel: 'Scope Label',
|
||||||
).debugFillProperties(builder);
|
).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>[
|
expect(description, <String>[
|
||||||
'debugLabel: "Scope Label"',
|
'context: null',
|
||||||
|
'canRequestFocus: true',
|
||||||
|
'hasFocus: false',
|
||||||
|
'hasPrimaryFocus: false'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
|
testWidgets('debugDescribeFocusTree produces correct output', (WidgetTester tester) async {
|
||||||
@ -663,43 +669,36 @@ void main() {
|
|||||||
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← [root]\n'
|
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← [root]\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─rootScope: FocusScopeNode#00000(Root Focus Scope)\n'
|
' └─rootScope: FocusScopeNode#00000(Root Focus Scope)\n'
|
||||||
' │ FOCUSED\n'
|
' │ IN FOCUS PATH\n'
|
||||||
' │ debugLabel: "Root Focus Scope"\n'
|
|
||||||
' │ focusedChildren: FocusScopeNode#00000\n'
|
' │ focusedChildren: FocusScopeNode#00000\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' ├─Child 1: FocusScopeNode#00000(Scope 1)\n'
|
' ├─Child 1: FocusScopeNode#00000(Scope 1)\n'
|
||||||
' │ │ context: Container-[GlobalKey#00000]\n'
|
' │ │ context: Container-[GlobalKey#00000]\n'
|
||||||
' │ │ debugLabel: "Scope 1"\n'
|
|
||||||
' │ │\n'
|
' │ │\n'
|
||||||
' │ └─Child 1: FocusNode#00000(Parent 1)\n'
|
' │ └─Child 1: FocusNode#00000(Parent 1)\n'
|
||||||
' │ │ context: Container-[GlobalKey#00000]\n'
|
' │ │ context: Container-[GlobalKey#00000]\n'
|
||||||
' │ │ debugLabel: "Parent 1"\n'
|
|
||||||
' │ │\n'
|
' │ │\n'
|
||||||
' │ ├─Child 1: FocusNode#00000(Child 1)\n'
|
' │ ├─Child 1: FocusNode#00000(Child 1)\n'
|
||||||
' │ │ context: Container-[GlobalKey#00000]\n'
|
' │ │ context: Container-[GlobalKey#00000]\n'
|
||||||
' │ │ debugLabel: "Child 1"\n'
|
|
||||||
' │ │\n'
|
' │ │\n'
|
||||||
' │ └─Child 2: FocusNode#00000\n'
|
' │ └─Child 2: FocusNode#00000\n'
|
||||||
' │ context: Container-[GlobalKey#00000]\n'
|
' │ context: Container-[GlobalKey#00000]\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─Child 2: FocusScopeNode#00000\n'
|
' └─Child 2: FocusScopeNode#00000\n'
|
||||||
' │ context: Container-[GlobalKey#00000]\n'
|
' │ context: Container-[GlobalKey#00000]\n'
|
||||||
' │ FOCUSED\n'
|
' │ IN FOCUS PATH\n'
|
||||||
' │ focusedChildren: FocusNode#00000(Child 4)\n'
|
' │ focusedChildren: FocusNode#00000(Child 4)\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─Child 1: FocusNode#00000(Parent 2)\n'
|
' └─Child 1: FocusNode#00000(Parent 2)\n'
|
||||||
' │ context: Container-[GlobalKey#00000]\n'
|
' │ context: Container-[GlobalKey#00000]\n'
|
||||||
' │ FOCUSED\n'
|
' │ IN FOCUS PATH\n'
|
||||||
' │ debugLabel: "Parent 2"\n'
|
|
||||||
' │\n'
|
' │\n'
|
||||||
' ├─Child 1: FocusNode#00000(Child 3)\n'
|
' ├─Child 1: FocusNode#00000(Child 3)\n'
|
||||||
' │ context: Container-[GlobalKey#00000]\n'
|
' │ context: Container-[GlobalKey#00000]\n'
|
||||||
' │ debugLabel: "Child 3"\n'
|
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─Child 2: FocusNode#00000(Child 4)\n'
|
' └─Child 2: FocusNode#00000(Child 4)\n'
|
||||||
' context: Container-[GlobalKey#00000]\n'
|
' context: Container-[GlobalKey#00000]\n'
|
||||||
' FOCUSED\n'
|
' PRIMARY FOCUS\n'
|
||||||
' debugLabel: "Child 4"\n'
|
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -229,34 +229,29 @@ void main() {
|
|||||||
parentFocusScope.toStringDeep(),
|
parentFocusScope.toStringDeep(),
|
||||||
equalsIgnoringHashCodes('FocusScopeNode#00000(Parent Scope Node)\n'
|
equalsIgnoringHashCodes('FocusScopeNode#00000(Parent Scope Node)\n'
|
||||||
' │ context: FocusScope\n'
|
' │ context: FocusScope\n'
|
||||||
' │ FOCUSED\n'
|
' │ IN FOCUS PATH\n'
|
||||||
' │ debugLabel: "Parent Scope Node"\n'
|
|
||||||
' │ focusedChildren: FocusNode#00000(Child)\n'
|
' │ focusedChildren: FocusNode#00000(Child)\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─Child 1: FocusNode#00000(Child)\n'
|
' └─Child 1: FocusNode#00000(Child)\n'
|
||||||
' context: Focus\n'
|
' context: Focus\n'
|
||||||
' FOCUSED\n'
|
' PRIMARY FOCUS\n'),
|
||||||
' debugLabel: "Child"\n'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep);
|
expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep);
|
||||||
expect(
|
expect(
|
||||||
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
|
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
|
||||||
equalsIgnoringHashCodes('FocusScopeNode#00000(Root Focus Scope)\n'
|
equalsIgnoringHashCodes('FocusScopeNode#00000(Root Focus Scope)\n'
|
||||||
' │ FOCUSED\n'
|
' │ IN FOCUS PATH\n'
|
||||||
' │ debugLabel: "Root Focus Scope"\n'
|
|
||||||
' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node)\n'
|
' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node)\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─Child 1: FocusScopeNode#00000(Parent Scope Node)\n'
|
' └─Child 1: FocusScopeNode#00000(Parent Scope Node)\n'
|
||||||
' │ context: FocusScope\n'
|
' │ context: FocusScope\n'
|
||||||
' │ FOCUSED\n'
|
' │ IN FOCUS PATH\n'
|
||||||
' │ debugLabel: "Parent Scope Node"\n'
|
|
||||||
' │ focusedChildren: FocusNode#00000(Child)\n'
|
' │ focusedChildren: FocusNode#00000(Child)\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─Child 1: FocusNode#00000(Child)\n'
|
' └─Child 1: FocusNode#00000(Child)\n'
|
||||||
' context: Focus\n'
|
' context: Focus\n'
|
||||||
' FOCUSED\n'
|
' PRIMARY FOCUS\n'),
|
||||||
' debugLabel: "Child"\n'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add the child focus scope to the focus tree.
|
// Add the child focus scope to the focus tree.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user