diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart index 2151ccac464..f32b2689fbb 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 @@ class _BottomNavigationTile extends StatelessWidget { child: Semantics( container: true, selected: selected, - child: Focus( - child: Stack( - children: [ - InkResponse( - onTap: onTap, - child: Padding( - padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - _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: [ + InkResponse( + onTap: onTap, + child: Padding( + padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + _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 3f438f1261c..367344bf34d 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -330,39 +330,37 @@ class _RawMaterialButtonState extends State { final Color effectiveTextColor = MaterialStateProperty.resolveAs(widget.textStyle?.color, _states); final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs(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 669abac8c52..64d0869bd76 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -1774,73 +1774,71 @@ class _RawChipState extends State with TickerProviderStateMixin(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([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([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 e2ef27ddace..6fe788db1d8 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -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. ), ), ); diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index a69c8ff3dcc..01628a69b3a 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -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 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 extends State with AutomaticKeepAliveClientMixin { Set _splashes; InteractiveInkFeature _currentSplash; - FocusNode _focusNode; bool _hovering = false; final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{}; @@ -474,27 +494,18 @@ class _InkResponseState extends State 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 extends State 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 extends State 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 extends State 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(); - _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(); + _splashes.add(splash); + _currentSplash = splash; updateKeepAlive(); updateHighlight(_HighlightType.pressed, value: true); } @@ -722,18 +756,37 @@ class _InkResponseState extends State 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: { + 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 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, ); } diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart index 8008ee04ca9..e642da25816 100644 --- a/packages/flutter/lib/src/widgets/actions.dart +++ b/packages/flutter/lib/src/widgets/actions.dart @@ -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(ActivateAction); +} diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 9b1e990d24e..d830082d00b 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -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 implements WidgetsBindingObserv assert(_debugCheckLocalizations(appLocale)); - return Actions( - actions: { - DoNothingAction.key: () => const DoNothingAction(), + return Shortcuts( + shortcuts: { + 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: { + 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 f829d05de76..760e97c4c94 100644 --- a/packages/flutter/lib/src/widgets/focus_scope.dart +++ b/packages/flutter/lib/src/widgets/focus_scope.dart @@ -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 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 diff --git a/packages/flutter/test/material/ink_paint_test.dart b/packages/flutter/test/material/ink_paint_test.dart index 03cd7c10521..643f81455c0 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 @@ void main() { ..translate(x: 0.0, y: 0.0) ..translate(x: tapDownOffset.dx, y: tapDownOffset.dy) ..something((Symbol method, List 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(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 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}'; })); }); - } diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart index 9b4a8f93b47..dfad562e235 100644 --- a/packages/flutter/test/material/ink_well_test.dart +++ b/packages/flutter/test/material/ink_well_test.dart @@ -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, ), diff --git a/packages/flutter/test/material/raw_material_button_test.dart b/packages/flutter/test/material/raw_material_button_test.dart index bed44b050aa..074f3da4468 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(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 1a7454f0548..9533598c072 100644 --- a/packages/flutter/test/widgets/actions_test.dart +++ b/packages/flutter/test/widgets/actions_test.dart @@ -324,11 +324,11 @@ void main() { ).debugFillProperties(builder); final List 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}'));