diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart index afc4c9461c7..5d326c4c70f 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'button_theme.dart'; @@ -11,6 +12,7 @@ import 'constants.dart'; import 'ink_well.dart'; import 'material.dart'; import 'theme.dart'; +import 'theme_data.dart'; /// Creates a button based on [Semantics], [Material], and [InkWell] /// widgets. @@ -38,13 +40,14 @@ class RawMaterialButton extends StatefulWidget { this.elevation = 2.0, this.highlightElevation = 8.0, this.disabledElevation = 0.0, - this.outerPadding, this.padding = EdgeInsets.zero, this.constraints = const BoxConstraints(minWidth: 88.0, minHeight: 36.0), this.shape = const RoundedRectangleBorder(), this.animationDuration = kThemeChangeDuration, + MaterialTapTargetSize materialTapTargetSize, this.child, - }) : assert(shape != null), + }) : this.materialTapTargetSize = materialTapTargetSize ?? MaterialTapTargetSize.padded, + assert(shape != null), assert(elevation != null), assert(highlightElevation != null), assert(disabledElevation != null), @@ -58,10 +61,6 @@ class RawMaterialButton extends StatefulWidget { /// If this is set to null, the button will be disabled, see [enabled]. final VoidCallback onPressed; - /// Padding to increase the size of the gesture detector which doesn't - /// increase the visible material of the button. - final EdgeInsets outerPadding; - /// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged] /// callback. final ValueChanged onHighlightChanged; @@ -138,6 +137,15 @@ class RawMaterialButton extends StatefulWidget { /// property to a non-null value. bool get enabled => onPressed != null; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [MaterialTapTargetSize.padded]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + @override _RawMaterialButtonState createState() => new _RawMaterialButtonState(); } @@ -186,18 +194,23 @@ class _RawMaterialButtonState extends State { ), ), ); - - if (widget.outerPadding != null) { - result = new GestureDetector( - behavior: HitTestBehavior.translucent, - excludeFromSemantics: true, - onTap: widget.onPressed, - child: new Padding( - padding: widget.outerPadding, - child: result - ), - ); + BoxConstraints constraints; + switch (widget.materialTapTargetSize) { + case MaterialTapTargetSize.padded: + constraints = const BoxConstraints(minWidth: 48.0, minHeight: 48.0); + break; + case MaterialTapTargetSize.shrinkWrap: + constraints = const BoxConstraints(); + break; } + result = new _ButtonRedirectingHitDetectionWidget( + constraints: constraints, + child: new Center( + child: result, + widthFactor: 1.0, + heightFactor: 1.0, + ), + ); return new Semantics( container: true, @@ -248,6 +261,7 @@ class MaterialButton extends StatelessWidget { this.minWidth, this.height, this.padding, + this.materialTapTargetSize, @required this.onPressed, this.child }) : super(key: key); @@ -353,6 +367,15 @@ class MaterialButton extends StatelessWidget { /// {@macro flutter.widgets.child} final Widget child; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + /// Whether the button is enabled or disabled. Buttons are disabled by default. To /// enable a button, set its [onPressed] property to a non-null value. bool get enabled => onPressed != null; @@ -412,6 +435,7 @@ class MaterialButton extends StatelessWidget { ), shape: buttonTheme.shape, child: child, + materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize, ); } @@ -421,3 +445,38 @@ class MaterialButton extends StatelessWidget { properties.add(new FlagProperty('enabled', value: enabled, ifFalse: 'disabled')); } } + +/// Redirects the position passed to [RenderBox.hitTest] to the center of the widget. +/// +/// The primary purpose of this widget is to allow padding around [Material] widgets +/// to trigger the child ink feature without increasing the size of the material. +class _ButtonRedirectingHitDetectionWidget extends SingleChildRenderObjectWidget { + const _ButtonRedirectingHitDetectionWidget({ + Key key, + Widget child, + this.constraints + }) : super(key: key, child: child); + + final BoxConstraints constraints; + + @override + RenderObject createRenderObject(BuildContext context) { + return new _RenderButtonRedirectingHitDetection(constraints); + } + + @override + void updateRenderObject(BuildContext context, covariant _RenderButtonRedirectingHitDetection renderObject) { + renderObject.additionalConstraints = constraints; + } +} + +class _RenderButtonRedirectingHitDetection extends RenderConstrainedBox { + _RenderButtonRedirectingHitDetection (BoxConstraints additionalConstraints) : super(additionalConstraints: additionalConstraints); + + @override + bool hitTest(HitTestResult result, {Offset position}) { + if (!size.contains(position)) + return false; + return child.hitTest(result, position: size.center(Offset.zero)); + } +} diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart index 52e7cefcfb1..953cf961a70 100644 --- a/packages/flutter/lib/src/material/checkbox.dart +++ b/packages/flutter/lib/src/material/checkbox.dart @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; import 'constants.dart'; import 'debug.dart'; import 'theme.dart'; +import 'theme_data.dart'; import 'toggleable.dart'; /// A material design checkbox. @@ -58,6 +59,7 @@ class Checkbox extends StatefulWidget { this.tristate = false, @required this.onChanged, this.activeColor, + this.materialTapTargetSize, }) : assert(tristate != null), assert(tristate || value != null), super(key: key); @@ -113,6 +115,15 @@ class Checkbox extends StatefulWidget { /// If tristate is false (the default), [value] must not be null. final bool tristate; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + /// The width of a checkbox widget. static const double width = 18.0; @@ -125,12 +136,23 @@ class _CheckboxState extends State with TickerProviderStateMixin { Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData themeData = Theme.of(context); + Size size; + switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { + case MaterialTapTargetSize.padded: + size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0); + break; + case MaterialTapTargetSize.shrinkWrap: + size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius); + break; + } + final BoxConstraints additionalConstraints = new BoxConstraints.tight(size); return new _CheckboxRenderObjectWidget( value: widget.value, tristate: widget.tristate, activeColor: widget.activeColor ?? themeData.toggleableActiveColor, inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor, onChanged: widget.onChanged, + additionalConstraints: additionalConstraints, vsync: this, ); } @@ -145,6 +167,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { @required this.inactiveColor, @required this.onChanged, @required this.vsync, + @required this.additionalConstraints, }) : assert(tristate != null), assert(tristate || value != null), assert(activeColor != null), @@ -158,6 +181,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { final Color inactiveColor; final ValueChanged onChanged; final TickerProvider vsync; + final BoxConstraints additionalConstraints; @override _RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox( @@ -167,6 +191,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { inactiveColor: inactiveColor, onChanged: onChanged, vsync: vsync, + additionalConstraints: additionalConstraints, ); @override @@ -177,6 +202,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ..activeColor = activeColor ..inactiveColor = inactiveColor ..onChanged = onChanged + ..additionalConstraints = additionalConstraints ..vsync = vsync; } } @@ -191,6 +217,7 @@ class _RenderCheckbox extends RenderToggleable { bool tristate, Color activeColor, Color inactiveColor, + BoxConstraints additionalConstraints, ValueChanged onChanged, @required TickerProvider vsync, }): _oldValue = value, @@ -200,7 +227,7 @@ class _RenderCheckbox extends RenderToggleable { activeColor: activeColor, inactiveColor: inactiveColor, onChanged: onChanged, - size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), + additionalConstraints: additionalConstraints, vsync: vsync, ); diff --git a/packages/flutter/lib/src/material/checkbox_list_tile.dart b/packages/flutter/lib/src/material/checkbox_list_tile.dart index d85d1e80065..9c1c9fe34f1 100644 --- a/packages/flutter/lib/src/material/checkbox_list_tile.dart +++ b/packages/flutter/lib/src/material/checkbox_list_tile.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'checkbox.dart'; import 'list_tile.dart'; import 'theme.dart'; +import 'theme_data.dart'; /// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label. /// @@ -173,6 +174,7 @@ class CheckboxListTile extends StatelessWidget { value: value, onChanged: onChanged, activeColor: activeColor, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); Widget leading, trailing; switch (controlAffinity) { diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index cec26a6242b..9ac6524b5a6 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -17,6 +17,7 @@ import 'ink_well.dart'; import 'material.dart'; import 'material_localizations.dart'; import 'theme.dart'; +import 'theme_data.dart'; import 'tooltip.dart'; // Some design constants @@ -99,6 +100,15 @@ abstract class ChipAttributes { /// By default, this is 4 logical pixels at the beginning and the end of the /// label, and zero on top and bottom. EdgeInsetsGeometry get labelPadding; + + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + MaterialTapTargetSize get materialTapTargetSize; } /// An interface for material design chips that can be deleted. @@ -423,6 +433,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri this.shape, this.backgroundColor, this.padding, + this.materialTapTargetSize, }) : assert(label != null), super(key: key); @@ -448,6 +459,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri final Color deleteIconColor; @override final String deleteButtonTooltipMessage; + @override + final MaterialTapTargetSize materialTapTargetSize; @override Widget build(BuildContext context) { @@ -465,6 +478,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri shape: shape, backgroundColor: backgroundColor, padding: padding, + materialTapTargetSize: materialTapTargetSize, isEnabled: true, ); } @@ -547,6 +561,7 @@ class InputChip extends StatelessWidget this.shape, this.backgroundColor, this.padding, + this.materialTapTargetSize, }) : assert(selected != null), assert(isEnabled != null), assert(label != null), @@ -588,6 +603,8 @@ class InputChip extends StatelessWidget final Color backgroundColor; @override final EdgeInsetsGeometry padding; + @override + final MaterialTapTargetSize materialTapTargetSize; @override Widget build(BuildContext context) { @@ -611,6 +628,7 @@ class InputChip extends StatelessWidget shape: shape, backgroundColor: backgroundColor, padding: padding, + materialTapTargetSize: materialTapTargetSize, isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null), ); } @@ -689,6 +707,7 @@ class ChoiceChip extends StatelessWidget this.shape, this.backgroundColor, this.padding, + this.materialTapTargetSize, }) : assert(selected != null), assert(label != null), super(key: key); @@ -717,6 +736,8 @@ class ChoiceChip extends StatelessWidget final Color backgroundColor; @override final EdgeInsetsGeometry padding; + @override + final MaterialTapTargetSize materialTapTargetSize; @override bool get isEnabled => onSelected != null; @@ -741,6 +762,7 @@ class ChoiceChip extends StatelessWidget backgroundColor: backgroundColor, padding: padding, isEnabled: isEnabled, + materialTapTargetSize: materialTapTargetSize, ); } } @@ -852,6 +874,7 @@ class FilterChip extends StatelessWidget this.shape, this.backgroundColor, this.padding, + this.materialTapTargetSize, }) : assert(selected != null), assert(label != null), super(key: key); @@ -880,6 +903,8 @@ class FilterChip extends StatelessWidget final Color backgroundColor; @override final EdgeInsetsGeometry padding; + @override + final MaterialTapTargetSize materialTapTargetSize; @override bool get isEnabled => onSelected != null; @@ -901,6 +926,7 @@ class FilterChip extends StatelessWidget selectedColor: selectedColor, padding: padding, isEnabled: isEnabled, + materialTapTargetSize: materialTapTargetSize, ); } } @@ -966,6 +992,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.shape, this.backgroundColor, this.padding, + this.materialTapTargetSize, }) : assert(label != null), assert( onPressed != null, @@ -992,6 +1019,8 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip final Color backgroundColor; @override final EdgeInsetsGeometry padding; + @override + final MaterialTapTargetSize materialTapTargetSize; @override Widget build(BuildContext context) { @@ -1007,6 +1036,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip padding: padding, labelPadding: labelPadding, isEnabled: true, + materialTapTargetSize: materialTapTargetSize ); } } @@ -1076,6 +1106,7 @@ class RawChip extends StatefulWidget this.tooltip, this.shape, this.backgroundColor, + this.materialTapTargetSize, }) : assert(label != null), assert(isEnabled != null), deleteIcon = deleteIcon ?? _kDefaultDeleteIcon, @@ -1117,6 +1148,8 @@ class RawChip extends StatefulWidget final Color backgroundColor; @override final EdgeInsetsGeometry padding; + @override + final MaterialTapTargetSize materialTapTargetSize; /// Whether or not to show a check mark when [selected] is true. /// @@ -1368,7 +1401,7 @@ class _RawChipState extends State with TickerProviderStateMixin with TickerProviderStateMixin 0.66) + hitTestChild = deleteIcon ?? label ?? avatar; + else + hitTestChild = label ?? avatar; + break; + case TextDirection.rtl: + if (position.dx / size.width < 0.33) + hitTestChild = deleteIcon ?? label ?? avatar; + else + hitTestChild = label ?? avatar; + break; + } + return hitTestChild?.hitTest(result, position: hitTestChild.size.center(Offset.zero)) ?? false; + } + @override void performLayout() { final BoxConstraints contentConstraints = constraints.loosen(); @@ -2250,20 +2367,4 @@ class _RenderChip extends RenderBox { @override bool hitTestSelf(Offset position) => deleteButtonRect.contains(position) || pressRect.contains(position); - - @override - bool hitTestChildren(HitTestResult result, {@required Offset position}) { - assert(position != null); - if (deleteIcon != null && deleteButtonRect.contains(position)) { - // This simulates a position at the center of the delete icon if the hit - // on the chip is inside of the delete area. - return deleteIcon.hitTest(result, position: (Offset.zero & _boxSize(deleteIcon)).center); - } - for (RenderBox child in _children) { - if (child.hasSize && child.hitTest(result, position: position - _boxParentData(child).offset)) { - return true; - } - } - return false; - } } diff --git a/packages/flutter/lib/src/material/flat_button.dart b/packages/flutter/lib/src/material/flat_button.dart index ee002ab8e02..a816abd9236 100644 --- a/packages/flutter/lib/src/material/flat_button.dart +++ b/packages/flutter/lib/src/material/flat_button.dart @@ -9,6 +9,7 @@ import 'button.dart'; import 'button_theme.dart'; import 'colors.dart'; import 'theme.dart'; +import 'theme_data.dart'; /// A material design "flat button". /// @@ -62,6 +63,7 @@ class FlatButton extends StatelessWidget { this.colorBrightness, this.padding, this.shape, + this.materialTapTargetSize, @required this.child, }) : super(key: key); @@ -85,6 +87,7 @@ class FlatButton extends StatelessWidget { this.splashColor, this.colorBrightness, this.shape, + this.materialTapTargetSize, @required Widget icon, @required Widget label, }) : assert(icon != null), @@ -185,6 +188,15 @@ class FlatButton extends StatelessWidget { /// Defaults to the theme's brightness, [ThemeData.brightness]. final Brightness colorBrightness; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + /// The widget below this widget in the tree. /// /// Typically a [Text] widget in all caps. @@ -290,6 +302,7 @@ class FlatButton extends StatelessWidget { splashColor: _getSplashColor(theme, buttonTheme), elevation: 0.0, highlightElevation: 0.0, + materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize, padding: padding ?? buttonTheme.padding, constraints: buttonTheme.constraints, shape: shape ?? buttonTheme.shape, @@ -311,5 +324,6 @@ class FlatButton extends StatelessWidget { properties.add(new DiagnosticsProperty('colorBrightness', colorBrightness, defaultValue: null)); properties.add(new DiagnosticsProperty('padding', padding, defaultValue: null)); properties.add(new DiagnosticsProperty('shape', shape, defaultValue: null)); + properties.add(new DiagnosticsProperty('materialTapTargetSize', materialTapTargetSize, defaultValue: null)); } } diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart index 86f93e42686..ae21a3f6606 100644 --- a/packages/flutter/lib/src/material/floating_action_button.dart +++ b/packages/flutter/lib/src/material/floating_action_button.dart @@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; import 'button.dart'; import 'scaffold.dart'; import 'theme.dart'; +import 'theme_data.dart'; import 'tooltip.dart'; const BoxConstraints _kSizeConstraints = const BoxConstraints.tightFor( @@ -67,6 +68,7 @@ class FloatingActionButton extends StatefulWidget { @required this.onPressed, this.mini = false, this.shape = const CircleBorder(), + this.materialTapTargetSize, this.isExtended = false, }) : assert(elevation != null), assert(highlightElevation != null), @@ -92,6 +94,7 @@ class FloatingActionButton extends StatefulWidget { @required this.onPressed, this.shape = const StadiumBorder(), this.isExtended = true, + this.materialTapTargetSize, @required Widget icon, @required Widget label, }) : assert(elevation != null), @@ -196,6 +199,15 @@ class FloatingActionButton extends StatefulWidget { /// floating action buttons are scaled and faded in. final bool isExtended; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + final BoxConstraints _sizeConstraints; @override @@ -241,7 +253,7 @@ class _FloatingActionButtonState extends State { onHighlightChanged: _handleHighlightChanged, elevation: _highlight ? widget.highlightElevation : widget.elevation, constraints: widget._sizeConstraints, - outerPadding: widget.mini ? const EdgeInsets.all(4.0) : null, + materialTapTargetSize: widget.materialTapTargetSize ?? theme.materialTapTargetSize, fillColor: widget.backgroundColor ?? theme.accentColor, textStyle: theme.accentTextTheme.button.copyWith( color: foregroundColor, diff --git a/packages/flutter/lib/src/material/input_border.dart b/packages/flutter/lib/src/material/input_border.dart index 79a5b3a7f2d..ea2155bb88f 100644 --- a/packages/flutter/lib/src/material/input_border.dart +++ b/packages/flutter/lib/src/material/input_border.dart @@ -31,7 +31,7 @@ abstract class InputBorder extends ShapeBorder { /// No input border. /// /// Use this value with [InputDecoration.border] to specify that no border - /// should be drawn. The [InputDecoration.collapsed] constructor sets + /// should be drawn. The [InputDecoration.shrinkWrap] constructor sets /// its border to this value. static const InputBorder none = const _NoInputBorder(); diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index c901bde4f8c..60f794b7d5a 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; import 'constants.dart'; import 'debug.dart'; import 'theme.dart'; +import 'theme_data.dart'; import 'toggleable.dart'; const double _kOuterRadius = 8.0; @@ -54,7 +55,8 @@ class Radio extends StatefulWidget { @required this.value, @required this.groupValue, @required this.onChanged, - this.activeColor + this.activeColor, + this.materialTapTargetSize, }) : super(key: key); /// The value represented by this radio button. @@ -96,6 +98,15 @@ class Radio extends StatefulWidget { /// Defaults to [ThemeData.toggleableActiveColor]. final Color activeColor; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + @override _RadioState createState() => new _RadioState(); } @@ -116,11 +127,22 @@ class _RadioState extends State> with TickerProviderStateMixin { Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData themeData = Theme.of(context); + Size size; + switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { + case MaterialTapTargetSize.padded: + size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0); + break; + case MaterialTapTargetSize.shrinkWrap: + size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius); + break; + } + final BoxConstraints additionalConstraints = new BoxConstraints.tight(size); return new _RadioRenderObjectWidget( selected: widget.value == widget.groupValue, activeColor: widget.activeColor ?? themeData.toggleableActiveColor, inactiveColor: _getInactiveColor(themeData), onChanged: _enabled ? _handleChanged : null, + additionalConstraints: additionalConstraints, vsync: this, ); } @@ -132,6 +154,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { @required this.selected, @required this.activeColor, @required this.inactiveColor, + @required this.additionalConstraints, this.onChanged, @required this.vsync, }) : assert(selected != null), @@ -145,6 +168,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { final Color activeColor; final ValueChanged onChanged; final TickerProvider vsync; + final BoxConstraints additionalConstraints; @override _RenderRadio createRenderObject(BuildContext context) => new _RenderRadio( @@ -153,6 +177,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { inactiveColor: inactiveColor, onChanged: onChanged, vsync: vsync, + additionalConstraints: additionalConstraints, ); @override @@ -162,6 +187,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ..activeColor = activeColor ..inactiveColor = inactiveColor ..onChanged = onChanged + ..additionalConstraints = additionalConstraints ..vsync = vsync; } } @@ -172,6 +198,7 @@ class _RenderRadio extends RenderToggleable { Color activeColor, Color inactiveColor, ValueChanged onChanged, + BoxConstraints additionalConstraints, @required TickerProvider vsync, }): super( value: value, @@ -179,7 +206,7 @@ class _RenderRadio extends RenderToggleable { activeColor: activeColor, inactiveColor: inactiveColor, onChanged: onChanged, - size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), + additionalConstraints: additionalConstraints, vsync: vsync, ); diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index 618c89020b8..698e46f25fb 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'list_tile.dart'; import 'radio.dart'; import 'theme.dart'; +import 'theme_data.dart'; /// A [ListTile] with a [Radio]. In other words, a radio button with a label. /// @@ -198,6 +199,7 @@ class RadioListTile extends StatelessWidget { groupValue: groupValue, onChanged: onChanged, activeColor: activeColor, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); Widget leading, trailing; switch (controlAffinity) { diff --git a/packages/flutter/lib/src/material/raised_button.dart b/packages/flutter/lib/src/material/raised_button.dart index df7dfd97dfe..f53c9915abb 100644 --- a/packages/flutter/lib/src/material/raised_button.dart +++ b/packages/flutter/lib/src/material/raised_button.dart @@ -10,6 +10,7 @@ import 'button_theme.dart'; import 'colors.dart'; import 'constants.dart'; import 'theme.dart'; +import 'theme_data.dart'; /// A material design "raised button". /// @@ -62,6 +63,7 @@ class RaisedButton extends StatelessWidget { this.disabledElevation = 0.0, this.padding, this.shape, + this.materialTapTargetSize, this.animationDuration = kThemeChangeDuration, this.child, }) : assert(elevation != null), @@ -94,6 +96,7 @@ class RaisedButton extends StatelessWidget { this.highlightElevation = 8.0, this.disabledElevation = 0.0, this.shape, + this.materialTapTargetSize, this.animationDuration = kThemeChangeDuration, @required Widget icon, @required Widget label, @@ -289,6 +292,15 @@ class RaisedButton extends StatelessWidget { /// The default value is [kThemeChangeDuration]. final Duration animationDuration; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + Brightness _getBrightness(ThemeData theme) { return colorBrightness ?? theme.brightness; } @@ -380,6 +392,7 @@ class RaisedButton extends StatelessWidget { shape: shape ?? buttonTheme.shape, animationDuration: animationDuration, child: child, + materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize, ); } diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index 2e997121ce2..3dda965d923 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -12,8 +12,17 @@ import 'constants.dart'; import 'debug.dart'; import 'shadows.dart'; import 'theme.dart'; +import 'theme_data.dart'; import 'toggleable.dart'; +const double _kTrackHeight = 14.0; +const double _kTrackWidth = 33.0; +const double _kTrackRadius = _kTrackHeight / 2.0; +const double _kThumbRadius = 10.0; +const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius; +const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0; +const double _kSwitchHeightCollapsed = 2 * kRadialReactionRadius; + /// A material design switch. /// /// Used to toggle the on/off state of a single setting. @@ -54,7 +63,8 @@ class Switch extends StatefulWidget { this.inactiveThumbColor, this.inactiveTrackColor, this.activeThumbImage, - this.inactiveThumbImage + this.inactiveThumbImage, + this.materialTapTargetSize, }) : super(key: key); /// Whether this switch is on or off. @@ -112,6 +122,15 @@ class Switch extends StatefulWidget { /// An image to use on the thumb of this switch when the switch is off. final ImageProvider inactiveThumbImage; + /// Configures the minimum size of the tap target. + /// + /// Defaults to [ThemeData.materialTapTargetSize]. + /// + /// See also: + /// + /// * [MaterialTapTargetSize], for a description of how this affects tap targets. + final MaterialTapTargetSize materialTapTargetSize; + @override _SwitchState createState() => new _SwitchState(); @@ -142,6 +161,16 @@ class _SwitchState extends State with TickerProviderStateMixin { inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400); inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12); } + Size size; + switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { + case MaterialTapTargetSize.padded: + size = const Size(_kSwitchWidth, _kSwitchHeight); + break; + case MaterialTapTargetSize.shrinkWrap: + size = const Size(_kSwitchWidth, _kSwitchHeightCollapsed); + break; + } + final BoxConstraints additionalConstraints = new BoxConstraints.tight(size); return new _SwitchRenderObjectWidget( value: widget.value, @@ -153,6 +182,7 @@ class _SwitchState extends State with TickerProviderStateMixin { inactiveTrackColor: inactiveTrackColor, configuration: createLocalImageConfiguration(context), onChanged: widget.onChanged, + additionalConstraints: additionalConstraints, vsync: this, ); } @@ -171,6 +201,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { this.configuration, this.onChanged, this.vsync, + this.additionalConstraints, }) : super(key: key); final bool value; @@ -183,6 +214,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { final ImageConfiguration configuration; final ValueChanged onChanged; final TickerProvider vsync; + final BoxConstraints additionalConstraints; @override _RenderSwitch createRenderObject(BuildContext context) { @@ -197,6 +229,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { configuration: configuration, onChanged: onChanged, textDirection: Directionality.of(context), + additionalConstraints: additionalConstraints, vsync: vsync, ); } @@ -214,17 +247,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ..configuration = configuration ..onChanged = onChanged ..textDirection = Directionality.of(context) + ..additionalConstraints = additionalConstraints ..vsync = vsync; } } -const double _kTrackHeight = 14.0; -const double _kTrackWidth = 33.0; -const double _kTrackRadius = _kTrackHeight / 2.0; -const double _kThumbRadius = 10.0; -const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius; -const double _kSwitchHeight = 2 * kRadialReactionRadius; - class _RenderSwitch extends RenderToggleable { _RenderSwitch({ bool value, @@ -235,6 +262,7 @@ class _RenderSwitch extends RenderToggleable { Color activeTrackColor, Color inactiveTrackColor, ImageConfiguration configuration, + BoxConstraints additionalConstraints, @required TextDirection textDirection, ValueChanged onChanged, @required TickerProvider vsync, @@ -251,7 +279,7 @@ class _RenderSwitch extends RenderToggleable { activeColor: activeColor, inactiveColor: inactiveColor, onChanged: onChanged, - size: const Size(_kSwitchWidth, _kSwitchHeight), + additionalConstraints: additionalConstraints, vsync: vsync, ) { _drag = new HorizontalDragGestureRecognizer() diff --git a/packages/flutter/lib/src/material/switch_list_tile.dart b/packages/flutter/lib/src/material/switch_list_tile.dart index b9231803adc..151234cba56 100644 --- a/packages/flutter/lib/src/material/switch_list_tile.dart +++ b/packages/flutter/lib/src/material/switch_list_tile.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'list_tile.dart'; import 'switch.dart'; import 'theme.dart'; +import 'theme_data.dart'; /// A [ListTile] with a [Switch]. In other words, a switch with a label. /// @@ -171,6 +172,7 @@ class SwitchListTile extends StatelessWidget { activeColor: activeColor, activeThumbImage: activeThumbImage, inactiveThumbImage: inactiveThumbImage, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); return new MergeSemantics( child: ListTileTheme.merge( diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 945dfe262af..bce84383003 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -36,6 +36,42 @@ const Color _kLightThemeSplashColor = const Color(0x66C8C8C8); const Color _kDarkThemeHighlightColor = const Color(0x40CCCCCC); const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC); +/// Configures the tap target and layout size of certain Material widgets. +/// +/// Changing the value in [ThemeData.materialTapTargetSize] will affect the +/// accessibility experience. +/// +/// Some of the impacted widgets include: +/// +/// * [FloatingActionButton], only the mini tap target size is increased. +/// * [MaterialButton] +/// * [OutlineButton] +/// * [FlatButton] +/// * [RaisedButton] +/// * [TimePicker] +/// * [SnackBar] +/// * [Chip] +/// * [RawChip] +/// * [InputChip] +/// * [ChoiceChip] +/// * [FilterChip] +/// * [ActionChip] +/// * [Radio] +/// * [Switch] +/// * [Checkbox] +enum MaterialTapTargetSize { + /// Expands the minimum tap target size to 48px by 48px. + /// + /// This is the default value of [ThemeData.materialHitTestSize] and the + /// recommended size to conform to Android accessibility scanner + /// recommendations. + padded, + + /// Shrinks the tap target size to the minimum provided by the Material + /// specification. + shrinkWrap, +} + /// Holds the color and typography values for a material design theme. /// /// Use this class to configure a [Theme] widget. @@ -105,7 +141,9 @@ class ThemeData extends Diagnosticable { SliderThemeData sliderTheme, ChipThemeData chipTheme, TargetPlatform platform, + MaterialTapTargetSize materialTapTargetSize, }) { + materialTapTargetSize ??= MaterialTapTargetSize.padded; brightness ??= Brightness.light; final bool isDark = brightness == Brightness.dark; primarySwatch ??= Colors.blue; @@ -208,6 +246,7 @@ class ThemeData extends Diagnosticable { sliderTheme: sliderTheme, chipTheme: chipTheme, platform: platform, + materialTapTargetSize: materialTapTargetSize, ); } @@ -257,6 +296,7 @@ class ThemeData extends Diagnosticable { @required this.sliderTheme, @required this.chipTheme, @required this.platform, + @required this.materialTapTargetSize, }) : assert(brightness != null), assert(primaryColor != null), assert(primaryColorBrightness != null), @@ -294,7 +334,8 @@ class ThemeData extends Diagnosticable { assert(accentIconTheme != null), assert(sliderTheme != null), assert(chipTheme != null), - assert(platform != null); + assert(platform != null), + assert(materialTapTargetSize != null); /// A default light blue theme. /// @@ -483,6 +524,9 @@ class ThemeData extends Diagnosticable { /// Defaults to the current platform. final TargetPlatform platform; + /// Configures the hit test size of certain Material widgets. + final MaterialTapTargetSize materialTapTargetSize; + /// Creates a copy of this theme but with the given fields replaced with the new values. ThemeData copyWith({ Brightness brightness, @@ -524,6 +568,7 @@ class ThemeData extends Diagnosticable { SliderThemeData sliderTheme, ChipThemeData chipTheme, TargetPlatform platform, + MaterialTapTargetSize materialTapTargetSize, }) { return new ThemeData.raw( brightness: brightness ?? this.brightness, @@ -565,6 +610,7 @@ class ThemeData extends Diagnosticable { sliderTheme: sliderTheme ?? this.sliderTheme, chipTheme: chipTheme ?? this.chipTheme, platform: platform ?? this.platform, + materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize, ); } @@ -692,6 +738,7 @@ class ThemeData extends Diagnosticable { sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t), chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t), platform: t < 0.5 ? a.platform : b.platform, + materialTapTargetSize: t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize, ); } @@ -736,7 +783,8 @@ class ThemeData extends Diagnosticable { (otherData.accentIconTheme == accentIconTheme) && (otherData.sliderTheme == sliderTheme) && (otherData.chipTheme == chipTheme) && - (otherData.platform == platform); + (otherData.platform == platform) && + (otherData.materialTapTargetSize == materialTapTargetSize); } @override @@ -780,6 +828,7 @@ class ThemeData extends Diagnosticable { sliderTheme, chipTheme, platform, + materialTapTargetSize ), ); } @@ -824,6 +873,7 @@ class ThemeData extends Diagnosticable { properties.add(new DiagnosticsProperty('accentIconTheme', accentIconTheme)); properties.add(new DiagnosticsProperty('sliderTheme', sliderTheme)); properties.add(new DiagnosticsProperty('chipTheme', chipTheme)); + properties.add(new DiagnosticsProperty('materialTapTargetSize', materialTapTargetSize)); } } diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 689299719d6..46cad861383 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -17,6 +17,7 @@ import 'feedback.dart'; import 'flat_button.dart'; import 'material_localizations.dart'; import 'theme.dart'; +import 'theme_data.dart'; import 'time.dart'; import 'typography.dart'; @@ -29,11 +30,15 @@ enum _TimePickerMode { hour, minute } const double _kTimePickerHeaderPortraitHeight = 96.0; const double _kTimePickerHeaderLandscapeWidth = 168.0; + const double _kTimePickerWidthPortrait = 328.0; const double _kTimePickerWidthLandscape = 512.0; -const double _kTimePickerHeightPortrait = 484.0; -const double _kTimePickerHeightLandscape = 304.0; +const double _kTimePickerHeightPortrait = 496.0; +const double _kTimePickerHeightLandscape = 316.0; + +const double _kTimePickerHeightPortraitCollapsed = 484.0; +const double _kTimePickerHeightLandscapeCollapsed = 304.0; /// The horizontal gap between the day period fragment and the fragment /// positioned next to it horizontally. @@ -1575,12 +1580,25 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ), ); + double timePickerHeightPortrait; + double timePickerHeightLandscape; + switch (theme.materialTapTargetSize) { + case MaterialTapTargetSize.padded: + timePickerHeightPortrait = _kTimePickerHeightPortrait; + timePickerHeightLandscape = _kTimePickerHeightLandscape; + break; + case MaterialTapTargetSize.shrinkWrap: + timePickerHeightPortrait = _kTimePickerHeightPortraitCollapsed; + timePickerHeightLandscape = _kTimePickerHeightLandscapeCollapsed; + break; + } + assert(orientation != null); switch (orientation) { case Orientation.portrait: return new SizedBox( width: _kTimePickerWidthPortrait, - height: _kTimePickerHeightPortrait, + height: timePickerHeightPortrait, child: new Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -1595,7 +1613,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { case Orientation.landscape: return new SizedBox( width: _kTimePickerWidthLandscape, - height: _kTimePickerHeightLandscape, + height: timePickerHeightLandscape, child: new Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/packages/flutter/lib/src/material/toggleable.dart b/packages/flutter/lib/src/material/toggleable.dart index c4c24882428..7e4d5310ada 100644 --- a/packages/flutter/lib/src/material/toggleable.dart +++ b/packages/flutter/lib/src/material/toggleable.dart @@ -26,10 +26,10 @@ abstract class RenderToggleable extends RenderConstrainedBox { RenderToggleable({ @required bool value, bool tristate = false, - Size size, @required Color activeColor, @required Color inactiveColor, ValueChanged onChanged, + BoxConstraints additionalConstraints, @required TickerProvider vsync, }) : assert(tristate != null), assert(tristate || value != null), @@ -42,7 +42,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { _inactiveColor = inactiveColor, _onChanged = onChanged, _vsync = vsync, - super(additionalConstraints: new BoxConstraints.tight(size)) { + super(additionalConstraints: additionalConstraints) { _tap = new TapGestureRecognizer() ..onTapDown = _handleTapDown ..onTap = _handleTap diff --git a/packages/flutter/test/material/buttons_test.dart b/packages/flutter/test/material/buttons_test.dart index 7c976cbeb3f..7378d81995c 100644 --- a/packages/flutter/test/material/buttons_test.dart +++ b/packages/flutter/test/material/buttons_test.dart @@ -39,8 +39,8 @@ void main() { SemanticsAction.tap, ], label: 'ABC', - rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), - transform: new Matrix4.translationValues(356.0, 282.0, 0.0), + rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + transform: new Matrix4.translationValues(356.0, 276.0, 0.0), flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, @@ -79,8 +79,8 @@ void main() { SemanticsAction.tap, ], label: 'ABC', - rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), - transform: new Matrix4.translationValues(356.0, 282.0, 0.0), + rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + transform: new Matrix4.translationValues(356.0, 276.0, 0.0), flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, @@ -113,7 +113,7 @@ void main() { ), ); - expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 36.0))); + expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 48.0))); expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0))); // textScaleFactor expands text, but not button. @@ -134,7 +134,7 @@ void main() { ), ); - expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 36.0))); + expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 48.0))); // Scaled text rendering is different on Linux and Mac by one pixel. // TODO(#12357): Update this test when text rendering is fixed. expect(tester.getSize(find.byType(Text)).width, isIn([54.0, 55.0])); @@ -162,7 +162,7 @@ void main() { // Scaled text rendering is different on Linux and Mac by one pixel. // TODO(#12357): Update this test when text rendering is fixed. expect(tester.getSize(find.byType(FlatButton)).width, isIn([158.0, 159.0])); - expect(tester.getSize(find.byType(FlatButton)).height, equals(42.0)); + expect(tester.getSize(find.byType(FlatButton)).height, equals(48.0)); expect(tester.getSize(find.byType(Text)).width, isIn([126.0, 127.0])); expect(tester.getSize(find.byType(Text)).height, equals(42.0)); }); @@ -187,7 +187,9 @@ void main() { new Directionality( textDirection: TextDirection.ltr, child: new Theme( - data: new ThemeData(), + data: new ThemeData( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), child: buttonWidget, ), ), @@ -233,6 +235,7 @@ void main() { data: new ThemeData( highlightColor: themeHighlightColor1, splashColor: themeSplashColor1, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: buttonWidget, ), @@ -260,6 +263,7 @@ void main() { data: new ThemeData( highlightColor: themeHighlightColor2, splashColor: themeSplashColor2, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: buttonWidget, // same widget, so does not get updated because of us ), @@ -279,7 +283,7 @@ void main() { testWidgets('Disabled MaterialButton has same semantic size as enabled and exposes disabled semantics', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); - final Rect expectedButtonSize = new Rect.fromLTRB(0.0, 0.0, 116.0, 36.0); + final Rect expectedButtonSize = new Rect.fromLTRB(0.0, 0.0, 116.0, 48.0); // Button is in center of screen final Matrix4 expectedButtonTransform = new Matrix4.identity() ..translate( @@ -354,4 +358,136 @@ void main() { semantics.dispose(); }); + + testWidgets('MaterialButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + final Key key1 = new UniqueKey(); + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new MaterialButton( + key: key1, + child: const SizedBox(width: 50.0, height: 8.0), + onPressed: () {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key1)), const Size(88.0, 48.0)); + + final Key key2 = new UniqueKey(); + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new MaterialButton( + key: key2, + child: const SizedBox(width: 50.0, height: 8.0), + onPressed: () {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0)); + }); + + testWidgets('FlatButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + final Key key1 = new UniqueKey(); + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new FlatButton( + key: key1, + child: const SizedBox(width: 50.0, height: 8.0), + onPressed: () {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key1)), const Size(88.0, 48.0)); + + final Key key2 = new UniqueKey(); + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new FlatButton( + key: key2, + child: const SizedBox(width: 50.0, height: 8.0), + onPressed: () {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0)); + }); + + testWidgets('RaisedButton size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + final Key key1 = new UniqueKey(); + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new RaisedButton( + key: key1, + child: const SizedBox(width: 50.0, height: 8.0), + onPressed: () {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key1)), const Size(88.0, 48.0)); + + final Key key2 = new UniqueKey(); + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new RaisedButton( + key: key2, + child: const SizedBox(width: 50.0, height: 8.0), + onPressed: () {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key2)), const Size(88.0, 36.0)); + }); } diff --git a/packages/flutter/test/material/checkbox_test.dart b/packages/flutter/test/material/checkbox_test.dart index 898f888112c..29f22e76c5e 100644 --- a/packages/flutter/test/material/checkbox_test.dart +++ b/packages/flutter/test/material/checkbox_test.dart @@ -16,13 +16,38 @@ void main() { debugResetSemanticsIdCounter(); }); - testWidgets('Checkbox size is 40x40', (WidgetTester tester) async { + testWidgets('Checkbox size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { await tester.pumpWidget( - new Material( - child: new Center( - child: new Checkbox( - value: false, - onChanged: (bool newValue) { }, + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Checkbox( + value: true, + onChanged: (bool newValue) {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byType(Checkbox)), const Size(48.0, 48.0)); + + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Checkbox( + value: true, + onChanged: (bool newValue) {}, + ), + ), ), ), ), diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 058888ab34c..f44e8b81c2a 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -4,11 +4,13 @@ import 'dart:ui' show window; +import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import '../rendering/mock_canvas.dart'; +import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; Finder findRenderChipElement() { @@ -249,7 +251,7 @@ void main() { ), ); expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0)); - expect(tester.getSize(find.byType(Chip)), const Size(64.0, 32.0)); + expect(tester.getSize(find.byType(Chip)), const Size(64.0,48.0)); await tester.pumpWidget( _wrapForChip( child: new Row( @@ -260,7 +262,7 @@ void main() { ), ); expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0)); - expect(tester.getSize(find.byType(Chip)), const Size(64.0, 32.0)); + expect(tester.getSize(find.byType(Chip)), const Size(64.0, 48.0)); await tester.pumpWidget( _wrapForChip( child: new Row( @@ -271,7 +273,7 @@ void main() { ), ); expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0)); - expect(tester.getSize(find.byType(Chip)), const Size(800.0, 32.0)); + expect(tester.getSize(find.byType(Chip)), const Size(800.0, 48.0)); }); testWidgets('Chip elements are ordered horizontally for locale', (WidgetTester tester) async { @@ -338,8 +340,8 @@ void main() { tester.getSize(find.text('Chip B')), anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)), ); - expect(tester.getSize(find.byType(Chip).first), anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0))); - expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0))); + expect(tester.getSize(find.byType(Chip).first), anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0))); + expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0))); await tester.pumpWidget( _wrapForChip( @@ -392,7 +394,7 @@ void main() { expect(tester.getSize(find.text('Chip B')), anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0))); expect(tester.getSize(find.byType(Chip).first).width, anyOf(318.0, 319.0)); expect(tester.getSize(find.byType(Chip).first).height, equals(50.0)); - expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0))); + expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0))); }); testWidgets('Labels can be non-text widgets', (WidgetTester tester) async { @@ -424,9 +426,9 @@ void main() { expect(tester.getSize(find.byKey(keyB)), const Size(10.0, 10.0)); expect( tester.getSize(find.byType(Chip).first), - anyOf(const Size(132.0, 32.0), const Size(131.0, 32.0)), + anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0)), ); - expect(tester.getSize(find.byType(Chip).last), const Size(58.0, 32.0)); + expect(tester.getSize(find.byType(Chip).last), const Size(58.0, 48.0)); }); testWidgets('Avatars can be non-circle avatar widgets', (WidgetTester tester) async { @@ -561,7 +563,7 @@ void main() { // No avatar await pushChip(); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0))); final GlobalKey avatarKey = new GlobalKey(); // Add an avatar @@ -574,10 +576,10 @@ void main() { ), ); // Avatar drawer should start out closed. - expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0))); expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0))); - expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(-20.0, 4.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(-20.0, 12.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); await tester.pump(const Duration(milliseconds: 20)); // Avatar drawer should start expanding. @@ -607,18 +609,18 @@ void main() { // Wait for being done with animation, and make sure it didn't change // height. await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0))); expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0))); - expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 4.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 9.0))); + expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 12.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 17.0))); // Remove the avatar again await pushChip(); // Avatar drawer should start out open. - expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0))); expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0))); - expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 4.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 9.0))); + expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 12.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 17.0))); await tester.pump(const Duration(milliseconds: 20)); // Avatar drawer should start contracting. @@ -648,8 +650,8 @@ void main() { // Wait for being done with animation, make sure it didn't change // height, and make sure that the avatar is no longer drawn. await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); expect(find.byKey(avatarKey), findsNothing); }); @@ -684,22 +686,22 @@ void main() { // No delete button await pushChip(); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0))); // Add a delete button await pushChip(deletable: true); // Delete button drawer should start out closed. - expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0))); expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0))); - expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(52.0, 4.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(52.0, 12.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); await tester.pump(const Duration(milliseconds: 20)); // Delete button drawer should start expanding. expect(tester.getSize(find.byType(RawChip)).width, closeTo(81.2, 0.1)); expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0))); expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(53.2, 0.1)); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); await tester.pump(const Duration(milliseconds: 20)); expect(tester.getSize(find.byType(RawChip)).width, closeTo(86.7, 0.1)); @@ -719,10 +721,10 @@ void main() { // Wait for being done with animation, and make sure it didn't change // height. await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0))); expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0))); - expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 4.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 12.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); // Test the tap work for the delete button, but not the rest of the chip. expect(wasDeleted, isFalse); @@ -734,17 +736,17 @@ void main() { // Remove the delete button again await pushChip(); // Delete button drawer should start out open. - expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0))); expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0))); - expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 4.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 12.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); await tester.pump(const Duration(milliseconds: 20)); // Delete button drawer should start contracting. expect(tester.getSize(find.byType(RawChip)).width, closeTo(103.8, 0.1)); expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0))); expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, closeTo(75.8, 0.1)); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); await tester.pump(const Duration(milliseconds: 20)); expect(tester.getSize(find.byType(RawChip)).width, closeTo(102.9, 0.1)); @@ -764,8 +766,8 @@ void main() { // Wait for being done with animation, make sure it didn't change // height, and make sure that the delete button is no longer drawn. await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0))); - expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 9.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0))); + expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0))); expect(find.byKey(deleteButtonKey), findsNothing); }); @@ -806,7 +808,7 @@ void main() { await pushChip( avatar: new Container(width: 40.0, height: 40.0, key: avatarKey), ); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0))); // Turn on selection. await pushChip( @@ -885,7 +887,7 @@ void main() { // Without avatar, but not selectable. await pushChip(); - expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 32.0))); + expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0))); // Turn on selection. await pushChip(selectable: true); @@ -1017,6 +1019,42 @@ void main() { expect(materialBox, paints..path(color: chipTheme.disabledColor)); }); + testWidgets('Chip size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + final Key key1 = new UniqueKey(); + await tester.pumpWidget( + _wrapForChip( + child: new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Center( + child: new RawChip( + key: key1, + label: const Text('test'), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key1)), const Size(80.0, 48.0)); + + final Key key2 = new UniqueKey(); + await tester.pumpWidget( + _wrapForChip( + child: new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Center( + child: new RawChip( + key: key2, + label: const Text('test'), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key2)), const Size(80.0, 32.0)); + }); + testWidgets('Chip uses the right theme colors for the right components', (WidgetTester tester) async { final ThemeData themeData = new ThemeData( platform: TargetPlatform.android, @@ -1138,4 +1176,255 @@ void main() { expect(materialBox, paints..path(color: customTheme.disabledColor)); expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); }); + + group('Chip semantics', () { + testWidgets('label only', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new MaterialApp( + home: const Material( + child: const RawChip( + label: const Text('test'), + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics( + textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + label: 'test', + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + ], + ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); + semanticsTester.dispose(); + }); + + testWidgets('with onPressed', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new RawChip( + label: const Text('test'), + onPressed: () {}, + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics( + textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + label: 'test', + textDirection: TextDirection.ltr, + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], + actions: [SemanticsAction.tap], + ), + ], + ), + ], + ), + ], + ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); + + semanticsTester.dispose(); + }); + + + testWidgets('with onSelected', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + bool selected = false; + + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new RawChip( + isEnabled: true, + label: const Text('test'), + selected: selected, + onSelected: (bool value) { + selected = value; + }, + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics( + textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + label: 'test', + textDirection: TextDirection.ltr, + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], + actions: [SemanticsAction.tap], + ), + ], + ), + ], + ), + ], + ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); + + await tester.tap(find.byType(RawChip)); + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new RawChip( + isEnabled: true, + label: const Text('test'), + selected: selected, + onSelected: (bool value) { + selected = value; + }, + ), + ), + )); + + expect(selected, true); + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics( + textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + label: 'test', + textDirection: TextDirection.ltr, + flags: [ + SemanticsFlag.isSelected, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], + actions: [SemanticsAction.tap], + ), + ], + ), + ], + ), + ], + ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); + + semanticsTester.dispose(); + }); + + testWidgets('disabled', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new RawChip( + isEnabled: false, + onPressed: () {}, + label: const Text('test'), + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics( + textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + label: 'test', + textDirection: TextDirection.ltr, + flags: [], + actions: [], + ), + ], + ), + ], + ), + ], + ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); + + semanticsTester.dispose(); + }); + }); + + testWidgets('can be tapped outside of chip delete icon', (WidgetTester tester) async { + bool deleted = false; + await tester.pumpWidget( + _wrapForChip( + child: new Row( + children: [ + new Chip( + materialTapTargetSize: MaterialTapTargetSize.padded, + shape: const RoundedRectangleBorder(borderRadius: const BorderRadius.all(const Radius.circular(0.0))), + avatar: const CircleAvatar(child: const Text('A')), + label: const Text('Chip A'), + onDeleted: () { + deleted = true; + }, + deleteIcon: const Icon(Icons.delete), + ), + ], + ), + ), + ); + + await tester.tapAt(tester.getTopRight(find.byType(Chip)) - const Offset(2.0, -2.0)); + await tester.pumpAndSettle(); + expect(deleted, true); + }); + + testWidgets('can be tapped outside of chip body', (WidgetTester tester) async { + bool pressed = false; + await tester.pumpWidget( + _wrapForChip( + child: new Row( + children: [ + new InputChip( + materialTapTargetSize: MaterialTapTargetSize.padded, + shape: const RoundedRectangleBorder(borderRadius: const BorderRadius.all(const Radius.circular(0.0))), + avatar: const CircleAvatar(child: const Text('A')), + label: const Text('Chip A'), + onPressed: () { + pressed = true; + }, + ), + ], + ), + ), + ); + + await tester.tapAt(tester.getRect(find.byType(InputChip)).topCenter); + await tester.pumpAndSettle(); + expect(pressed, true); + }); } diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index df98c447b46..1a20808fbe4 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -67,6 +67,44 @@ void main() { expect(find.byType(Text), findsOneWidget); }); + testWidgets('FlatActionButton mini size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + final Key key1 = new UniqueKey(); + await tester.pumpWidget( + new MaterialApp( + home: new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Scaffold( + floatingActionButton: new FloatingActionButton( + key: key1, + mini: true, + onPressed: null, + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key1)), const Size(48.0, 48.0)); + + final Key key2 = new UniqueKey(); + await tester.pumpWidget( + new MaterialApp( + home: new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Scaffold( + floatingActionButton: new FloatingActionButton( + key: key2, + mini: true, + onPressed: null, + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0)); + }); + testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async { await tester.pumpWidget( new MaterialApp( diff --git a/packages/flutter/test/material/outline_button_test.dart b/packages/flutter/test/material/outline_button_test.dart index b00198401bd..8435cbad60e 100644 --- a/packages/flutter/test/material/outline_button_test.dart +++ b/packages/flutter/test/material/outline_button_test.dart @@ -55,7 +55,7 @@ void main() { new Directionality( textDirection: TextDirection.ltr, child: new Theme( - data: new ThemeData(), + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), child: new Container( alignment: Alignment.topLeft, child: new OutlineButton( @@ -140,8 +140,8 @@ void main() { SemanticsAction.tap, ], label: 'ABC', - rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), - transform: new Matrix4.translationValues(356.0, 282.0, 0.0), + rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + transform: new Matrix4.translationValues(356.0, 276.0, 0.0), flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, @@ -175,7 +175,7 @@ void main() { ), ); - expect(tester.getSize(find.byType(OutlineButton)), equals(const Size(88.0, 36.0))); + expect(tester.getSize(find.byType(OutlineButton)), equals(const Size(88.0, 48.0))); expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0))); // textScaleFactor expands text, but not button. @@ -196,7 +196,7 @@ void main() { ), ); - expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 36.0))); + expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 48.0))); // Scaled text rendering is different on Linux and Mac by one pixel. // TODO(#12357): Update this test when text rendering is fixed. expect(tester.getSize(find.byType(Text)).width, isIn([54.0, 55.0])); @@ -224,7 +224,7 @@ void main() { // Scaled text rendering is different on Linux and Mac by one pixel. // TODO(#12357): Update this test when text rendering is fixed. expect(tester.getSize(find.byType(FlatButton)).width, isIn([158.0, 159.0])); - expect(tester.getSize(find.byType(FlatButton)).height, equals(42.0)); + expect(tester.getSize(find.byType(FlatButton)).height, equals(48.0)); expect(tester.getSize(find.byType(Text)).width, isIn([126.0, 127.0])); expect(tester.getSize(find.byType(Text)).height, equals(42.0)); }); diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart index 811c11e5b29..5fd0196c3fc 100644 --- a/packages/flutter/test/material/radio_test.dart +++ b/packages/flutter/test/material/radio_test.dart @@ -64,23 +64,50 @@ void main() { expect(log, isEmpty); }); - testWidgets('Radio size is 40x40', (WidgetTester tester) async { - final Key key = new UniqueKey(); - + testWidgets('Radio size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + final Key key1 = new UniqueKey(); await tester.pumpWidget( - new Material( - child: new Center( - child: new Radio( - key: key, - value: 1, - groupValue: 2, - onChanged: (int newValue) { }, + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Radio( + key: key1, + groupValue: true, + value: true, + onChanged: (bool newValue) {}, + ), + ), ), ), ), ); - expect(tester.getSize(find.byKey(key)), const Size(40.0, 40.0)); + expect(tester.getSize(find.byKey(key1)), const Size(48.0, 48.0)); + + final Key key2 = new UniqueKey(); + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Radio( + key: key2, + groupValue: true, + value: true, + onChanged: (bool newValue) {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0)); }); diff --git a/packages/flutter/test/material/raw_material_button_test.dart b/packages/flutter/test/material/raw_material_button_test.dart index 16e8177537f..20654edc804 100644 --- a/packages/flutter/test/material/raw_material_button_test.dart +++ b/packages/flutter/test/material/raw_material_button_test.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { - testWidgets('outerPadding expands hit test area', (WidgetTester tester) async { + testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async { int pressed = 0; await tester.pumpWidget(new RawMaterialButton( @@ -13,23 +14,23 @@ void main() { pressed++; }, constraints: new BoxConstraints.tight(const Size(10.0, 10.0)), - outerPadding: const EdgeInsets.all(50.0), + materialTapTargetSize: MaterialTapTargetSize.padded, child: const Text('+', textDirection: TextDirection.ltr), )); - await tester.tapAt(const Offset(100.0, 100.0)); + await tester.tapAt(const Offset(40.0, 400.0)); expect(pressed, 1); }); - testWidgets('outerPadding expands semantics area', (WidgetTester tester) async { + testWidgets('materialTapTargetSize.padded expands semantics area', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); await tester.pumpWidget( new Center( child: new RawMaterialButton( onPressed: () {}, constraints: new BoxConstraints.tight(const Size(10.0, 10.0)), - outerPadding: const EdgeInsets.all(50.0), + materialTapTargetSize: MaterialTapTargetSize.padded, child: const Text('+', textDirection: TextDirection.ltr), ), ), @@ -50,7 +51,7 @@ void main() { ], label: '+', textDirection: TextDirection.ltr, - rect: Rect.fromLTRB(0.0, 0.0, 110.0, 110.0), + rect: Rect.fromLTRB(0.0, 0.0, 48.0, 48.0), children: [], ), ] @@ -58,4 +59,57 @@ void main() { semantics.dispose(); }); + + testWidgets('Ink splash from center tap originates in correct location', (WidgetTester tester) async { + const Color highlightColor = const Color(0xAAFF0000); + const Color splashColor = const Color(0xAA0000FF); + const Color fillColor = const Color(0xFFEF5350); + + await tester.pumpWidget( + new RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.padded, + onPressed: () {}, + fillColor: fillColor, + highlightColor: highlightColor, + splashColor: splashColor, + child: const SizedBox(), + ) + ); + + final Offset center = tester.getCenter(find.byType(InkWell)); + final TestGesture gesture = await tester.startGesture(center); + await tester.pump(); // start gesture + await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way + + final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic; + // centered in material button. + expect(box, paints..circle(x: 44.0, y: 18.0, color: splashColor)); + await gesture.up(); + }); + + testWidgets('Ink splash from tap above material originates in correct location', (WidgetTester tester) async { + const Color highlightColor = const Color(0xAAFF0000); + const Color splashColor = const Color(0xAA0000FF); + const Color fillColor = const Color(0xFFEF5350); + + await tester.pumpWidget( + new RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.padded, + onPressed: () {}, + fillColor: fillColor, + highlightColor: highlightColor, + splashColor: splashColor, + child: const SizedBox(), + ) + ); + + final Offset top = tester.getRect(find.byType(InkWell)).topCenter; + final TestGesture gesture = await tester.startGesture(top); + await tester.pump(); // start gesture + await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way + final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic; + // paints above above material + expect(box, paints..circle(x: 44.0, y: 0.0, color: splashColor)); + await gesture.up(); + }); } \ No newline at end of file diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 9d0d79c89ab..36c250869af 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -542,7 +542,7 @@ void main() { ), ), )); - expect(tester.element(find.byKey(testKey)).size, const Size(88.0, 36.0)); + expect(tester.element(find.byKey(testKey)).size, const Size(88.0, 48.0)); expect(tester.renderObject(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); }); }); diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index da3dd439497..86458b52992 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -341,10 +341,10 @@ void main() { final Offset snackBarBottomRight = snackBarBox.localToGlobal(snackBarBox.size.bottomRight(Offset.zero)); expect(textBottomLeft.dx - snackBarBottomLeft.dx, 24.0 + 10.0); // margin + left padding - expect(snackBarBottomLeft.dy - textBottomLeft.dy, 14.0 + 40.0); // margin + bottom padding + expect(snackBarBottomLeft.dy - textBottomLeft.dy, 17.0 + 40.0); // margin + bottom padding expect(actionTextBottomLeft.dx - textBottomRight.dx, 24.0); expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 24.0 + 30.0); // margin + right padding - expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 14.0 + 40.0); // margin + bottom padding + expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 17.0 + 40.0); // margin + bottom padding }); testWidgets('SnackBar is positioned above BottomNavigationBar', (WidgetTester tester) async { @@ -398,10 +398,10 @@ void main() { final Offset snackBarBottomRight = snackBarBox.localToGlobal(snackBarBox.size.bottomRight(Offset.zero)); expect(textBottomLeft.dx - snackBarBottomLeft.dx, 24.0 + 10.0); // margin + left padding - expect(snackBarBottomLeft.dy - textBottomLeft.dy, 14.0); // margin (with no bottom padding) + expect(snackBarBottomLeft.dy - textBottomLeft.dy, 17.0); // margin (with no bottom padding) expect(actionTextBottomLeft.dx - textBottomRight.dx, 24.0); expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 24.0 + 30.0); // margin + right padding - expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 14.0); // margin (with no bottom padding) + expect(snackBarBottomRight.dy - actionTextBottomRight.dy, 17.0); // margin (with no bottom padding) }); testWidgets('SnackBarClosedReason', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart index 48a72949e04..c0e18417c6e 100644 --- a/packages/flutter/test/material/switch_test.dart +++ b/packages/flutter/test/material/switch_test.dart @@ -43,6 +43,46 @@ void main() { expect(value, isTrue); }); + testWidgets('Switch size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Switch( + value: true, + onChanged: (bool newValue) {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byType(Switch)), const Size(59.0, 48.0)); + + await tester.pumpWidget( + new Theme( + data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Switch( + value: true, + onChanged: (bool newValue) {}, + ), + ), + ), + ), + ), + ); + + expect(tester.getSize(find.byType(Switch)), const Size(59.0, 40.0)); + }); + testWidgets('Switch can drag (LTR)', (WidgetTester tester) async { bool value = false; diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index 37d05e1e952..6849de8f4d9 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -103,6 +103,12 @@ void main() { expect(darkTheme.accentTextTheme.title.color, typography.white.title.color); }); + test('Defaults to MaterialTapTargetBehavior.expanded', () { + final ThemeData themeData = new ThemeData(); + + expect(themeData.materialTapTargetSize, MaterialTapTargetSize.padded); + }); + test('Can control fontFamily', () { final ThemeData themeData = new ThemeData(fontFamily: 'Ahem');