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 {
|
||||
SetFocusActionBase(LocalKey name) : super(name);
|
||||
class UndoableFocusActionBase extends UndoableAction {
|
||||
UndoableFocusActionBase(LocalKey name) : super(name);
|
||||
|
||||
FocusNode _previousFocus;
|
||||
|
||||
@ -286,10 +286,8 @@ class SetFocusActionBase extends UndoableAction {
|
||||
}
|
||||
}
|
||||
|
||||
class SetFocusAction extends SetFocusActionBase {
|
||||
SetFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(SetFocusAction);
|
||||
class UndoableRequestFocusAction extends UndoableFocusActionBase {
|
||||
UndoableRequestFocusAction() : super(RequestFocusAction.key);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent intent) {
|
||||
@ -299,10 +297,8 @@ class SetFocusAction extends SetFocusActionBase {
|
||||
}
|
||||
|
||||
/// Actions for manipulating focus.
|
||||
class NextFocusAction extends SetFocusActionBase {
|
||||
NextFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(NextFocusAction);
|
||||
class UndoableNextFocusAction extends UndoableFocusActionBase {
|
||||
UndoableNextFocusAction() : super(NextFocusAction.key);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent intent) {
|
||||
@ -311,10 +307,8 @@ class NextFocusAction extends SetFocusActionBase {
|
||||
}
|
||||
}
|
||||
|
||||
class PreviousFocusAction extends SetFocusActionBase {
|
||||
PreviousFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
|
||||
class UndoablePreviousFocusAction extends UndoableFocusActionBase {
|
||||
UndoablePreviousFocusAction() : super(PreviousFocusAction.key);
|
||||
|
||||
@override
|
||||
void invoke(FocusNode node, Intent intent) {
|
||||
@ -323,16 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase {
|
||||
}
|
||||
}
|
||||
|
||||
class DirectionalFocusIntent extends Intent {
|
||||
const DirectionalFocusIntent(this.direction) : super(DirectionalFocusAction.key);
|
||||
|
||||
final TraversalDirection direction;
|
||||
}
|
||||
|
||||
class DirectionalFocusAction extends SetFocusActionBase {
|
||||
DirectionalFocusAction() : super(key);
|
||||
|
||||
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
|
||||
class UndoableDirectionalFocusAction extends UndoableFocusActionBase {
|
||||
UndoableDirectionalFocusAction() : super(DirectionalFocusAction.key);
|
||||
|
||||
TraversalDirection direction;
|
||||
|
||||
@ -366,7 +352,7 @@ class _DemoButtonState extends State<DemoButton> {
|
||||
void _handleOnPressed() {
|
||||
print('Button ${widget.name} pressed.');
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||
return Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||
return Actions(
|
||||
dispatcher: dispatcher,
|
||||
actions: <LocalKey, ActionFactory>{
|
||||
RequestFocusAction.key: () => UndoableRequestFocusAction(),
|
||||
NextFocusAction.key: () => UndoableNextFocusAction(),
|
||||
PreviousFocusAction.key: () => UndoablePreviousFocusAction(),
|
||||
DirectionalFocusAction.key: () => UndoableDirectionalFocusAction(),
|
||||
kUndoActionKey: () => kUndoAction,
|
||||
kRedoActionKey: () => kRedoAction,
|
||||
},
|
||||
child: Actions(
|
||||
dispatcher: dispatcher,
|
||||
actions: <LocalKey, ActionFactory>{
|
||||
SetFocusAction.key: () => SetFocusAction(),
|
||||
NextFocusAction.key: () => NextFocusAction(),
|
||||
PreviousFocusAction.key: () => PreviousFocusAction(),
|
||||
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
||||
kUndoActionKey: () => kUndoAction,
|
||||
kRedoActionKey: () => kRedoAction,
|
||||
},
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
||||
},
|
||||
child: FocusScope(
|
||||
debugLabel: 'Scope',
|
||||
autofocus: true,
|
||||
child: DefaultTextStyle(
|
||||
style: textTheme.display1,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Actions Demo'),
|
||||
),
|
||||
body: Center(
|
||||
child: Builder(builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'One'),
|
||||
DemoButton(name: 'Two'),
|
||||
DemoButton(name: 'Three'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Four'),
|
||||
DemoButton(name: 'Five'),
|
||||
DemoButton(name: 'Six'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Seven'),
|
||||
DemoButton(name: 'Eight'),
|
||||
DemoButton(name: 'Nine'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RaisedButton(
|
||||
child: const Text('UNDO'),
|
||||
onPressed: canUndo
|
||||
? () {
|
||||
Actions.invoke(context, kUndoIntent);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
||||
},
|
||||
child: FocusScope(
|
||||
debugLabel: 'Scope',
|
||||
autofocus: true,
|
||||
child: DefaultTextStyle(
|
||||
style: textTheme.display1,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Actions Demo'),
|
||||
),
|
||||
body: Center(
|
||||
child: Builder(builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'One'),
|
||||
DemoButton(name: 'Two'),
|
||||
DemoButton(name: 'Three'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Four'),
|
||||
DemoButton(name: 'Five'),
|
||||
DemoButton(name: 'Six'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
DemoButton(name: 'Seven'),
|
||||
DemoButton(name: 'Eight'),
|
||||
DemoButton(name: 'Nine'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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),
|
||||
child: RaisedButton(
|
||||
child: const Text('REDO'),
|
||||
onPressed: canRedo
|
||||
? () {
|
||||
Actions.invoke(context, kRedoIntent);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RaisedButton(
|
||||
child: const Text('REDO'),
|
||||
onPressed: canRedo
|
||||
? () {
|
||||
Actions.invoke(context, kRedoIntent);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -45,7 +45,7 @@ void main() {
|
||||
await tester.pumpWidget(MaterialApp(home: ChipDemo()));
|
||||
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||||
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 {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
@ -512,7 +512,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
|
||||
tabFocusNodes.addAll(
|
||||
List<FocusScopeNode>.generate(
|
||||
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.
|
||||
/// 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:
|
||||
///
|
||||
/// * [listEquals], which does something similar for lists.
|
||||
/// * [mapEquals], which does something similar for maps.
|
||||
bool setEquals<T>(Set<T> a, Set<T> b) {
|
||||
if (a == null)
|
||||
return b == null;
|
||||
if (b == null || a.length != b.length)
|
||||
return false;
|
||||
if (identical(a, b))
|
||||
return true;
|
||||
for (T value in a) {
|
||||
if (!b.contains(value))
|
||||
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
|
||||
/// 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.
|
||||
/// * [mapEquals], which does something similar for maps.
|
||||
bool listEquals<T>(List<T> a, List<T> b) {
|
||||
if (a == null)
|
||||
return b == null;
|
||||
if (b == null || a.length != b.length)
|
||||
return false;
|
||||
if (identical(a, b))
|
||||
return true;
|
||||
for (int index = 0; index < a.length; index += 1) {
|
||||
if (a[index] != b[index])
|
||||
return false;
|
||||
@ -46,6 +62,37 @@ bool listEquals<T>(List<T> a, List<T> b) {
|
||||
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 `-1` if the `value` is not in the list. Requires the list items
|
||||
|
@ -474,45 +474,43 @@ class _BottomNavigationTile extends StatelessWidget {
|
||||
child: Semantics(
|
||||
container: true,
|
||||
selected: selected,
|
||||
child: Focus(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
InkResponse(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_TileIcon(
|
||||
colorTween: colorTween,
|
||||
animation: animation,
|
||||
iconSize: iconSize,
|
||||
selected: selected,
|
||||
item: item,
|
||||
selectedIconTheme: selectedIconTheme,
|
||||
unselectedIconTheme: unselectedIconTheme,
|
||||
),
|
||||
_Label(
|
||||
colorTween: colorTween,
|
||||
animation: animation,
|
||||
item: item,
|
||||
selectedLabelStyle: selectedLabelStyle,
|
||||
unselectedLabelStyle: unselectedLabelStyle,
|
||||
showSelectedLabels: showSelectedLabels,
|
||||
showUnselectedLabels: showUnselectedLabels,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
InkResponse(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_TileIcon(
|
||||
colorTween: colorTween,
|
||||
animation: animation,
|
||||
iconSize: iconSize,
|
||||
selected: selected,
|
||||
item: item,
|
||||
selectedIconTheme: selectedIconTheme,
|
||||
unselectedIconTheme: unselectedIconTheme,
|
||||
),
|
||||
_Label(
|
||||
colorTween: colorTween,
|
||||
animation: animation,
|
||||
item: item,
|
||||
selectedLabelStyle: selectedLabelStyle,
|
||||
unselectedLabelStyle: unselectedLabelStyle,
|
||||
showSelectedLabels: showSelectedLabels,
|
||||
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 ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
|
||||
|
||||
final Widget result = Focus(
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: _handleFocusedChanged,
|
||||
autofocus: widget.autofocus,
|
||||
child: ConstrainedBox(
|
||||
constraints: widget.constraints,
|
||||
child: Material(
|
||||
elevation: _effectiveElevation,
|
||||
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
|
||||
shape: effectiveShape,
|
||||
color: widget.fillColor,
|
||||
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
|
||||
animationDuration: widget.animationDuration,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
child: InkWell(
|
||||
onHighlightChanged: _handleHighlightChanged,
|
||||
splashColor: widget.splashColor,
|
||||
highlightColor: widget.highlightColor,
|
||||
focusColor: widget.focusColor,
|
||||
hoverColor: widget.hoverColor,
|
||||
onHover: _handleHoveredChanged,
|
||||
onTap: widget.onPressed,
|
||||
customBorder: effectiveShape,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: effectiveTextColor),
|
||||
child: Container(
|
||||
padding: widget.padding,
|
||||
child: Center(
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
child: widget.child,
|
||||
),
|
||||
final Widget result = ConstrainedBox(
|
||||
constraints: widget.constraints,
|
||||
child: Material(
|
||||
elevation: _effectiveElevation,
|
||||
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
|
||||
shape: effectiveShape,
|
||||
color: widget.fillColor,
|
||||
type: widget.fillColor == null ? MaterialType.transparency : MaterialType.button,
|
||||
animationDuration: widget.animationDuration,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
child: InkWell(
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: _handleFocusedChanged,
|
||||
autofocus: widget.autofocus,
|
||||
onHighlightChanged: _handleHighlightChanged,
|
||||
splashColor: widget.splashColor,
|
||||
highlightColor: widget.highlightColor,
|
||||
focusColor: widget.focusColor,
|
||||
hoverColor: widget.hoverColor,
|
||||
onHover: _handleHoveredChanged,
|
||||
onTap: widget.onPressed,
|
||||
customBorder: effectiveShape,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: effectiveTextColor),
|
||||
child: Container(
|
||||
padding: widget.padding,
|
||||
child: Center(
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
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 TextStyle resolvedLabelStyle = effectiveLabelStyle?.copyWith(color: resolvedLabelColor);
|
||||
|
||||
Widget result = Focus(
|
||||
onFocusChange: _handleFocus,
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
canRequestFocus: widget.isEnabled,
|
||||
child: Material(
|
||||
elevation: isTapping ? pressElevation : elevation,
|
||||
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
|
||||
animationDuration: pressedAnimationDuration,
|
||||
shape: shape,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
child: InkWell(
|
||||
onTap: canTap ? _handleTap : null,
|
||||
onTapDown: canTap ? _handleTapDown : null,
|
||||
onTapCancel: canTap ? _handleTapCancel : null,
|
||||
onHover: canTap ? _handleHover : null,
|
||||
customBorder: shape,
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge(<Listenable>[selectController, enableController]),
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: shape,
|
||||
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,
|
||||
Widget result = Material(
|
||||
elevation: isTapping ? pressElevation : elevation,
|
||||
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
|
||||
animationDuration: pressedAnimationDuration,
|
||||
shape: shape,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
child: InkWell(
|
||||
onFocusChange: _handleFocus,
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
canRequestFocus: widget.isEnabled,
|
||||
onTap: canTap ? _handleTap : null,
|
||||
onTapDown: canTap ? _handleTapDown : null,
|
||||
onTapCancel: canTap ? _handleTapCancel : null,
|
||||
onHover: canTap ? _handleHover : null,
|
||||
customBorder: shape,
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge(<Listenable>[selectController, enableController]),
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: shape,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -309,22 +309,20 @@ class IconButton extends StatelessWidget {
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: onPressed != null,
|
||||
child: Focus(
|
||||
child: InkResponse(
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
canRequestFocus: onPressed != null,
|
||||
child: InkResponse(
|
||||
onTap: onPressed,
|
||||
child: result,
|
||||
focusColor: focusColor ?? Theme.of(context).focusColor,
|
||||
hoverColor: hoverColor ?? Theme.of(context).hoverColor,
|
||||
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
|
||||
splashColor: splashColor ?? Theme.of(context).splashColor,
|
||||
radius: math.max(
|
||||
Material.defaultSplashRadius,
|
||||
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
|
||||
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
|
||||
),
|
||||
onTap: onPressed,
|
||||
child: result,
|
||||
focusColor: focusColor ?? Theme.of(context).focusColor,
|
||||
hoverColor: hoverColor ?? Theme.of(context).hoverColor,
|
||||
highlightColor: highlightColor ?? Theme.of(context).highlightColor,
|
||||
splashColor: splashColor ?? Theme.of(context).splashColor,
|
||||
radius: math.max(
|
||||
Material.defaultSplashRadius,
|
||||
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
|
||||
// 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.enableFeedback = true,
|
||||
this.excludeFromSemantics = false,
|
||||
this.focusNode,
|
||||
this.canRequestFocus = true,
|
||||
this.onFocusChange,
|
||||
this.autofocus = false,
|
||||
}) : assert(containedInkWell != null),
|
||||
assert(highlightShape != null),
|
||||
assert(enableFeedback != null),
|
||||
assert(excludeFromSemantics != null),
|
||||
assert(autofocus != null),
|
||||
assert(canRequestFocus != null),
|
||||
super(key: key);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
@ -400,6 +406,21 @@ class InkResponse extends StatefulWidget {
|
||||
/// duplication of information.
|
||||
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 splash effects if [containedInkWell] is true.
|
||||
///
|
||||
@ -462,39 +483,41 @@ enum _HighlightType {
|
||||
class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> {
|
||||
Set<InteractiveInkFeature> _splashes;
|
||||
InteractiveInkFeature _currentSplash;
|
||||
FocusNode _focusNode;
|
||||
bool _hovering = false;
|
||||
final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{};
|
||||
Map<LocalKey, ActionFactory> _actionMap;
|
||||
|
||||
bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
|
||||
|
||||
@override
|
||||
void 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);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_focusNode?.removeListener(_handleFocusUpdate);
|
||||
_focusNode = Focus.of(context, nullOk: true);
|
||||
_focusNode?.addListener(_handleFocusUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(InkResponse oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
|
||||
_handleHoverChange(_hovering);
|
||||
_handleFocusUpdate();
|
||||
_updateFocusHighlights();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange);
|
||||
_focusNode?.removeListener(_handleFocusUpdate);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -560,7 +583,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
||||
}
|
||||
assert(value == (_highlights[type] != null && _highlights[type].active));
|
||||
|
||||
switch(type) {
|
||||
switch (type) {
|
||||
case _HighlightType.pressed:
|
||||
if (widget.onHighlightChanged != null)
|
||||
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 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 RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null;
|
||||
final BorderRadius borderRadius = widget.borderRadius;
|
||||
@ -616,31 +639,54 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_handleFocusUpdate();
|
||||
_updateFocusHighlights();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleFocusUpdate() {
|
||||
void _updateFocusHighlights() {
|
||||
bool showFocus;
|
||||
switch (WidgetsBinding.instance.focusManager.highlightMode) {
|
||||
case FocusHighlightMode.touch:
|
||||
showFocus = false;
|
||||
break;
|
||||
case FocusHighlightMode.traditional:
|
||||
showFocus = enabled && (Focus.of(context, nullOk: true)?.hasPrimaryFocus ?? false);
|
||||
showFocus = enabled && _hasFocus;
|
||||
break;
|
||||
}
|
||||
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) {
|
||||
final InteractiveInkFeature splash = _createInkFeature(details);
|
||||
_splashes ??= HashSet<InteractiveInkFeature>();
|
||||
_splashes.add(splash);
|
||||
_currentSplash = splash;
|
||||
_startSplash(details: details);
|
||||
if (widget.onTapDown != null) {
|
||||
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();
|
||||
updateHighlight(_HighlightType.pressed, value: true);
|
||||
}
|
||||
@ -722,18 +768,27 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
|
||||
_highlights[type]?.color = getHighlightColorForType(type);
|
||||
}
|
||||
_currentSplash?.color = widget.splashColor ?? Theme.of(context).splashColor;
|
||||
return MouseRegion(
|
||||
onEnter: enabled ? _handleMouseEnter : null,
|
||||
onExit: enabled ? _handleMouseExit : null,
|
||||
child: GestureDetector(
|
||||
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,
|
||||
child: widget.child,
|
||||
excludeFromSemantics: widget.excludeFromSemantics,
|
||||
return Actions(
|
||||
actions: _actionMap,
|
||||
child: Focus(
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.canRequestFocus,
|
||||
onFocusChange: _handleFocusUpdate,
|
||||
autofocus: widget.autofocus,
|
||||
child: MouseRegion(
|
||||
onEnter: enabled ? _handleMouseEnter : null,
|
||||
onExit: enabled ? _handleMouseExit : null,
|
||||
child: GestureDetector(
|
||||
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,
|
||||
bool enableFeedback = true,
|
||||
bool excludeFromSemantics = false,
|
||||
FocusNode focusNode,
|
||||
bool canRequestFocus = true,
|
||||
ValueChanged<bool> onFocusChange,
|
||||
bool autofocus = false,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
@ -876,5 +935,9 @@ class InkWell extends InkResponse {
|
||||
customBorder: customBorder,
|
||||
enableFeedback: enableFeedback ?? true,
|
||||
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
|
||||
/// 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;
|
||||
|
||||
// Finds the nearest valid ActionDispatcher, or creates a new one if it
|
||||
@ -341,7 +345,7 @@ class Actions extends InheritedWidget {
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(Actions oldWidget) {
|
||||
return oldWidget.dispatcher != dispatcher || oldWidget.actions != actions;
|
||||
return oldWidget.dispatcher != dispatcher || !mapEquals<LocalKey, ActionFactory>(oldWidget.actions, actions);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -368,3 +372,16 @@ class DoNothingAction extends Action {
|
||||
@override
|
||||
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/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'banner.dart';
|
||||
@ -20,6 +21,7 @@ import 'navigator.dart';
|
||||
import 'pages.dart';
|
||||
import 'performance_overlay.dart';
|
||||
import 'semantics_debugger.dart';
|
||||
import 'shortcuts.dart';
|
||||
import 'text.dart';
|
||||
import 'title.dart';
|
||||
import 'widget_inspector.dart';
|
||||
@ -1036,6 +1038,24 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
Widget navigator;
|
||||
@ -1147,17 +1167,18 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
|
||||
assert(_debugCheckLocalizations(appLocale));
|
||||
|
||||
return Actions(
|
||||
actions: <LocalKey, ActionFactory>{
|
||||
DoNothingAction.key: () => const DoNothingAction(),
|
||||
},
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: _MediaQueryFromWindow(
|
||||
child: Localizations(
|
||||
locale: appLocale,
|
||||
delegates: _localizationsDelegates.toList(),
|
||||
child: title,
|
||||
return Shortcuts(
|
||||
shortcuts: _keyMap,
|
||||
child: Actions(
|
||||
actions: _actionMap,
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: _MediaQueryFromWindow(
|
||||
child: Localizations(
|
||||
locale: appLocale,
|
||||
delegates: _localizationsDelegates.toList(),
|
||||
child: title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -186,7 +186,7 @@ class Focus extends StatefulWidget {
|
||||
|
||||
/// 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.
|
||||
final ValueChanged<bool> onFocusChange;
|
||||
|
||||
@ -230,6 +230,7 @@ class Focus extends StatefulWidget {
|
||||
/// still be focused explicitly.
|
||||
final bool skipTraversal;
|
||||
|
||||
/// {@template flutter.widgets.Focus.canRequestFocus}
|
||||
/// If true, this widget may request the primary focus.
|
||||
///
|
||||
/// Defaults to true. Set to false if you want the [FocusNode] this widget
|
||||
@ -249,6 +250,7 @@ class Focus extends StatefulWidget {
|
||||
/// its descendants.
|
||||
/// - [FocusTraversalPolicy], a class that can be extended to describe a
|
||||
/// traversal policy.
|
||||
/// {@endtemplate}
|
||||
final bool canRequestFocus;
|
||||
|
||||
/// 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
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'binding.dart';
|
||||
import 'focus_manager.dart';
|
||||
@ -790,3 +793,138 @@ class DefaultFocusTraversal extends InheritedWidget {
|
||||
@override
|
||||
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;
|
||||
}
|
||||
final KeySet<T> typedOther = other;
|
||||
return _keys.length == typedOther._keys.length && _keys.containsAll(typedOther._keys);
|
||||
return setEquals<T>(_keys, typedOther._keys);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -169,10 +169,7 @@ class ShortcutManager extends ChangeNotifier with DiagnosticableMixin {
|
||||
Map<LogicalKeySet, Intent> get shortcuts => _shortcuts;
|
||||
Map<LogicalKeySet, Intent> _shortcuts;
|
||||
set shortcuts(Map<LogicalKeySet, Intent> value) {
|
||||
if (_shortcuts == value) {
|
||||
return;
|
||||
}
|
||||
if (_shortcuts != value) {
|
||||
if (!mapEquals<LogicalKeySet, Intent>(_shortcuts, value)) {
|
||||
_shortcuts = value;
|
||||
notifyListeners();
|
||||
}
|
||||
@ -259,6 +256,10 @@ class Shortcuts extends StatefulWidget {
|
||||
final ShortcutManager manager;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// The child widget for this [Shortcuts] widget.
|
||||
@ -324,15 +325,15 @@ class _ShortcutsState extends State<Shortcuts> {
|
||||
@override
|
||||
void didUpdateWidget(Shortcuts oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.manager != oldWidget.manager || widget.shortcuts != oldWidget.shortcuts) {
|
||||
if (widget.manager != oldWidget.manager) {
|
||||
if (widget.manager != null) {
|
||||
_internalManager?.dispose();
|
||||
_internalManager = null;
|
||||
} else {
|
||||
_internalManager ??= ShortcutManager();
|
||||
}
|
||||
manager.shortcuts = widget.shortcuts;
|
||||
}
|
||||
manager.shortcuts = widget.shortcuts;
|
||||
}
|
||||
|
||||
bool _handleOnKey(FocusNode node, RawKeyEvent event) {
|
||||
@ -345,7 +346,7 @@ class _ShortcutsState extends State<Shortcuts> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
debugLabel: describeIdentity(widget),
|
||||
debugLabel: '$Shortcuts',
|
||||
canRequestFocus: false,
|
||||
onKey: _handleOnKey,
|
||||
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),
|
||||
transform: null,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasToggledState,
|
||||
SemanticsFlag.isToggled,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.hasToggledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
SemanticsFlag.isToggled,
|
||||
],
|
||||
actions: SemanticsAction.tap.index,
|
||||
label: 'aaa\nAAA',
|
||||
@ -255,9 +256,10 @@ void main() {
|
||||
transform: Matrix4.translationValues(0.0, 56.0, 0.0),
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.isChecked,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isChecked,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: SemanticsAction.tap.index,
|
||||
label: 'bbb\nBBB',
|
||||
@ -270,6 +272,7 @@ void main() {
|
||||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
SemanticsFlag.isInMutuallyExclusiveGroup,
|
||||
],
|
||||
actions: SemanticsAction.tap.index,
|
||||
|
@ -432,12 +432,16 @@ void _tests() {
|
||||
thickness: 0.0,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: '2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isSelected],
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: 'Fri, Jan 15',
|
||||
textDirection: TextDirection.ltr,
|
||||
|
@ -1028,24 +1028,28 @@ void main() {
|
||||
TestSemantics(
|
||||
label: 'one',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
),
|
||||
TestSemantics(
|
||||
label: 'two',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
),
|
||||
TestSemantics(
|
||||
label: 'three',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
),
|
||||
TestSemantics(
|
||||
label: 'four',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
),
|
||||
|
@ -1014,6 +1014,7 @@ void main() {
|
||||
expect(tester.getSemantics(find.byKey(expandedKey)), matchesSemantics(
|
||||
label: 'Expanded',
|
||||
isButton: true,
|
||||
isFocusable: true,
|
||||
hasEnabledState: true,
|
||||
hasTapAction: true,
|
||||
));
|
||||
@ -1021,6 +1022,7 @@ void main() {
|
||||
expect(tester.getSemantics(find.byKey(collapsedKey)), matchesSemantics(
|
||||
label: 'Collapsed',
|
||||
isButton: true,
|
||||
isFocusable: true,
|
||||
hasEnabledState: true,
|
||||
hasTapAction: true,
|
||||
));
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
@ -251,6 +252,102 @@ void main() {
|
||||
await gesture.up();
|
||||
}, 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 {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/14391
|
||||
await tester.pumpWidget(
|
||||
@ -331,5 +428,4 @@ void main() {
|
||||
throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
|
||||
}));
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -103,9 +103,9 @@ void main() {
|
||||
splashColor: const Color(0xffff0000),
|
||||
focusColor: const Color(0xff0000ff),
|
||||
highlightColor: const Color(0xf00fffff),
|
||||
onTap: () {},
|
||||
onLongPress: () {},
|
||||
onHover: (bool hover) {},
|
||||
onTap: () { },
|
||||
onLongPress: () { },
|
||||
onHover: (bool hover) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -123,29 +123,29 @@ void main() {
|
||||
testWidgets('ink response changes color on focus', (WidgetTester tester) async {
|
||||
WidgetsBinding.instance.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
|
||||
await tester.pumpWidget(Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Focus(
|
||||
focusNode: focusNode,
|
||||
await tester.pumpWidget(
|
||||
Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: InkWell(
|
||||
focusNode: focusNode,
|
||||
hoverColor: const Color(0xff00ff00),
|
||||
splashColor: const Color(0xffff0000),
|
||||
focusColor: const Color(0xff0000ff),
|
||||
highlightColor: const Color(0xf00fffff),
|
||||
onTap: () {},
|
||||
onLongPress: () {},
|
||||
onHover: (bool hover) {},
|
||||
onTap: () { },
|
||||
onLongPress: () { },
|
||||
onHover: (bool hover) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0));
|
||||
@ -172,9 +172,9 @@ void main() {
|
||||
splashColor: const Color(0xffff0000),
|
||||
focusColor: const Color(0xff0000ff),
|
||||
highlightColor: const Color(0xf00fffff),
|
||||
onTap: () {},
|
||||
onLongPress: () {},
|
||||
onHover: (bool hover) {},
|
||||
onTap: () { },
|
||||
onLongPress: () { },
|
||||
onHover: (bool hover) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -206,8 +206,8 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
onLongPress: () {},
|
||||
onTap: () { },
|
||||
onLongPress: () { },
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -234,8 +234,8 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
onLongPress: () {},
|
||||
onTap: () { },
|
||||
onLongPress: () { },
|
||||
enableFeedback: false,
|
||||
),
|
||||
),
|
||||
@ -301,7 +301,7 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Material(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
onTap: () { },
|
||||
child: const Text('Button'),
|
||||
),
|
||||
),
|
||||
@ -312,7 +312,7 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Material(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
onTap: () { },
|
||||
child: const Text('Button'),
|
||||
excludeFromSemantics: true,
|
||||
),
|
||||
|
@ -368,33 +368,41 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, hasSemantics(
|
||||
TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics.rootChild(
|
||||
label: 'one',
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
],
|
||||
),
|
||||
TestSemantics.rootChild(
|
||||
label: 'two',
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
],
|
||||
),
|
||||
TestSemantics.rootChild(
|
||||
label: 'three',
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasEnabledState,
|
||||
],
|
||||
),
|
||||
],
|
||||
expect(
|
||||
semantics,
|
||||
hasSemantics(
|
||||
TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics.rootChild(
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
label: 'one',
|
||||
),
|
||||
TestSemantics.rootChild(
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
label: 'two',
|
||||
),
|
||||
TestSemantics.rootChild(
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
label: 'three',
|
||||
),
|
||||
],
|
||||
),
|
||||
ignoreTransform: true,
|
||||
ignoreId: true,
|
||||
ignoreRect: true,
|
||||
),
|
||||
ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
||||
);
|
||||
|
||||
semantics.dispose();
|
||||
|
@ -520,26 +520,28 @@ void main() {
|
||||
|
||||
testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: PopupMenuButton<int>(
|
||||
itemBuilder: (BuildContext context) {
|
||||
return <PopupMenuItem<int>>[
|
||||
const PopupMenuItem<int>(value: 1, child: Text('1')),
|
||||
const PopupMenuItem<int>(value: 2, child: Text('2')),
|
||||
const PopupMenuItem<int>(value: 3, child: Text('3')),
|
||||
const PopupMenuItem<int>(value: 4, child: Text('4')),
|
||||
const PopupMenuItem<int>(value: 5, child: Text('5')),
|
||||
];
|
||||
},
|
||||
child: const SizedBox(
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
child: Text('XXX'),
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: PopupMenuButton<int>(
|
||||
itemBuilder: (BuildContext context) {
|
||||
return <PopupMenuItem<int>>[
|
||||
const PopupMenuItem<int>(value: 1, child: Text('1')),
|
||||
const PopupMenuItem<int>(value: 2, child: Text('2')),
|
||||
const PopupMenuItem<int>(value: 3, child: Text('3')),
|
||||
const PopupMenuItem<int>(value: 4, child: Text('4')),
|
||||
const PopupMenuItem<int>(value: 5, child: Text('5')),
|
||||
];
|
||||
},
|
||||
child: const SizedBox(
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
child: Text('XXX'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
await tester.tap(find.text('XXX'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -563,26 +565,31 @@ void main() {
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: '1',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: '2',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: '3',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: '4',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: '5',
|
||||
textDirection: TextDirection.ltr,
|
||||
|
@ -5,12 +5,77 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/src/services/keyboard_key.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
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 {
|
||||
int pressed = 0;
|
||||
|
||||
|
@ -469,6 +469,7 @@ void main() {
|
||||
hasToggledState: true,
|
||||
isToggled: true,
|
||||
isEnabled: true,
|
||||
isFocusable: true,
|
||||
hasEnabledState: true,
|
||||
label: 'Switch tile',
|
||||
hasTapAction: true,
|
||||
|
@ -1614,15 +1614,19 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 4,
|
||||
actions: SemanticsAction.tap.index,
|
||||
flags: SemanticsFlag.isSelected.index,
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
label: 'TAB #0\nTab 1 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||
),
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
actions: SemanticsAction.tap.index,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: 'TAB #1\nTab 2 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||
@ -1878,15 +1882,19 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 4,
|
||||
actions: SemanticsAction.tap.index,
|
||||
flags: SemanticsFlag.isSelected.index,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: 'Semantics override 0\nTab 1 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||
),
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
actions: SemanticsAction.tap.index,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: 'Semantics override 1\nTab 2 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||
|
@ -481,6 +481,7 @@ void main() {
|
||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
||||
label: 'Signed in\nname\nemail',
|
||||
textDirection: TextDirection.ltr,
|
||||
children: <TestSemantics>[
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -914,5 +916,112 @@ void main() {
|
||||
expect(focusCenter.hasFocus, isFalse);
|
||||
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