Add an ActivateAction to controls that use InkWell. (#41220)
Adds an ActivateAction to controls that use InkWell. Make InkWell host the Focus for those controls, and add the top level binding to the ENTER key. This will make it possible to trigger a button using the enter key, and to get an ink response when the button is triggered.
This is a breaking change because it moves the Focus widget into the InkWell. If you have a component that uses the InkWell directly and you used to wrap that InkWell in a Focus widget to give it its notion of focus, it will now not look for that Focus ancestor for its focus state anymore. In order to control focus on the InkWell, you need to give it a FocusNode directly, via the new focusNode parameter. This should not affect users of widgets like OutlineButton or FloatingActionButton and the like, since those have been modified in this PR.
diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
index 2151cca..f32b268 100644
--- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart
+++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
@@ -474,45 +474,43 @@
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,
+ ),
+ ],
),
),
);
diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart
index 3f438f1..367344b 100644
--- a/packages/flutter/lib/src/material/button.dart
+++ b/packages/flutter/lib/src/material/button.dart
@@ -330,39 +330,37 @@
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,
),
),
),
diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart
index 669abac..64d0869 100644
--- a/packages/flutter/lib/src/material/chip.dart
+++ b/packages/flutter/lib/src/material/chip.dart
@@ -1774,73 +1774,71 @@
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,
),
),
),
diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart
index e2ef27d..6fe788d 100644
--- a/packages/flutter/lib/src/material/icon_button.dart
+++ b/packages/flutter/lib/src/material/icon_button.dart
@@ -309,22 +309,20 @@
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.
),
),
);
diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart
index a69c8ff..01628a6 100644
--- a/packages/flutter/lib/src/material/ink_well.dart
+++ b/packages/flutter/lib/src/material/ink_well.dart
@@ -210,10 +210,16 @@
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 @@
/// 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,7 +483,6 @@
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>{};
@@ -475,26 +495,17 @@
}
@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 +571,7 @@
}
assert(value == (_highlights[type] != null && _highlights[type].active));
- switch(type) {
+ switch (type) {
case _HighlightType.pressed:
if (widget.onHighlightChanged != null)
widget.onHighlightChanged(value);
@@ -574,10 +585,10 @@
}
}
- 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 +627,54 @@
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 +756,37 @@
_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: <LocalKey, ActionFactory>{
+ ActivateAction.key: () {
+ return CallbackAction(
+ ActivateAction.key,
+ onInvoke: (FocusNode node, Intent intent) {
+ _startSplash(context: node.context);
+ _handleTap(node.context);
+ },
+ );
+ },
+ },
+ 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 +907,10 @@
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 +933,9 @@
customBorder: customBorder,
enableFeedback: enableFeedback ?? true,
excludeFromSemantics: excludeFromSemantics ?? false,
+ focusNode: focusNode,
+ canRequestFocus: canRequestFocus ?? true,
+ onFocusChange: onFocusChange,
+ autofocus: autofocus ?? false,
);
}
diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart
index 8008ee0..e642da2 100644
--- a/packages/flutter/lib/src/widgets/actions.dart
+++ b/packages/flutter/lib/src/widgets/actions.dart
@@ -345,6 +345,20 @@
}
@override
+ bool operator ==(dynamic other) {
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ if (identical(this, other)) {
+ return true;
+ }
+ return !updateShouldNotify(other);
+ }
+
+ @override
+ int get hashCode => hashValues(dispatcher, actions);
+
+ @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
@@ -368,3 +382,16 @@
@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);
+}
diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart
index 9b1e990..d830082 100644
--- a/packages/flutter/lib/src/widgets/app.dart
+++ b/packages/flutter/lib/src/widgets/app.dart
@@ -7,6 +7,7 @@
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 'pages.dart';
import 'performance_overlay.dart';
import 'semantics_debugger.dart';
+import 'shortcuts.dart';
import 'text.dart';
import 'title.dart';
import 'widget_inspector.dart';
@@ -1195,18 +1197,23 @@
assert(_debugCheckLocalizations(appLocale));
- return Actions(
- actions: <LocalKey, ActionFactory>{
- DoNothingAction.key: () => const DoNothingAction(),
+ return Shortcuts(
+ shortcuts: <LogicalKeySet, Intent>{
+ LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
},
- child: DefaultFocusTraversal(
- policy: ReadingOrderTraversalPolicy(),
- child: MediaQuery(
- data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
- child: Localizations(
- locale: appLocale,
- delegates: _localizationsDelegates.toList(),
- child: title,
+ child: Actions(
+ actions: <LocalKey, ActionFactory>{
+ DoNothingAction.key: () => const DoNothingAction(),
+ },
+ child: DefaultFocusTraversal(
+ policy: ReadingOrderTraversalPolicy(),
+ child: MediaQuery(
+ data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
+ child: Localizations(
+ locale: appLocale,
+ delegates: _localizationsDelegates.toList(),
+ child: title,
+ ),
),
),
),
diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart
index f829d05..760e97c 100644
--- a/packages/flutter/lib/src/widgets/focus_scope.dart
+++ b/packages/flutter/lib/src/widgets/focus_scope.dart
@@ -146,10 +146,11 @@
this.onFocusChange,
this.onKey,
this.debugLabel,
- this.canRequestFocus,
+ this.canRequestFocus = true,
this.skipTraversal,
}) : assert(child != null),
assert(autofocus != null),
+ assert(canRequestFocus != null),
super(key: key);
/// A debug label for this widget.
@@ -186,7 +187,7 @@
/// 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 +231,7 @@
/// 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 +251,7 @@
/// 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
diff --git a/packages/flutter/test/material/ink_paint_test.dart b/packages/flutter/test/material/ink_paint_test.dart
index 03cd7c1..643f814 100644
--- a/packages/flutter/test/material/ink_paint_test.dart
+++ b/packages/flutter/test/material/ink_paint_test.dart
@@ -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';
@@ -127,17 +128,21 @@
..translate(x: 0.0, y: 0.0)
..translate(x: tapDownOffset.dx, y: tapDownOffset.dy)
..something((Symbol method, List<dynamic> arguments) {
- if (method != #drawCircle)
+ if (method != #drawCircle) {
return false;
+ }
final Offset center = arguments[0];
final double radius = arguments[1];
final Paint paint = arguments[2];
- if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == expectedAlpha)
+ if (offsetsAreClose(center, expectedCenter) &&
+ radiiAreClose(radius, expectedRadius) &&
+ paint.color.alpha == expectedAlpha) {
return true;
+ }
throw '''
Expected: center == $expectedCenter, radius == $expectedRadius, alpha == $expectedAlpha
Found: center == $center radius == $radius alpha == ${paint.color.alpha}''';
- }
+ },
);
}
@@ -251,6 +256,102 @@
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 +432,4 @@
throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
}));
});
-
}
diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart
index 9b4a8f9..dfad562 100644
--- a/packages/flutter/test/material/ink_well_test.dart
+++ b/packages/flutter/test/material/ink_well_test.dart
@@ -103,9 +103,9 @@
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 @@
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 @@
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 @@
textDirection: TextDirection.ltr,
child: Center(
child: InkWell(
- onTap: () {},
- onLongPress: () {},
+ onTap: () { },
+ onLongPress: () { },
),
),
),
@@ -234,8 +234,8 @@
textDirection: TextDirection.ltr,
child: Center(
child: InkWell(
- onTap: () {},
- onLongPress: () {},
+ onTap: () { },
+ onLongPress: () { },
enableFeedback: false,
),
),
@@ -301,7 +301,7 @@
textDirection: TextDirection.ltr,
child: Material(
child: InkWell(
- onTap: () {},
+ onTap: () { },
child: const Text('Button'),
),
),
@@ -312,7 +312,7 @@
textDirection: TextDirection.ltr,
child: Material(
child: InkWell(
- onTap: () {},
+ onTap: () { },
child: const Text('Button'),
excludeFromSemantics: true,
),
diff --git a/packages/flutter/test/material/raw_material_button_test.dart b/packages/flutter/test/material/raw_material_button_test.dart
index bed44b0..074f3da 100644
--- a/packages/flutter/test/material/raw_material_button_test.dart
+++ b/packages/flutter/test/material/raw_material_button_test.dart
@@ -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;
diff --git a/packages/flutter/test/widgets/actions_test.dart b/packages/flutter/test/widgets/actions_test.dart
index 1a7454f..9533598 100644
--- a/packages/flutter/test/widgets/actions_test.dart
+++ b/packages/flutter/test/widgets/actions_test.dart
@@ -324,11 +324,11 @@
).debugFillProperties(builder);
final List<String> description = builder.properties
- .where((DiagnosticsNode node) {
- return !node.isFiltered(DiagnosticLevel.info);
- })
- .map((DiagnosticsNode node) => node.toString())
- .toList();
+ .where((DiagnosticsNode node) {
+ return !node.isFiltered(DiagnosticLevel.info);
+ })
+ .map((DiagnosticsNode node) => node.toString())
+ .toList();
expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
expect(description[1], equals('actions: {[<\'bar\'>]: Closure: () => TestAction}'));