Unverified Commit 4512a1c1 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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.
parent 0af2a84c
......@@ -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),
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,
),
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,
deleteIcon: AnimatedSwitcher(
child: _buildDeleteIcon(context, theme, chipTheme),
duration: _kDrawerDuration,
switchInCurve: Curves.fastOutSlowIn,
),
value: widget.selected,
checkmarkAnimation: checkmarkAnimation,
enableAnimation: enableAnimation,
avatarDrawerAnimation: avatarDrawerAnimation,
deleteDrawerAnimation: deleteDrawerAnimation,
isEnabled: widget.isEnabled,
avatarBorder: widget.avatarBorder,
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,7 +483,6 @@ 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>{};
......@@ -474,27 +494,18 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
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 +571,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 +585,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 +627,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 +756,37 @@ 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: <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 @@ 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 +933,9 @@ class InkWell extends InkResponse {
customBorder: customBorder,
enableFeedback: enableFeedback ?? true,
excludeFromSemantics: excludeFromSemantics ?? false,
focusNode: focusNode,
canRequestFocus: canRequestFocus ?? true,
onFocusChange: onFocusChange,
autofocus: autofocus ?? false,
);
}
......@@ -344,6 +344,20 @@ class Actions extends InheritedWidget {
return oldWidget.dispatcher != dispatcher || oldWidget.actions != actions;
}
@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);
......@@ -368,3 +382,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';
......@@ -1195,18 +1197,23 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
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,
),
),
),
),
......
......@@ -146,10 +146,11 @@ class Focus extends StatefulWidget {
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 @@ 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 +231,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 +251,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
......
......@@ -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 @@ void main() {
..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 @@ 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 +432,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,
),
......
......@@ -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;
......
......@@ -324,11 +324,11 @@ void main() {
).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}'));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment