mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Re-land keyboard traversal PRs (#42278)
This attempts to reland #40186 and #41220, that were reverted in #41945. The main modifications from the original PRs are that I predefine the shortcuts and actions maps instead of defining them inline in the build function, and I use a new mapEquals to do a deep comparison so that we don't rebuild modified things if the contents of the map haven't changed. I also eliminated an operator== and hashCode that were defined on the Actions widget, since widgets shouldn't have those. (it's too bad though: I get an 85% speedup if we leave this in! Too bad it prevents rebuilding of the children...) Fixes #40101
This commit is contained in:
parent
a95a84ebfb
commit
ce1509714c
@ -247,8 +247,8 @@ abstract class UndoableAction extends Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SetFocusActionBase extends UndoableAction {
|
class UndoableFocusActionBase extends UndoableAction {
|
||||||
SetFocusActionBase(LocalKey name) : super(name);
|
UndoableFocusActionBase(LocalKey name) : super(name);
|
||||||
|
|
||||||
FocusNode _previousFocus;
|
FocusNode _previousFocus;
|
||||||
|
|
||||||
@ -286,10 +286,8 @@ class SetFocusActionBase extends UndoableAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SetFocusAction extends SetFocusActionBase {
|
class UndoableRequestFocusAction extends UndoableFocusActionBase {
|
||||||
SetFocusAction() : super(key);
|
UndoableRequestFocusAction() : super(RequestFocusAction.key);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(SetFocusAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(FocusNode node, Intent intent) {
|
||||||
@ -299,10 +297,8 @@ class SetFocusAction extends SetFocusActionBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Actions for manipulating focus.
|
/// Actions for manipulating focus.
|
||||||
class NextFocusAction extends SetFocusActionBase {
|
class UndoableNextFocusAction extends UndoableFocusActionBase {
|
||||||
NextFocusAction() : super(key);
|
UndoableNextFocusAction() : super(NextFocusAction.key);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(NextFocusAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(FocusNode node, Intent intent) {
|
||||||
@ -311,10 +307,8 @@ class NextFocusAction extends SetFocusActionBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreviousFocusAction extends SetFocusActionBase {
|
class UndoablePreviousFocusAction extends UndoableFocusActionBase {
|
||||||
PreviousFocusAction() : super(key);
|
UndoablePreviousFocusAction() : super(PreviousFocusAction.key);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(FocusNode node, Intent intent) {
|
||||||
@ -323,16 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DirectionalFocusIntent extends Intent {
|
class UndoableDirectionalFocusAction extends UndoableFocusActionBase {
|
||||||
const DirectionalFocusIntent(this.direction) : super(DirectionalFocusAction.key);
|
UndoableDirectionalFocusAction() : super(DirectionalFocusAction.key);
|
||||||
|
|
||||||
final TraversalDirection direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DirectionalFocusAction extends SetFocusActionBase {
|
|
||||||
DirectionalFocusAction() : super(key);
|
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
|
|
||||||
|
|
||||||
TraversalDirection direction;
|
TraversalDirection direction;
|
||||||
|
|
||||||
@ -366,7 +352,7 @@ class _DemoButtonState extends State<DemoButton> {
|
|||||||
void _handleOnPressed() {
|
void _handleOnPressed() {
|
||||||
print('Button ${widget.name} pressed.');
|
print('Button ${widget.name} pressed.');
|
||||||
setState(() {
|
setState(() {
|
||||||
Actions.invoke(context, const Intent(SetFocusAction.key), focusNode: _focusNode);
|
Actions.invoke(context, const Intent(RequestFocusAction.key), focusNode: _focusNode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,101 +420,91 @@ class _FocusDemoState extends State<FocusDemo> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||||
return Shortcuts(
|
return Actions(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
dispatcher: dispatcher,
|
||||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
actions: <LocalKey, ActionFactory>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
RequestFocusAction.key: () => UndoableRequestFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
NextFocusAction.key: () => UndoableNextFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
PreviousFocusAction.key: () => UndoablePreviousFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
DirectionalFocusAction.key: () => UndoableDirectionalFocusAction(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
kUndoActionKey: () => kUndoAction,
|
||||||
|
kRedoActionKey: () => kRedoAction,
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: DefaultFocusTraversal(
|
||||||
dispatcher: dispatcher,
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
actions: <LocalKey, ActionFactory>{
|
child: Shortcuts(
|
||||||
SetFocusAction.key: () => SetFocusAction(),
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
NextFocusAction.key: () => NextFocusAction(),
|
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
||||||
PreviousFocusAction.key: () => PreviousFocusAction(),
|
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
||||||
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
},
|
||||||
kUndoActionKey: () => kUndoAction,
|
child: FocusScope(
|
||||||
kRedoActionKey: () => kRedoAction,
|
debugLabel: 'Scope',
|
||||||
},
|
autofocus: true,
|
||||||
child: DefaultFocusTraversal(
|
child: DefaultTextStyle(
|
||||||
policy: ReadingOrderTraversalPolicy(),
|
style: textTheme.display1,
|
||||||
child: Shortcuts(
|
child: Scaffold(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
appBar: AppBar(
|
||||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
title: const Text('Actions Demo'),
|
||||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
),
|
||||||
},
|
body: Center(
|
||||||
child: FocusScope(
|
child: Builder(builder: (BuildContext context) {
|
||||||
debugLabel: 'Scope',
|
return Column(
|
||||||
autofocus: true,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: DefaultTextStyle(
|
children: <Widget>[
|
||||||
style: textTheme.display1,
|
Row(
|
||||||
child: Scaffold(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
appBar: AppBar(
|
children: const <Widget>[
|
||||||
title: const Text('Actions Demo'),
|
DemoButton(name: 'One'),
|
||||||
),
|
DemoButton(name: 'Two'),
|
||||||
body: Center(
|
DemoButton(name: 'Three'),
|
||||||
child: Builder(builder: (BuildContext context) {
|
],
|
||||||
return Column(
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Row(
|
||||||
children: <Widget>[
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(
|
children: const <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
DemoButton(name: 'Four'),
|
||||||
children: const <Widget>[
|
DemoButton(name: 'Five'),
|
||||||
DemoButton(name: 'One'),
|
DemoButton(name: 'Six'),
|
||||||
DemoButton(name: 'Two'),
|
],
|
||||||
DemoButton(name: 'Three'),
|
),
|
||||||
],
|
Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(
|
children: const <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
DemoButton(name: 'Seven'),
|
||||||
children: const <Widget>[
|
DemoButton(name: 'Eight'),
|
||||||
DemoButton(name: 'Four'),
|
DemoButton(name: 'Nine'),
|
||||||
DemoButton(name: 'Five'),
|
],
|
||||||
DemoButton(name: 'Six'),
|
),
|
||||||
],
|
Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(
|
children: <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Padding(
|
||||||
children: const <Widget>[
|
padding: const EdgeInsets.all(8.0),
|
||||||
DemoButton(name: 'Seven'),
|
child: RaisedButton(
|
||||||
DemoButton(name: 'Eight'),
|
child: const Text('UNDO'),
|
||||||
DemoButton(name: 'Nine'),
|
onPressed: canUndo
|
||||||
],
|
? () {
|
||||||
),
|
Actions.invoke(context, kUndoIntent);
|
||||||
Row(
|
}
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
: null,
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: RaisedButton(
|
|
||||||
child: const Text('UNDO'),
|
|
||||||
onPressed: canUndo
|
|
||||||
? () {
|
|
||||||
Actions.invoke(context, kUndoIntent);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(8.0),
|
Padding(
|
||||||
child: RaisedButton(
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: const Text('REDO'),
|
child: RaisedButton(
|
||||||
onPressed: canRedo
|
child: const Text('REDO'),
|
||||||
? () {
|
onPressed: canRedo
|
||||||
Actions.invoke(context, kRedoIntent);
|
? () {
|
||||||
}
|
Actions.invoke(context, kRedoIntent);
|
||||||
: null,
|
}
|
||||||
),
|
: null,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
}),
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -45,7 +45,7 @@ void main() {
|
|||||||
await tester.pumpWidget(MaterialApp(home: ChipDemo()));
|
await tester.pumpWidget(MaterialApp(home: ChipDemo()));
|
||||||
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||||||
handle.dispose();
|
handle.dispose();
|
||||||
});
|
}, skip: true); // TODO(gspencergoog): Stop skipping when issue is fixed. https://github.com/flutter/flutter/issues/42455
|
||||||
|
|
||||||
testWidgets('data_table_demo', (WidgetTester tester) async {
|
testWidgets('data_table_demo', (WidgetTester tester) async {
|
||||||
final SemanticsHandle handle = tester.ensureSemantics();
|
final SemanticsHandle handle = tester.ensureSemantics();
|
||||||
|
@ -512,7 +512,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
|
|||||||
tabFocusNodes.addAll(
|
tabFocusNodes.addAll(
|
||||||
List<FocusScopeNode>.generate(
|
List<FocusScopeNode>.generate(
|
||||||
widget.tabCount - tabFocusNodes.length,
|
widget.tabCount - tabFocusNodes.length,
|
||||||
(int index) => FocusScopeNode(debugLabel: '${describeIdentity(widget)} Tab ${index + tabFocusNodes.length}'),
|
(int index) => FocusScopeNode(debugLabel: '$CupertinoTabScaffold Tab ${index + tabFocusNodes.length}'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,22 @@
|
|||||||
/// the same length, and contain the same members. Returns false otherwise.
|
/// the same length, and contain the same members. Returns false otherwise.
|
||||||
/// Order is not compared.
|
/// Order is not compared.
|
||||||
///
|
///
|
||||||
|
/// The term "deep" above refers to the first level of equality: if the elements
|
||||||
|
/// are maps, lists, sets, or other collections/composite objects, then the
|
||||||
|
/// values of those elements are not compared element by element unless their
|
||||||
|
/// equality operators ([Object.operator==]) do so.
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [listEquals], which does something similar for lists.
|
/// * [listEquals], which does something similar for lists.
|
||||||
|
/// * [mapEquals], which does something similar for maps.
|
||||||
bool setEquals<T>(Set<T> a, Set<T> b) {
|
bool setEquals<T>(Set<T> a, Set<T> b) {
|
||||||
if (a == null)
|
if (a == null)
|
||||||
return b == null;
|
return b == null;
|
||||||
if (b == null || a.length != b.length)
|
if (b == null || a.length != b.length)
|
||||||
return false;
|
return false;
|
||||||
|
if (identical(a, b))
|
||||||
|
return true;
|
||||||
for (T value in a) {
|
for (T value in a) {
|
||||||
if (!b.contains(value))
|
if (!b.contains(value))
|
||||||
return false;
|
return false;
|
||||||
@ -31,14 +39,22 @@ bool setEquals<T>(Set<T> a, Set<T> b) {
|
|||||||
/// the same length, and contain the same members in the same order. Returns
|
/// the same length, and contain the same members in the same order. Returns
|
||||||
/// false otherwise.
|
/// false otherwise.
|
||||||
///
|
///
|
||||||
|
/// The term "deep" above refers to the first level of equality: if the elements
|
||||||
|
/// are maps, lists, sets, or other collections/composite objects, then the
|
||||||
|
/// values of those elements are not compared element by element unless their
|
||||||
|
/// equality operators ([Object.operator==]) do so.
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [setEquals], which does something similar for sets.
|
/// * [setEquals], which does something similar for sets.
|
||||||
|
/// * [mapEquals], which does something similar for maps.
|
||||||
bool listEquals<T>(List<T> a, List<T> b) {
|
bool listEquals<T>(List<T> a, List<T> b) {
|
||||||
if (a == null)
|
if (a == null)
|
||||||
return b == null;
|
return b == null;
|
||||||
if (b == null || a.length != b.length)
|
if (b == null || a.length != b.length)
|
||||||
return false;
|
return false;
|
||||||
|
if (identical(a, b))
|
||||||
|
return true;
|
||||||
for (int index = 0; index < a.length; index += 1) {
|
for (int index = 0; index < a.length; index += 1) {
|
||||||
if (a[index] != b[index])
|
if (a[index] != b[index])
|
||||||
return false;
|
return false;
|
||||||
@ -46,6 +62,37 @@ bool listEquals<T>(List<T> a, List<T> b) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compares two maps for deep equality.
|
||||||
|
///
|
||||||
|
/// Returns true if the maps are both null, or if they are both non-null, have
|
||||||
|
/// the same length, and contain the same keys associated with the same values.
|
||||||
|
/// Returns false otherwise.
|
||||||
|
///
|
||||||
|
/// The term "deep" above refers to the first level of equality: if the elements
|
||||||
|
/// are maps, lists, sets, or other collections/composite objects, then the
|
||||||
|
/// values of those elements are not compared element by element unless their
|
||||||
|
/// equality operators ([Object.operator==]) do so.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [setEquals], which does something similar for sets.
|
||||||
|
/// * [listEquals], which does something similar for lists.
|
||||||
|
bool mapEquals<T, U>(Map<T, U> a, Map<T, U> b) {
|
||||||
|
if (a == null)
|
||||||
|
return b == null;
|
||||||
|
if (b == null || a.length != b.length)
|
||||||
|
return false;
|
||||||
|
if (identical(a, b))
|
||||||
|
return true;
|
||||||
|
for (T key in a.keys) {
|
||||||
|
if (!b.containsKey(key) || b[key] != a[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Returns the position of `value` in the `sortedList`, if it exists.
|
/// Returns the position of `value` in the `sortedList`, if it exists.
|
||||||
///
|
///
|
||||||
/// Returns `-1` if the `value` is not in the list. Requires the list items
|
/// Returns `-1` if the `value` is not in the list. Requires the list items
|
||||||
|
@ -474,45 +474,43 @@ class _BottomNavigationTile extends StatelessWidget {
|
|||||||
child: Semantics(
|
child: Semantics(
|
||||||
container: true,
|
container: true,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
child: Focus(
|
child: Stack(
|
||||||
child: Stack(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
InkResponse(
|
||||||
InkResponse(
|
onTap: onTap,
|
||||||
onTap: onTap,
|
child: Padding(
|
||||||
child: Padding(
|
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
|
||||||
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: <Widget>[
|
||||||
children: <Widget>[
|
_TileIcon(
|
||||||
_TileIcon(
|
colorTween: colorTween,
|
||||||
colorTween: colorTween,
|
animation: animation,
|
||||||
animation: animation,
|
iconSize: iconSize,
|
||||||
iconSize: iconSize,
|
selected: selected,
|
||||||
selected: selected,
|
item: item,
|
||||||
item: item,
|
selectedIconTheme: selectedIconTheme,
|
||||||
selectedIconTheme: selectedIconTheme,
|
unselectedIconTheme: unselectedIconTheme,
|
||||||
unselectedIconTheme: unselectedIconTheme,
|
),
|
||||||
),
|
_Label(
|
||||||
_Label(
|
colorTween: colorTween,
|
||||||
colorTween: colorTween,
|
animation: animation,
|
||||||
animation: animation,
|
item: item,
|
||||||
item: item,
|
selectedLabelStyle: selectedLabelStyle,
|
||||||
selectedLabelStyle: selectedLabelStyle,
|
unselectedLabelStyle: unselectedLabelStyle,
|
||||||
unselectedLabelStyle: unselectedLabelStyle,
|
showSelectedLabels: showSelectedLabels,
|
||||||
showSelectedLabels: showSelectedLabels,
|
showUnselectedLabels: showUnselectedLabels,
|
||||||
showUnselectedLabels: showUnselectedLabels,
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Semantics(
|
),
|
||||||
label: indexLabel,
|
Semantics(
|
||||||
),
|
label: indexLabel,
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -330,39 +330,37 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
|
|||||||
final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(widget.textStyle?.color, _states);
|
final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(widget.textStyle?.color, _states);
|
||||||
final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
|
final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
|
||||||
|
|
||||||
final Widget result = Focus(
|
final Widget result = ConstrainedBox(
|
||||||
focusNode: widget.focusNode,
|
constraints: widget.constraints,
|
||||||
canRequestFocus: widget.enabled,
|
child: Material(
|
||||||
onFocusChange: _handleFocusedChanged,
|
elevation: _effectiveElevation,
|
||||||
autofocus: widget.autofocus,
|
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
|
||||||
child: ConstrainedBox(
|
shape: effectiveShape,
|
||||||
constraints: widget.constraints,
|
color: widget.fillColor,
|
||||||
child: Material(
|
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
|
||||||
elevation: _effectiveElevation,
|
animationDuration: widget.animationDuration,
|
||||||
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
|
clipBehavior: widget.clipBehavior,
|
||||||
shape: effectiveShape,
|
child: InkWell(
|
||||||
color: widget.fillColor,
|
focusNode: widget.focusNode,
|
||||||
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
|
canRequestFocus: widget.enabled,
|
||||||
animationDuration: widget.animationDuration,
|
onFocusChange: _handleFocusedChanged,
|
||||||
clipBehavior: widget.clipBehavior,
|
autofocus: widget.autofocus,
|
||||||
child: InkWell(
|
onHighlightChanged: _handleHighlightChanged,
|
||||||
onHighlightChanged: _handleHighlightChanged,
|
splashColor: widget.splashColor,
|
||||||
splashColor: widget.splashColor,
|
highlightColor: widget.highlightColor,
|
||||||
highlightColor: widget.highlightColor,
|
focusColor: widget.focusColor,
|
||||||
focusColor: widget.focusColor,
|
hoverColor: widget.hoverColor,
|
||||||
hoverColor: widget.hoverColor,
|
onHover: _handleHoveredChanged,
|
||||||
onHover: _handleHoveredChanged,
|
onTap: widget.onPressed,
|
||||||
onTap: widget.onPressed,
|
customBorder: effectiveShape,
|
||||||
customBorder: effectiveShape,
|
child: IconTheme.merge(
|
||||||
child: IconTheme.merge(
|
data: IconThemeData(color: effectiveTextColor),
|
||||||
data: IconThemeData(color: effectiveTextColor),
|
child: Container(
|
||||||
child: Container(
|
padding: widget.padding,
|
||||||
padding: widget.padding,
|
child: Center(
|
||||||
child: Center(
|
widthFactor: 1.0,
|
||||||
widthFactor: 1.0,
|
heightFactor: 1.0,
|
||||||
heightFactor: 1.0,
|
child: widget.child,
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1774,73 +1774,71 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
|
|||||||
final Color resolvedLabelColor = MaterialStateProperty.resolveAs<Color>(effectiveLabelStyle?.color, _states);
|
final Color resolvedLabelColor = MaterialStateProperty.resolveAs<Color>(effectiveLabelStyle?.color, _states);
|
||||||
final TextStyle resolvedLabelStyle = effectiveLabelStyle?.copyWith(color: resolvedLabelColor);
|
final TextStyle resolvedLabelStyle = effectiveLabelStyle?.copyWith(color: resolvedLabelColor);
|
||||||
|
|
||||||
Widget result = Focus(
|
Widget result = Material(
|
||||||
onFocusChange: _handleFocus,
|
elevation: isTapping ? pressElevation : elevation,
|
||||||
focusNode: widget.focusNode,
|
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
|
||||||
autofocus: widget.autofocus,
|
animationDuration: pressedAnimationDuration,
|
||||||
canRequestFocus: widget.isEnabled,
|
shape: shape,
|
||||||
child: Material(
|
clipBehavior: widget.clipBehavior,
|
||||||
elevation: isTapping ? pressElevation : elevation,
|
child: InkWell(
|
||||||
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
|
onFocusChange: _handleFocus,
|
||||||
animationDuration: pressedAnimationDuration,
|
focusNode: widget.focusNode,
|
||||||
shape: shape,
|
autofocus: widget.autofocus,
|
||||||
clipBehavior: widget.clipBehavior,
|
canRequestFocus: widget.isEnabled,
|
||||||
child: InkWell(
|
onTap: canTap ? _handleTap : null,
|
||||||
onTap: canTap ? _handleTap : null,
|
onTapDown: canTap ? _handleTapDown : null,
|
||||||
onTapDown: canTap ? _handleTapDown : null,
|
onTapCancel: canTap ? _handleTapCancel : null,
|
||||||
onTapCancel: canTap ? _handleTapCancel : null,
|
onHover: canTap ? _handleHover : null,
|
||||||
onHover: canTap ? _handleHover : null,
|
customBorder: shape,
|
||||||
customBorder: shape,
|
child: AnimatedBuilder(
|
||||||
child: AnimatedBuilder(
|
animation: Listenable.merge(<Listenable>[selectController, enableController]),
|
||||||
animation: Listenable.merge(<Listenable>[selectController, enableController]),
|
builder: (BuildContext context, Widget child) {
|
||||||
builder: (BuildContext context, Widget child) {
|
return Container(
|
||||||
return Container(
|
decoration: ShapeDecoration(
|
||||||
decoration: ShapeDecoration(
|
shape: shape,
|
||||||
shape: shape,
|
color: getBackgroundColor(chipTheme),
|
||||||
color: getBackgroundColor(chipTheme),
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _wrapWithTooltip(
|
|
||||||
widget.tooltip,
|
|
||||||
widget.onPressed,
|
|
||||||
_ChipRenderWidget(
|
|
||||||
theme: _ChipRenderTheme(
|
|
||||||
label: DefaultTextStyle(
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
maxLines: 1,
|
|
||||||
softWrap: false,
|
|
||||||
style: resolvedLabelStyle,
|
|
||||||
child: widget.label,
|
|
||||||
),
|
|
||||||
avatar: AnimatedSwitcher(
|
|
||||||
child: widget.avatar,
|
|
||||||
duration: _kDrawerDuration,
|
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
|
||||||
),
|
|
||||||
deleteIcon: AnimatedSwitcher(
|
|
||||||
child: _buildDeleteIcon(context, theme, chipTheme),
|
|
||||||
duration: _kDrawerDuration,
|
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
|
||||||
),
|
|
||||||
brightness: chipTheme.brightness,
|
|
||||||
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
|
|
||||||
labelPadding: (widget.labelPadding ?? chipTheme.labelPadding).resolve(textDirection),
|
|
||||||
showAvatar: hasAvatar,
|
|
||||||
showCheckmark: showCheckmark,
|
|
||||||
checkmarkColor: checkmarkColor,
|
|
||||||
canTapBody: canTap,
|
|
||||||
),
|
|
||||||
value: widget.selected,
|
|
||||||
checkmarkAnimation: checkmarkAnimation,
|
|
||||||
enableAnimation: enableAnimation,
|
|
||||||
avatarDrawerAnimation: avatarDrawerAnimation,
|
|
||||||
deleteDrawerAnimation: deleteDrawerAnimation,
|
|
||||||
isEnabled: widget.isEnabled,
|
|
||||||
avatarBorder: widget.avatarBorder,
|
|
||||||
),
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _wrapWithTooltip(
|
||||||
|
widget.tooltip,
|
||||||
|
widget.onPressed,
|
||||||
|
_ChipRenderWidget(
|
||||||
|
theme: _ChipRenderTheme(
|
||||||
|
label: DefaultTextStyle(
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
style: resolvedLabelStyle,
|
||||||
|
child: widget.label,
|
||||||
|
),
|
||||||
|
avatar: AnimatedSwitcher(
|
||||||
|
child: widget.avatar,
|
||||||
|
duration: _kDrawerDuration,
|
||||||
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
|
),
|
||||||
|
deleteIcon: AnimatedSwitcher(
|
||||||
|
child: _buildDeleteIcon(context, theme, chipTheme),
|
||||||
|
duration: _kDrawerDuration,
|
||||||
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
|
),
|
||||||
|
brightness: chipTheme.brightness,
|
||||||
|
padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
|
||||||
|
labelPadding: (widget.labelPadding ?? chipTheme.labelPadding).resolve(textDirection),
|
||||||
|
showAvatar: hasAvatar,
|
||||||
|
showCheckmark: showCheckmark,
|
||||||
|
checkmarkColor: checkmarkColor,
|
||||||
|
canTapBody: canTap,
|
||||||
|
),
|
||||||
|
value: widget.selected,
|
||||||
|
checkmarkAnimation: checkmarkAnimation,
|
||||||
|
enableAnimation: enableAnimation,
|
||||||
|
avatarDrawerAnimation: avatarDrawerAnimation,
|
||||||
|
deleteDrawerAnimation: deleteDrawerAnimation,
|
||||||
|
isEnabled: widget.isEnabled,
|
||||||
|
avatarBorder: widget.avatarBorder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -309,22 +309,20 @@ class IconButton extends StatelessWidget {
|
|||||||
return Semantics(
|
return Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
enabled: onPressed != null,
|
enabled: onPressed != null,
|
||||||
child: Focus(
|
child: InkResponse(
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
canRequestFocus: onPressed != null,
|
canRequestFocus: onPressed != null,
|
||||||
child: InkResponse(
|
onTap: onPressed,
|
||||||
onTap: onPressed,
|
child: result,
|
||||||
child: result,
|
focusColor: focusColor ?? Theme.of(context).focusColor,
|
||||||
focusColor: focusColor ?? Theme.of(context).focusColor,
|
hoverColor: hoverColor ?? Theme.of(context).hoverColor,
|
||||||
hoverColor: hoverColor ?? Theme.of(context).hoverColor,
|
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
|
||||||
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
|
splashColor: splashColor ?? Theme.of(context).splashColor,
|
||||||
splashColor: splashColor ?? Theme.of(context).splashColor,
|
radius: math.max(
|
||||||
radius: math.max(
|
Material.defaultSplashRadius,
|
||||||
Material.defaultSplashRadius,
|
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
|
||||||
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
|
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
|
||||||
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -210,10 +210,16 @@ class InkResponse extends StatefulWidget {
|
|||||||
this.splashFactory,
|
this.splashFactory,
|
||||||
this.enableFeedback = true,
|
this.enableFeedback = true,
|
||||||
this.excludeFromSemantics = false,
|
this.excludeFromSemantics = false,
|
||||||
|
this.focusNode,
|
||||||
|
this.canRequestFocus = true,
|
||||||
|
this.onFocusChange,
|
||||||
|
this.autofocus = false,
|
||||||
}) : assert(containedInkWell != null),
|
}) : assert(containedInkWell != null),
|
||||||
assert(highlightShape != null),
|
assert(highlightShape != null),
|
||||||
assert(enableFeedback != null),
|
assert(enableFeedback != null),
|
||||||
assert(excludeFromSemantics != null),
|
assert(excludeFromSemantics != null),
|
||||||
|
assert(autofocus != null),
|
||||||
|
assert(canRequestFocus != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
/// The widget below this widget in the tree.
|
||||||
@ -400,6 +406,21 @@ class InkResponse extends StatefulWidget {
|
|||||||
/// duplication of information.
|
/// duplication of information.
|
||||||
final bool excludeFromSemantics;
|
final bool excludeFromSemantics;
|
||||||
|
|
||||||
|
/// Handler called when the focus changes.
|
||||||
|
///
|
||||||
|
/// Called with true if this widget's node gains focus, and false if it loses
|
||||||
|
/// focus.
|
||||||
|
final ValueChanged<bool> onFocusChange;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.Focus.autofocus}
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.Focus.focusNode}
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.Focus.canRequestFocus}
|
||||||
|
final bool canRequestFocus;
|
||||||
|
|
||||||
/// The rectangle to use for the highlight effect and for clipping
|
/// The rectangle to use for the highlight effect and for clipping
|
||||||
/// the splash effects if [containedInkWell] is true.
|
/// the splash effects if [containedInkWell] is true.
|
||||||
///
|
///
|
||||||
@ -462,39 +483,41 @@ enum _HighlightType {
|
|||||||
class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> {
|
class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> {
|
||||||
Set<InteractiveInkFeature> _splashes;
|
Set<InteractiveInkFeature> _splashes;
|
||||||
InteractiveInkFeature _currentSplash;
|
InteractiveInkFeature _currentSplash;
|
||||||
FocusNode _focusNode;
|
|
||||||
bool _hovering = false;
|
bool _hovering = false;
|
||||||
final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{};
|
final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{};
|
||||||
|
Map<LocalKey, ActionFactory> _actionMap;
|
||||||
|
|
||||||
bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
|
bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_actionMap = <LocalKey, ActionFactory>{
|
||||||
|
ActivateAction.key: () {
|
||||||
|
return CallbackAction(
|
||||||
|
ActivateAction.key,
|
||||||
|
onInvoke: (FocusNode node, Intent intent) {
|
||||||
|
_startSplash(context: node.context);
|
||||||
|
_handleTap(node.context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
|
WidgetsBinding.instance.focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_focusNode?.removeListener(_handleFocusUpdate);
|
|
||||||
_focusNode = Focus.of(context, nullOk: true);
|
|
||||||
_focusNode?.addListener(_handleFocusUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(InkResponse oldWidget) {
|
void didUpdateWidget(InkResponse oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
|
if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
|
||||||
_handleHoverChange(_hovering);
|
_handleHoverChange(_hovering);
|
||||||
_handleFocusUpdate();
|
_updateFocusHighlights();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange);
|
WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange);
|
||||||
_focusNode?.removeListener(_handleFocusUpdate);
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,7 +583,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
|||||||
}
|
}
|
||||||
assert(value == (_highlights[type] != null && _highlights[type].active));
|
assert(value == (_highlights[type] != null && _highlights[type].active));
|
||||||
|
|
||||||
switch(type) {
|
switch (type) {
|
||||||
case _HighlightType.pressed:
|
case _HighlightType.pressed:
|
||||||
if (widget.onHighlightChanged != null)
|
if (widget.onHighlightChanged != null)
|
||||||
widget.onHighlightChanged(value);
|
widget.onHighlightChanged(value);
|
||||||
@ -574,10 +597,10 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
|
InteractiveInkFeature _createInkFeature(Offset globalPosition) {
|
||||||
final MaterialInkController inkController = Material.of(context);
|
final MaterialInkController inkController = Material.of(context);
|
||||||
final RenderBox referenceBox = context.findRenderObject();
|
final RenderBox referenceBox = context.findRenderObject();
|
||||||
final Offset position = referenceBox.globalToLocal(details.globalPosition);
|
final Offset position = referenceBox.globalToLocal(globalPosition);
|
||||||
final Color color = widget.splashColor ?? Theme.of(context).splashColor;
|
final Color color = widget.splashColor ?? Theme.of(context).splashColor;
|
||||||
final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null;
|
final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null;
|
||||||
final BorderRadius borderRadius = widget.borderRadius;
|
final BorderRadius borderRadius = widget.borderRadius;
|
||||||
@ -616,31 +639,54 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_handleFocusUpdate();
|
_updateFocusHighlights();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleFocusUpdate() {
|
void _updateFocusHighlights() {
|
||||||
bool showFocus;
|
bool showFocus;
|
||||||
switch (WidgetsBinding.instance.focusManager.highlightMode) {
|
switch (WidgetsBinding.instance.focusManager.highlightMode) {
|
||||||
case FocusHighlightMode.touch:
|
case FocusHighlightMode.touch:
|
||||||
showFocus = false;
|
showFocus = false;
|
||||||
break;
|
break;
|
||||||
case FocusHighlightMode.traditional:
|
case FocusHighlightMode.traditional:
|
||||||
showFocus = enabled && (Focus.of(context, nullOk: true)?.hasPrimaryFocus ?? false);
|
showFocus = enabled && _hasFocus;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
updateHighlight(_HighlightType.focus, value: showFocus);
|
updateHighlight(_HighlightType.focus, value: showFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _hasFocus = false;
|
||||||
|
void _handleFocusUpdate(bool hasFocus) {
|
||||||
|
_hasFocus = hasFocus;
|
||||||
|
_updateFocusHighlights();
|
||||||
|
if (widget.onFocusChange != null) {
|
||||||
|
widget.onFocusChange(hasFocus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTapDown(TapDownDetails details) {
|
void _handleTapDown(TapDownDetails details) {
|
||||||
final InteractiveInkFeature splash = _createInkFeature(details);
|
_startSplash(details: details);
|
||||||
_splashes ??= HashSet<InteractiveInkFeature>();
|
|
||||||
_splashes.add(splash);
|
|
||||||
_currentSplash = splash;
|
|
||||||
if (widget.onTapDown != null) {
|
if (widget.onTapDown != null) {
|
||||||
widget.onTapDown(details);
|
widget.onTapDown(details);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startSplash({TapDownDetails details, BuildContext context}) {
|
||||||
|
assert(details != null || context != null);
|
||||||
|
|
||||||
|
Offset globalPosition;
|
||||||
|
if (context != null) {
|
||||||
|
final RenderBox referenceBox = context.findRenderObject();
|
||||||
|
assert(referenceBox.hasSize, 'InkResponse must be done with layout before starting a splash.');
|
||||||
|
globalPosition = referenceBox.localToGlobal(referenceBox.paintBounds.center);
|
||||||
|
} else {
|
||||||
|
globalPosition = details.globalPosition;
|
||||||
|
}
|
||||||
|
final InteractiveInkFeature splash = _createInkFeature(globalPosition);
|
||||||
|
_splashes ??= HashSet<InteractiveInkFeature>();
|
||||||
|
_splashes.add(splash);
|
||||||
|
_currentSplash = splash;
|
||||||
updateKeepAlive();
|
updateKeepAlive();
|
||||||
updateHighlight(_HighlightType.pressed, value: true);
|
updateHighlight(_HighlightType.pressed, value: true);
|
||||||
}
|
}
|
||||||
@ -722,18 +768,27 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
|||||||
_highlights[type]?.color = getHighlightColorForType(type);
|
_highlights[type]?.color = getHighlightColorForType(type);
|
||||||
}
|
}
|
||||||
_currentSplash?.color = widget.splashColor ?? Theme.of(context).splashColor;
|
_currentSplash?.color = widget.splashColor ?? Theme.of(context).splashColor;
|
||||||
return MouseRegion(
|
return Actions(
|
||||||
onEnter: enabled ? _handleMouseEnter : null,
|
actions: _actionMap,
|
||||||
onExit: enabled ? _handleMouseExit : null,
|
child: Focus(
|
||||||
child: GestureDetector(
|
focusNode: widget.focusNode,
|
||||||
onTapDown: enabled ? _handleTapDown : null,
|
canRequestFocus: widget.canRequestFocus,
|
||||||
onTap: enabled ? () => _handleTap(context) : null,
|
onFocusChange: _handleFocusUpdate,
|
||||||
onTapCancel: enabled ? _handleTapCancel : null,
|
autofocus: widget.autofocus,
|
||||||
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
|
child: MouseRegion(
|
||||||
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
|
onEnter: enabled ? _handleMouseEnter : null,
|
||||||
behavior: HitTestBehavior.opaque,
|
onExit: enabled ? _handleMouseExit : null,
|
||||||
child: widget.child,
|
child: GestureDetector(
|
||||||
excludeFromSemantics: widget.excludeFromSemantics,
|
onTapDown: enabled ? _handleTapDown : null,
|
||||||
|
onTap: enabled ? () => _handleTap(context) : null,
|
||||||
|
onTapCancel: enabled ? _handleTapCancel : null,
|
||||||
|
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
|
||||||
|
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
excludeFromSemantics: widget.excludeFromSemantics,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -854,6 +909,10 @@ class InkWell extends InkResponse {
|
|||||||
ShapeBorder customBorder,
|
ShapeBorder customBorder,
|
||||||
bool enableFeedback = true,
|
bool enableFeedback = true,
|
||||||
bool excludeFromSemantics = false,
|
bool excludeFromSemantics = false,
|
||||||
|
FocusNode focusNode,
|
||||||
|
bool canRequestFocus = true,
|
||||||
|
ValueChanged<bool> onFocusChange,
|
||||||
|
bool autofocus = false,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
child: child,
|
child: child,
|
||||||
@ -876,5 +935,9 @@ class InkWell extends InkResponse {
|
|||||||
customBorder: customBorder,
|
customBorder: customBorder,
|
||||||
enableFeedback: enableFeedback ?? true,
|
enableFeedback: enableFeedback ?? true,
|
||||||
excludeFromSemantics: excludeFromSemantics ?? false,
|
excludeFromSemantics: excludeFromSemantics ?? false,
|
||||||
|
focusNode: focusNode,
|
||||||
|
canRequestFocus: canRequestFocus ?? true,
|
||||||
|
onFocusChange: onFocusChange,
|
||||||
|
autofocus: autofocus ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -200,6 +200,10 @@ class Actions extends InheritedWidget {
|
|||||||
|
|
||||||
/// A map of [Intent] keys to [ActionFactory] factory methods that defines
|
/// A map of [Intent] keys to [ActionFactory] factory methods that defines
|
||||||
/// which actions this widget knows about.
|
/// which actions this widget knows about.
|
||||||
|
///
|
||||||
|
/// For performance reasons, it is recommended that a pre-built map is
|
||||||
|
/// passed in here (e.g. a final variable from your widget class) instead of
|
||||||
|
/// defining it inline in the build function.
|
||||||
final Map<LocalKey, ActionFactory> actions;
|
final Map<LocalKey, ActionFactory> actions;
|
||||||
|
|
||||||
// Finds the nearest valid ActionDispatcher, or creates a new one if it
|
// Finds the nearest valid ActionDispatcher, or creates a new one if it
|
||||||
@ -341,7 +345,7 @@ class Actions extends InheritedWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(Actions oldWidget) {
|
bool updateShouldNotify(Actions oldWidget) {
|
||||||
return oldWidget.dispatcher != dispatcher || oldWidget.actions != actions;
|
return oldWidget.dispatcher != dispatcher || !mapEquals<LocalKey, ActionFactory>(oldWidget.actions, actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -368,3 +372,16 @@ class DoNothingAction extends Action {
|
|||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) { }
|
void invoke(FocusNode node, Intent intent) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An action that invokes the currently focused control.
|
||||||
|
///
|
||||||
|
/// This is an abstract class that serves as a base class for actions that
|
||||||
|
/// activate a control. It is bound to [LogicalKeyboardKey.enter] in the default
|
||||||
|
/// keyboard map in [WidgetsApp].
|
||||||
|
abstract class ActivateAction extends Action {
|
||||||
|
/// Creates a [ActivateAction] with a fixed [key];
|
||||||
|
const ActivateAction() : super(key);
|
||||||
|
|
||||||
|
/// The [LocalKey] that uniquely identifies this action.
|
||||||
|
static const LocalKey key = ValueKey<Type>(ActivateAction);
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'dart:collection' show HashMap;
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'banner.dart';
|
import 'banner.dart';
|
||||||
@ -20,6 +21,7 @@ import 'navigator.dart';
|
|||||||
import 'pages.dart';
|
import 'pages.dart';
|
||||||
import 'performance_overlay.dart';
|
import 'performance_overlay.dart';
|
||||||
import 'semantics_debugger.dart';
|
import 'semantics_debugger.dart';
|
||||||
|
import 'shortcuts.dart';
|
||||||
import 'text.dart';
|
import 'text.dart';
|
||||||
import 'title.dart';
|
import 'title.dart';
|
||||||
import 'widget_inspector.dart';
|
import 'widget_inspector.dart';
|
||||||
@ -1036,6 +1038,24 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Map<LogicalKeySet, Intent> _keyMap = <LogicalKeySet, Intent>{
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<LocalKey, ActionFactory> _actionMap = <LocalKey, ActionFactory>{
|
||||||
|
DoNothingAction.key: () => const DoNothingAction(),
|
||||||
|
RequestFocusAction.key: () => RequestFocusAction(),
|
||||||
|
NextFocusAction.key: () => NextFocusAction(),
|
||||||
|
PreviousFocusAction.key: () => PreviousFocusAction(),
|
||||||
|
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget navigator;
|
Widget navigator;
|
||||||
@ -1147,17 +1167,18 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
assert(_debugCheckLocalizations(appLocale));
|
assert(_debugCheckLocalizations(appLocale));
|
||||||
|
|
||||||
return Actions(
|
return Shortcuts(
|
||||||
actions: <LocalKey, ActionFactory>{
|
shortcuts: _keyMap,
|
||||||
DoNothingAction.key: () => const DoNothingAction(),
|
child: Actions(
|
||||||
},
|
actions: _actionMap,
|
||||||
child: DefaultFocusTraversal(
|
child: DefaultFocusTraversal(
|
||||||
policy: ReadingOrderTraversalPolicy(),
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
child: _MediaQueryFromWindow(
|
child: _MediaQueryFromWindow(
|
||||||
child: Localizations(
|
child: Localizations(
|
||||||
locale: appLocale,
|
locale: appLocale,
|
||||||
delegates: _localizationsDelegates.toList(),
|
delegates: _localizationsDelegates.toList(),
|
||||||
child: title,
|
child: title,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -186,7 +186,7 @@ class Focus extends StatefulWidget {
|
|||||||
|
|
||||||
/// Handler called when the focus changes.
|
/// Handler called when the focus changes.
|
||||||
///
|
///
|
||||||
/// Called with true if this node gains focus, and false if it loses
|
/// Called with true if this widget's node gains focus, and false if it loses
|
||||||
/// focus.
|
/// focus.
|
||||||
final ValueChanged<bool> onFocusChange;
|
final ValueChanged<bool> onFocusChange;
|
||||||
|
|
||||||
@ -230,6 +230,7 @@ class Focus extends StatefulWidget {
|
|||||||
/// still be focused explicitly.
|
/// still be focused explicitly.
|
||||||
final bool skipTraversal;
|
final bool skipTraversal;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.Focus.canRequestFocus}
|
||||||
/// If true, this widget may request the primary focus.
|
/// If true, this widget may request the primary focus.
|
||||||
///
|
///
|
||||||
/// Defaults to true. Set to false if you want the [FocusNode] this widget
|
/// Defaults to true. Set to false if you want the [FocusNode] this widget
|
||||||
@ -249,6 +250,7 @@ class Focus extends StatefulWidget {
|
|||||||
/// its descendants.
|
/// its descendants.
|
||||||
/// - [FocusTraversalPolicy], a class that can be extended to describe a
|
/// - [FocusTraversalPolicy], a class that can be extended to describe a
|
||||||
/// traversal policy.
|
/// traversal policy.
|
||||||
|
/// {@endtemplate}
|
||||||
final bool canRequestFocus;
|
final bool canRequestFocus;
|
||||||
|
|
||||||
/// Returns the [focusNode] of the [Focus] that most tightly encloses the
|
/// Returns the [focusNode] of the [Focus] that most tightly encloses the
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
// 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 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
import 'actions.dart';
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'binding.dart';
|
import 'binding.dart';
|
||||||
import 'focus_manager.dart';
|
import 'focus_manager.dart';
|
||||||
@ -790,3 +793,138 @@ class DefaultFocusTraversal extends InheritedWidget {
|
|||||||
@override
|
@override
|
||||||
bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy;
|
bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A base class for all of the default actions that request focus for a node.
|
||||||
|
class _RequestFocusActionBase extends Action {
|
||||||
|
_RequestFocusActionBase(LocalKey name) : super(name);
|
||||||
|
|
||||||
|
FocusNode _previousFocus;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void invoke(FocusNode node, Intent tag) {
|
||||||
|
_previousFocus = WidgetsBinding.instance.focusManager.primaryFocus;
|
||||||
|
node.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DiagnosticsProperty<FocusNode>('previous', _previousFocus));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [Action] that requests the focus on the node it is invoked on.
|
||||||
|
///
|
||||||
|
/// This action can be used to request focus for a particular node, by calling
|
||||||
|
/// [Action.invoke] like so:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// Actions.invoke(context, const Intent(RequestFocusAction.key), focusNode: _focusNode);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Where the `_focusNode` is the node for which the focus will be requested.
|
||||||
|
///
|
||||||
|
/// The difference between requesting focus in this way versus calling
|
||||||
|
/// [_focusNode.requestFocus] directly is that it will use the [Action]
|
||||||
|
/// registered in the nearest [Actions] widget associated with [key] to make the
|
||||||
|
/// request, rather than just requesting focus directly. This allows the action
|
||||||
|
/// to have additional side effects, like logging, or undo and redo
|
||||||
|
/// functionality.
|
||||||
|
///
|
||||||
|
/// However, this [RequestFocusAction] is the default action associated with the
|
||||||
|
/// [key] in the [WidgetsApp], and it simply requests focus and has no side
|
||||||
|
/// effects.
|
||||||
|
class RequestFocusAction extends _RequestFocusActionBase {
|
||||||
|
/// Creates a [RequestFocusAction] with a fixed [key].
|
||||||
|
RequestFocusAction() : super(key);
|
||||||
|
|
||||||
|
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
||||||
|
static const LocalKey key = ValueKey<Type>(RequestFocusAction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void invoke(FocusNode node, Intent tag) {
|
||||||
|
super.invoke(node, tag);
|
||||||
|
node.requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [Action] that moves the focus to the next focusable node in the focus
|
||||||
|
/// order.
|
||||||
|
///
|
||||||
|
/// This action is the default action registered for the [key], and by default
|
||||||
|
/// is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp].
|
||||||
|
class NextFocusAction extends _RequestFocusActionBase {
|
||||||
|
/// Creates a [NextFocusAction] with a fixed [key];
|
||||||
|
NextFocusAction() : super(key);
|
||||||
|
|
||||||
|
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
||||||
|
static const LocalKey key = ValueKey<Type>(NextFocusAction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void invoke(FocusNode node, Intent tag) {
|
||||||
|
super.invoke(node, tag);
|
||||||
|
node.nextFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [Action] that moves the focus to the previous focusable node in the focus
|
||||||
|
/// order.
|
||||||
|
///
|
||||||
|
/// This action is the default action registered for the [key], and by default
|
||||||
|
/// is bound to a combination of the [LogicalKeyboardKey.tab] key and the
|
||||||
|
/// [LogicalKeyboardKey.shift] key in the [WidgetsApp].
|
||||||
|
class PreviousFocusAction extends _RequestFocusActionBase {
|
||||||
|
/// Creates a [PreviousFocusAction] with a fixed [key];
|
||||||
|
PreviousFocusAction() : super(key);
|
||||||
|
|
||||||
|
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
||||||
|
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void invoke(FocusNode node, Intent tag) {
|
||||||
|
super.invoke(node, tag);
|
||||||
|
node.previousFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [Intent] that represents moving to the next focusable node in the given
|
||||||
|
/// [direction].
|
||||||
|
///
|
||||||
|
/// This is the [Intent] bound by default to the [LogicalKeyboardKey.arrowUp],
|
||||||
|
/// [LogicalKeyboardKey.arrowDown], [LogicalKeyboardKey.arrowLeft], and
|
||||||
|
/// [LogicalKeyboardKey.arrowRight] keys in the [WidgetsApp], with the
|
||||||
|
/// appropriate associated directions.
|
||||||
|
class DirectionalFocusIntent extends Intent {
|
||||||
|
/// Creates a [DirectionalFocusIntent] with a fixed [key], and the given
|
||||||
|
/// [direction].
|
||||||
|
const DirectionalFocusIntent(this.direction) : super(DirectionalFocusAction.key);
|
||||||
|
|
||||||
|
/// The direction in which to look for the next focusable node when the
|
||||||
|
/// associated [DirectionalFocusAction] is invoked.
|
||||||
|
final TraversalDirection direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [Action] that moves the focus to the focusable node in the given
|
||||||
|
/// [direction] configured by the associated [DirectionalFocusIntent].
|
||||||
|
///
|
||||||
|
/// This is the [Action] associated with the [key] and bound by default to the
|
||||||
|
/// [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
|
||||||
|
/// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in
|
||||||
|
/// the [WidgetsApp], with the appropriate associated directions.
|
||||||
|
class DirectionalFocusAction extends _RequestFocusActionBase {
|
||||||
|
/// Creates a [DirectionalFocusAction] with a fixed [key];
|
||||||
|
DirectionalFocusAction() : super(key);
|
||||||
|
|
||||||
|
/// The [LocalKey] that uniquely identifies this action to [DirectionalFocusIntent].
|
||||||
|
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
|
||||||
|
|
||||||
|
/// The direction in which to look for the next focusable node when invoked.
|
||||||
|
TraversalDirection direction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void invoke(FocusNode node, DirectionalFocusIntent tag) {
|
||||||
|
super.invoke(node, tag);
|
||||||
|
final DirectionalFocusIntent args = tag;
|
||||||
|
node.focusInDirection(args.direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -91,7 +91,7 @@ class KeySet<T extends KeyboardKey> extends Diagnosticable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final KeySet<T> typedOther = other;
|
final KeySet<T> typedOther = other;
|
||||||
return _keys.length == typedOther._keys.length && _keys.containsAll(typedOther._keys);
|
return setEquals<T>(_keys, typedOther._keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -169,10 +169,7 @@ class ShortcutManager extends ChangeNotifier with DiagnosticableMixin {
|
|||||||
Map<LogicalKeySet, Intent> get shortcuts => _shortcuts;
|
Map<LogicalKeySet, Intent> get shortcuts => _shortcuts;
|
||||||
Map<LogicalKeySet, Intent> _shortcuts;
|
Map<LogicalKeySet, Intent> _shortcuts;
|
||||||
set shortcuts(Map<LogicalKeySet, Intent> value) {
|
set shortcuts(Map<LogicalKeySet, Intent> value) {
|
||||||
if (_shortcuts == value) {
|
if (!mapEquals<LogicalKeySet, Intent>(_shortcuts, value)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_shortcuts != value) {
|
|
||||||
_shortcuts = value;
|
_shortcuts = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -259,6 +256,10 @@ class Shortcuts extends StatefulWidget {
|
|||||||
final ShortcutManager manager;
|
final ShortcutManager manager;
|
||||||
|
|
||||||
/// The map of shortcuts that the [manager] will be given to manage.
|
/// The map of shortcuts that the [manager] will be given to manage.
|
||||||
|
///
|
||||||
|
/// For performance reasons, it is recommended that a pre-built map is passed
|
||||||
|
/// in here (e.g. a final variable from your widget class) instead of defining
|
||||||
|
/// it inline in the build function.
|
||||||
final Map<LogicalKeySet, Intent> shortcuts;
|
final Map<LogicalKeySet, Intent> shortcuts;
|
||||||
|
|
||||||
/// The child widget for this [Shortcuts] widget.
|
/// The child widget for this [Shortcuts] widget.
|
||||||
@ -324,15 +325,15 @@ class _ShortcutsState extends State<Shortcuts> {
|
|||||||
@override
|
@override
|
||||||
void didUpdateWidget(Shortcuts oldWidget) {
|
void didUpdateWidget(Shortcuts oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.manager != oldWidget.manager || widget.shortcuts != oldWidget.shortcuts) {
|
if (widget.manager != oldWidget.manager) {
|
||||||
if (widget.manager != null) {
|
if (widget.manager != null) {
|
||||||
_internalManager?.dispose();
|
_internalManager?.dispose();
|
||||||
_internalManager = null;
|
_internalManager = null;
|
||||||
} else {
|
} else {
|
||||||
_internalManager ??= ShortcutManager();
|
_internalManager ??= ShortcutManager();
|
||||||
}
|
}
|
||||||
manager.shortcuts = widget.shortcuts;
|
|
||||||
}
|
}
|
||||||
|
manager.shortcuts = widget.shortcuts;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _handleOnKey(FocusNode node, RawKeyEvent event) {
|
bool _handleOnKey(FocusNode node, RawKeyEvent event) {
|
||||||
@ -345,7 +346,7 @@ class _ShortcutsState extends State<Shortcuts> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Focus(
|
return Focus(
|
||||||
debugLabel: describeIdentity(widget),
|
debugLabel: '$Shortcuts',
|
||||||
canRequestFocus: false,
|
canRequestFocus: false,
|
||||||
onKey: _handleOnKey,
|
onKey: _handleOnKey,
|
||||||
child: _ShortcutsMarker(
|
child: _ShortcutsMarker(
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:flutter/src/foundation/collections.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('binarySearch', () {
|
|
||||||
final List<int> items = <int>[1, 2, 3];
|
|
||||||
|
|
||||||
expect(binarySearch(items, 1), 0);
|
|
||||||
expect(binarySearch(items, 2), 1);
|
|
||||||
expect(binarySearch(items, 3), 2);
|
|
||||||
expect(binarySearch(items, 12), -1);
|
|
||||||
});
|
|
||||||
}
|
|
61
packages/flutter/test/foundation/collections_test.dart
Normal file
61
packages/flutter/test/foundation/collections_test.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/src/foundation/collections.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('listEquals', () {
|
||||||
|
final List<int> listA = <int>[1, 2, 3];
|
||||||
|
final List<int> listB = <int>[1, 2, 3];
|
||||||
|
final List<int> listC = <int>[1, 2];
|
||||||
|
final List<int> listD = <int>[3, 2, 1];
|
||||||
|
|
||||||
|
expect(listEquals<void>(null, null), isTrue);
|
||||||
|
expect(listEquals(listA, null), isFalse);
|
||||||
|
expect(listEquals(null, listB), isFalse);
|
||||||
|
expect(listEquals(listA, listA), isTrue);
|
||||||
|
expect(listEquals(listA, listB), isTrue);
|
||||||
|
expect(listEquals(listA, listC), isFalse);
|
||||||
|
expect(listEquals(listA, listD), isFalse);
|
||||||
|
});
|
||||||
|
test('setEquals', () {
|
||||||
|
final Set<int> setA = <int>{1, 2, 3};
|
||||||
|
final Set<int> setB = <int>{1, 2, 3};
|
||||||
|
final Set<int> setC = <int>{1, 2};
|
||||||
|
final Set<int> setD = <int>{3, 2, 1};
|
||||||
|
|
||||||
|
expect(setEquals<void>(null, null), isTrue);
|
||||||
|
expect(setEquals(setA, null), isFalse);
|
||||||
|
expect(setEquals(null, setB), isFalse);
|
||||||
|
expect(setEquals(setA, setA), isTrue);
|
||||||
|
expect(setEquals(setA, setB), isTrue);
|
||||||
|
expect(setEquals(setA, setC), isFalse);
|
||||||
|
expect(setEquals(setA, setD), isTrue);
|
||||||
|
});
|
||||||
|
test('mapEquals', () {
|
||||||
|
final Map<int, int> mapA = <int, int>{1:1, 2:2, 3:3};
|
||||||
|
final Map<int, int> mapB = <int, int>{1:1, 2:2, 3:3};
|
||||||
|
final Map<int, int> mapC = <int, int>{1:1, 2:2};
|
||||||
|
final Map<int, int> mapD = <int, int>{3:3, 2:2, 1:1};
|
||||||
|
final Map<int, int> mapE = <int, int>{3:1, 2:2, 1:3};
|
||||||
|
|
||||||
|
expect(mapEquals<void, void>(null, null), isTrue);
|
||||||
|
expect(mapEquals(mapA, null), isFalse);
|
||||||
|
expect(mapEquals(null, mapB), isFalse);
|
||||||
|
expect(mapEquals(mapA, mapA), isTrue);
|
||||||
|
expect(mapEquals(mapA, mapB), isTrue);
|
||||||
|
expect(mapEquals(mapA, mapC), isFalse);
|
||||||
|
expect(mapEquals(mapA, mapD), isTrue);
|
||||||
|
expect(mapEquals(mapA, mapE), isFalse);
|
||||||
|
});
|
||||||
|
test('binarySearch', () {
|
||||||
|
final List<int> items = <int>[1, 2, 3];
|
||||||
|
|
||||||
|
expect(binarySearch(items, 1), 0);
|
||||||
|
expect(binarySearch(items, 2), 1);
|
||||||
|
expect(binarySearch(items, 3), 2);
|
||||||
|
expect(binarySearch(items, 12), -1);
|
||||||
|
});
|
||||||
|
}
|
@ -241,10 +241,11 @@ void main() {
|
|||||||
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
|
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
|
||||||
transform: null,
|
transform: null,
|
||||||
flags: <SemanticsFlag>[
|
flags: <SemanticsFlag>[
|
||||||
SemanticsFlag.hasToggledState,
|
|
||||||
SemanticsFlag.isToggled,
|
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
|
SemanticsFlag.hasToggledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
|
SemanticsFlag.isToggled,
|
||||||
],
|
],
|
||||||
actions: SemanticsAction.tap.index,
|
actions: SemanticsAction.tap.index,
|
||||||
label: 'aaa\nAAA',
|
label: 'aaa\nAAA',
|
||||||
@ -255,9 +256,10 @@ void main() {
|
|||||||
transform: Matrix4.translationValues(0.0, 56.0, 0.0),
|
transform: Matrix4.translationValues(0.0, 56.0, 0.0),
|
||||||
flags: <SemanticsFlag>[
|
flags: <SemanticsFlag>[
|
||||||
SemanticsFlag.hasCheckedState,
|
SemanticsFlag.hasCheckedState,
|
||||||
SemanticsFlag.isChecked,
|
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
|
SemanticsFlag.isChecked,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
],
|
],
|
||||||
actions: SemanticsAction.tap.index,
|
actions: SemanticsAction.tap.index,
|
||||||
label: 'bbb\nBBB',
|
label: 'bbb\nBBB',
|
||||||
@ -270,6 +272,7 @@ void main() {
|
|||||||
SemanticsFlag.hasCheckedState,
|
SemanticsFlag.hasCheckedState,
|
||||||
SemanticsFlag.hasEnabledState,
|
SemanticsFlag.hasEnabledState,
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isEnabled,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
SemanticsFlag.isInMutuallyExclusiveGroup,
|
SemanticsFlag.isInMutuallyExclusiveGroup,
|
||||||
],
|
],
|
||||||
actions: SemanticsAction.tap.index,
|
actions: SemanticsAction.tap.index,
|
||||||
|
@ -432,12 +432,16 @@ void _tests() {
|
|||||||
thickness: 0.0,
|
thickness: 0.0,
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: '2016',
|
label: '2016',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.isSelected],
|
flags: <SemanticsFlag>[
|
||||||
|
SemanticsFlag.isSelected,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
|
],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: 'Fri, Jan 15',
|
label: 'Fri, Jan 15',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
@ -1028,24 +1028,28 @@ void main() {
|
|||||||
TestSemantics(
|
TestSemantics(
|
||||||
label: 'one',
|
label: 'one',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
label: 'two',
|
label: 'two',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
label: 'three',
|
label: 'three',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
label: 'four',
|
label: 'four',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
),
|
),
|
||||||
|
@ -1014,6 +1014,7 @@ void main() {
|
|||||||
expect(tester.getSemantics(find.byKey(expandedKey)), matchesSemantics(
|
expect(tester.getSemantics(find.byKey(expandedKey)), matchesSemantics(
|
||||||
label: 'Expanded',
|
label: 'Expanded',
|
||||||
isButton: true,
|
isButton: true,
|
||||||
|
isFocusable: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
hasTapAction: true,
|
hasTapAction: true,
|
||||||
));
|
));
|
||||||
@ -1021,6 +1022,7 @@ void main() {
|
|||||||
expect(tester.getSemantics(find.byKey(collapsedKey)), matchesSemantics(
|
expect(tester.getSemantics(find.byKey(collapsedKey)), matchesSemantics(
|
||||||
label: 'Collapsed',
|
label: 'Collapsed',
|
||||||
isButton: true,
|
isButton: true,
|
||||||
|
isFocusable: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
hasTapAction: true,
|
hasTapAction: true,
|
||||||
));
|
));
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../rendering/mock_canvas.dart';
|
import '../rendering/mock_canvas.dart';
|
||||||
@ -251,6 +252,102 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
}, skip: isBrowser);
|
}, skip: isBrowser);
|
||||||
|
|
||||||
|
testWidgets('The InkWell widget renders an ActivateAction-induced ink ripple', (WidgetTester tester) async {
|
||||||
|
const Color highlightColor = Color(0xAAFF0000);
|
||||||
|
const Color splashColor = Color(0xB40000FF);
|
||||||
|
final BorderRadius borderRadius = BorderRadius.circular(6.0);
|
||||||
|
|
||||||
|
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Shortcuts(
|
||||||
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||||
|
},
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Material(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 100.0,
|
||||||
|
height: 100.0,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
highlightColor: highlightColor,
|
||||||
|
splashColor: splashColor,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onTap: () { },
|
||||||
|
radius: 100.0,
|
||||||
|
splashFactory: InkRipple.splashFactory,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset topLeft = tester.getTopLeft(find.byType(InkWell));
|
||||||
|
final Offset inkWellCenter = tester.getCenter(find.byType(InkWell)) - topLeft;
|
||||||
|
|
||||||
|
// Now activate it with a keypress.
|
||||||
|
focusNode.requestFocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
|
||||||
|
|
||||||
|
bool offsetsAreClose(Offset a, Offset b) => (a - b).distance < 1.0;
|
||||||
|
bool radiiAreClose(double a, double b) => (a - b).abs() < 1.0;
|
||||||
|
|
||||||
|
PaintPattern ripplePattern(double expectedRadius, int expectedAlpha) {
|
||||||
|
return paints
|
||||||
|
..translate(x: 0.0, y: 0.0)
|
||||||
|
..translate(x: topLeft.dx, y: topLeft.dy)
|
||||||
|
..something((Symbol method, List<dynamic> arguments) {
|
||||||
|
if (method != #drawCircle) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Offset center = arguments[0];
|
||||||
|
final double radius = arguments[1];
|
||||||
|
final Paint paint = arguments[2];
|
||||||
|
if (offsetsAreClose(center, inkWellCenter) &&
|
||||||
|
radiiAreClose(radius, expectedRadius) &&
|
||||||
|
paint.color.alpha == expectedAlpha) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw '''
|
||||||
|
Expected: center == $inkWellCenter, radius == $expectedRadius, alpha == $expectedAlpha
|
||||||
|
Found: center == $center radius == $radius alpha == ${paint.color.alpha}''';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ripplePattern always add a translation of topLeft.
|
||||||
|
expect(box, ripplePattern(30.0, 0));
|
||||||
|
|
||||||
|
// The ripple fades in for 75ms. During that time its alpha is eased from
|
||||||
|
// 0 to the splashColor's alpha value.
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
expect(box, ripplePattern(56.0, 120));
|
||||||
|
|
||||||
|
// At 75ms the ripple has faded in: it's alpha matches the splashColor's
|
||||||
|
// alpha.
|
||||||
|
await tester.pump(const Duration(milliseconds: 25));
|
||||||
|
expect(box, ripplePattern(73.0, 180));
|
||||||
|
|
||||||
|
// At this point the splash radius has expanded to its limit: 5 past the
|
||||||
|
// ink well's radius parameter. The fade-out is about to start.
|
||||||
|
// The fade-out begins at 225ms = 50ms + 25ms + 150ms.
|
||||||
|
await tester.pump(const Duration(milliseconds: 150));
|
||||||
|
expect(box, ripplePattern(105.0, 180));
|
||||||
|
|
||||||
|
// After another 150ms the fade-out is complete.
|
||||||
|
await tester.pump(const Duration(milliseconds: 150));
|
||||||
|
expect(box, ripplePattern(105.0, 0));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async {
|
testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/14391
|
// Regression test for https://github.com/flutter/flutter/issues/14391
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -331,5 +428,4 @@ void main() {
|
|||||||
throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
|
throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -103,9 +103,9 @@ void main() {
|
|||||||
splashColor: const Color(0xffff0000),
|
splashColor: const Color(0xffff0000),
|
||||||
focusColor: const Color(0xff0000ff),
|
focusColor: const Color(0xff0000ff),
|
||||||
highlightColor: const Color(0xf00fffff),
|
highlightColor: const Color(0xf00fffff),
|
||||||
onTap: () {},
|
onTap: () { },
|
||||||
onLongPress: () {},
|
onLongPress: () { },
|
||||||
onHover: (bool hover) {},
|
onHover: (bool hover) { },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -123,29 +123,29 @@ void main() {
|
|||||||
testWidgets('ink response changes color on focus', (WidgetTester tester) async {
|
testWidgets('ink response changes color on focus', (WidgetTester tester) async {
|
||||||
WidgetsBinding.instance.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
WidgetsBinding.instance.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||||
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
|
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
|
||||||
await tester.pumpWidget(Material(
|
await tester.pumpWidget(
|
||||||
child: Directionality(
|
Material(
|
||||||
textDirection: TextDirection.ltr,
|
child: Directionality(
|
||||||
child: Center(
|
textDirection: TextDirection.ltr,
|
||||||
child: Focus(
|
child: Center(
|
||||||
focusNode: focusNode,
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
focusNode: focusNode,
|
||||||
hoverColor: const Color(0xff00ff00),
|
hoverColor: const Color(0xff00ff00),
|
||||||
splashColor: const Color(0xffff0000),
|
splashColor: const Color(0xffff0000),
|
||||||
focusColor: const Color(0xff0000ff),
|
focusColor: const Color(0xff0000ff),
|
||||||
highlightColor: const Color(0xf00fffff),
|
highlightColor: const Color(0xf00fffff),
|
||||||
onTap: () {},
|
onTap: () { },
|
||||||
onLongPress: () {},
|
onLongPress: () { },
|
||||||
onHover: (bool hover) {},
|
onHover: (bool hover) { },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||||
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0));
|
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0));
|
||||||
@ -172,9 +172,9 @@ void main() {
|
|||||||
splashColor: const Color(0xffff0000),
|
splashColor: const Color(0xffff0000),
|
||||||
focusColor: const Color(0xff0000ff),
|
focusColor: const Color(0xff0000ff),
|
||||||
highlightColor: const Color(0xf00fffff),
|
highlightColor: const Color(0xf00fffff),
|
||||||
onTap: () {},
|
onTap: () { },
|
||||||
onLongPress: () {},
|
onLongPress: () { },
|
||||||
onHover: (bool hover) {},
|
onHover: (bool hover) { },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -206,8 +206,8 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {},
|
onTap: () { },
|
||||||
onLongPress: () {},
|
onLongPress: () { },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -234,8 +234,8 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {},
|
onTap: () { },
|
||||||
onLongPress: () {},
|
onLongPress: () { },
|
||||||
enableFeedback: false,
|
enableFeedback: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -301,7 +301,7 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
child: Material(
|
child: Material(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {},
|
onTap: () { },
|
||||||
child: const Text('Button'),
|
child: const Text('Button'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -312,7 +312,7 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
child: Material(
|
child: Material(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {},
|
onTap: () { },
|
||||||
child: const Text('Button'),
|
child: const Text('Button'),
|
||||||
excludeFromSemantics: true,
|
excludeFromSemantics: true,
|
||||||
),
|
),
|
||||||
|
@ -368,33 +368,41 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(semantics, hasSemantics(
|
expect(
|
||||||
TestSemantics.root(
|
semantics,
|
||||||
children: <TestSemantics>[
|
hasSemantics(
|
||||||
TestSemantics.rootChild(
|
TestSemantics.root(
|
||||||
label: 'one',
|
children: <TestSemantics>[
|
||||||
flags: <SemanticsFlag>[
|
TestSemantics.rootChild(
|
||||||
SemanticsFlag.hasEnabledState,
|
flags: <SemanticsFlag>[
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.hasEnabledState,
|
||||||
],
|
SemanticsFlag.isEnabled,
|
||||||
),
|
SemanticsFlag.isFocusable,
|
||||||
TestSemantics.rootChild(
|
],
|
||||||
label: 'two',
|
label: 'one',
|
||||||
flags: <SemanticsFlag>[
|
),
|
||||||
SemanticsFlag.isSelected,
|
TestSemantics.rootChild(
|
||||||
SemanticsFlag.hasEnabledState,
|
flags: <SemanticsFlag>[
|
||||||
SemanticsFlag.isEnabled,
|
SemanticsFlag.isSelected,
|
||||||
],
|
SemanticsFlag.hasEnabledState,
|
||||||
),
|
SemanticsFlag.isEnabled,
|
||||||
TestSemantics.rootChild(
|
SemanticsFlag.isFocusable,
|
||||||
label: 'three',
|
],
|
||||||
flags: <SemanticsFlag>[
|
label: 'two',
|
||||||
SemanticsFlag.hasEnabledState,
|
),
|
||||||
],
|
TestSemantics.rootChild(
|
||||||
),
|
flags: <SemanticsFlag>[
|
||||||
],
|
SemanticsFlag.hasEnabledState,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
|
],
|
||||||
|
label: 'three',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ignoreTransform: true,
|
||||||
|
ignoreId: true,
|
||||||
|
ignoreRect: true,
|
||||||
),
|
),
|
||||||
ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
|
@ -520,26 +520,28 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async {
|
testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async {
|
||||||
final SemanticsTester semantics = SemanticsTester(tester);
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(
|
||||||
home: Material(
|
MaterialApp(
|
||||||
child: PopupMenuButton<int>(
|
home: Material(
|
||||||
itemBuilder: (BuildContext context) {
|
child: PopupMenuButton<int>(
|
||||||
return <PopupMenuItem<int>>[
|
itemBuilder: (BuildContext context) {
|
||||||
const PopupMenuItem<int>(value: 1, child: Text('1')),
|
return <PopupMenuItem<int>>[
|
||||||
const PopupMenuItem<int>(value: 2, child: Text('2')),
|
const PopupMenuItem<int>(value: 1, child: Text('1')),
|
||||||
const PopupMenuItem<int>(value: 3, child: Text('3')),
|
const PopupMenuItem<int>(value: 2, child: Text('2')),
|
||||||
const PopupMenuItem<int>(value: 4, child: Text('4')),
|
const PopupMenuItem<int>(value: 3, child: Text('3')),
|
||||||
const PopupMenuItem<int>(value: 5, child: Text('5')),
|
const PopupMenuItem<int>(value: 4, child: Text('4')),
|
||||||
];
|
const PopupMenuItem<int>(value: 5, child: Text('5')),
|
||||||
},
|
];
|
||||||
child: const SizedBox(
|
},
|
||||||
height: 100.0,
|
child: const SizedBox(
|
||||||
width: 100.0,
|
height: 100.0,
|
||||||
child: Text('XXX'),
|
width: 100.0,
|
||||||
|
child: Text('XXX'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
await tester.tap(find.text('XXX'));
|
await tester.tap(find.text('XXX'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@ -563,26 +565,31 @@ void main() {
|
|||||||
],
|
],
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: '1',
|
label: '1',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: '2',
|
label: '2',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: '3',
|
label: '3',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: '4',
|
label: '4',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: '5',
|
label: '5',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
@ -5,12 +5,77 @@
|
|||||||
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';
|
||||||
|
import 'package:flutter/src/services/keyboard_key.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../rendering/mock_canvas.dart';
|
import '../rendering/mock_canvas.dart';
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
testWidgets('RawMaterialButton responds when tapped', (WidgetTester tester) async {
|
||||||
|
bool pressed = false;
|
||||||
|
const Color splashColor = Color(0xff00ff00);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Center(
|
||||||
|
child: RawMaterialButton(
|
||||||
|
splashColor: splashColor,
|
||||||
|
onPressed: () { pressed = true; },
|
||||||
|
child: const Text('BUTTON'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('BUTTON'));
|
||||||
|
await tester.pump(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
final RenderBox splash = Material.of(tester.element(find.byType(InkWell))) as dynamic;
|
||||||
|
expect(splash, paints..circle(color: splashColor));
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(pressed, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('RawMaterialButton responds to shortcut when activated', (WidgetTester tester) async {
|
||||||
|
bool pressed = false;
|
||||||
|
final FocusNode focusNode = FocusNode(debugLabel: 'Test Button');
|
||||||
|
const Color splashColor = Color(0xff00ff00);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Shortcuts(
|
||||||
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
||||||
|
},
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Center(
|
||||||
|
child: RawMaterialButton(
|
||||||
|
splashColor: splashColor,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onPressed: () { pressed = true; },
|
||||||
|
child: const Text('BUTTON'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
focusNode.requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||||
|
await tester.pump(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
final RenderBox splash = Material.of(tester.element(find.byType(InkWell))) as dynamic;
|
||||||
|
expect(splash, paints..circle(color: splashColor));
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(pressed, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async {
|
testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async {
|
||||||
int pressed = 0;
|
int pressed = 0;
|
||||||
|
|
||||||
|
@ -469,6 +469,7 @@ void main() {
|
|||||||
hasToggledState: true,
|
hasToggledState: true,
|
||||||
isToggled: true,
|
isToggled: true,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
label: 'Switch tile',
|
label: 'Switch tile',
|
||||||
hasTapAction: true,
|
hasTapAction: true,
|
||||||
|
@ -1614,15 +1614,19 @@ void main() {
|
|||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
id: 4,
|
id: 4,
|
||||||
actions: SemanticsAction.tap.index,
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
flags: SemanticsFlag.isSelected.index,
|
flags: <SemanticsFlag>[
|
||||||
|
SemanticsFlag.isSelected,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
|
],
|
||||||
label: 'TAB #0\nTab 1 of 2',
|
label: 'TAB #0\nTab 1 of 2',
|
||||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
id: 5,
|
id: 5,
|
||||||
actions: SemanticsAction.tap.index,
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: 'TAB #1\nTab 2 of 2',
|
label: 'TAB #1\nTab 2 of 2',
|
||||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||||
@ -1878,15 +1882,19 @@ void main() {
|
|||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
id: 4,
|
id: 4,
|
||||||
actions: SemanticsAction.tap.index,
|
flags: <SemanticsFlag>[
|
||||||
flags: SemanticsFlag.isSelected.index,
|
SemanticsFlag.isSelected,
|
||||||
|
SemanticsFlag.isFocusable,
|
||||||
|
],
|
||||||
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: 'Semantics override 0\nTab 1 of 2',
|
label: 'Semantics override 0\nTab 1 of 2',
|
||||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
id: 5,
|
id: 5,
|
||||||
actions: SemanticsAction.tap.index,
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
label: 'Semantics override 1\nTab 2 of 2',
|
label: 'Semantics override 1\nTab 2 of 2',
|
||||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||||
|
@ -481,6 +481,7 @@ void main() {
|
|||||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||||
label: 'Signed in\nname\nemail',
|
label: 'Signed in\nname\nemail',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
|
@ -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/painting.dart';
|
||||||
|
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 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -914,5 +916,112 @@ void main() {
|
|||||||
expect(focusCenter.hasFocus, isFalse);
|
expect(focusCenter.hasFocus, isFalse);
|
||||||
expect(focusTop.hasFocus, isTrue);
|
expect(focusTop.hasFocus, isTrue);
|
||||||
});
|
});
|
||||||
|
testWidgets('Focus traversal actions are invoked when shortcuts are used.', (WidgetTester tester) async {
|
||||||
|
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
|
||||||
|
final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey');
|
||||||
|
final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey');
|
||||||
|
final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
WidgetsApp(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return TestRoute(
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: FocusScope(
|
||||||
|
debugLabel: 'scope',
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Focus(
|
||||||
|
autofocus: true,
|
||||||
|
debugLabel: 'upperLeft',
|
||||||
|
child: Container(width: 100, height: 100, key: upperLeftKey),
|
||||||
|
),
|
||||||
|
Focus(
|
||||||
|
debugLabel: 'upperRight',
|
||||||
|
child: Container(width: 100, height: 100, key: upperRightKey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Focus(
|
||||||
|
debugLabel: 'lowerLeft',
|
||||||
|
child: Container(width: 100, height: 100, key: lowerLeftKey),
|
||||||
|
),
|
||||||
|
Focus(
|
||||||
|
debugLabel: 'lowerRight',
|
||||||
|
child: Container(width: 100, height: 100, key: lowerRightKey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial focus happens.
|
||||||
|
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
expect(Focus.of(upperRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
expect(Focus.of(lowerRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||||
|
expect(Focus.of(lowerRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||||
|
expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||||
|
expect(Focus.of(upperRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
||||||
|
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
|
||||||
|
// Traverse in a direction
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||||
|
expect(Focus.of(upperRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
expect(Focus.of(lowerRightKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||||
|
expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
// Initial focus happens.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TestRoute extends PageRouteBuilder<void> {
|
||||||
|
TestRoute({Widget child})
|
||||||
|
: super(
|
||||||
|
pageBuilder: (BuildContext _, Animation<double> __, Animation<double> ___) {
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user