Unverified Commit 989f5741 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add flag to ThemeData to expand tap targets of certain material widgets (#18369)

parent 6172e23c
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button_theme.dart'; import 'button_theme.dart';
...@@ -11,6 +12,7 @@ import 'constants.dart'; ...@@ -11,6 +12,7 @@ import 'constants.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
/// Creates a button based on [Semantics], [Material], and [InkWell] /// Creates a button based on [Semantics], [Material], and [InkWell]
/// widgets. /// widgets.
...@@ -38,13 +40,14 @@ class RawMaterialButton extends StatefulWidget { ...@@ -38,13 +40,14 @@ class RawMaterialButton extends StatefulWidget {
this.elevation = 2.0, this.elevation = 2.0,
this.highlightElevation = 8.0, this.highlightElevation = 8.0,
this.disabledElevation = 0.0, this.disabledElevation = 0.0,
this.outerPadding,
this.padding = EdgeInsets.zero, this.padding = EdgeInsets.zero,
this.constraints = const BoxConstraints(minWidth: 88.0, minHeight: 36.0), this.constraints = const BoxConstraints(minWidth: 88.0, minHeight: 36.0),
this.shape = const RoundedRectangleBorder(), this.shape = const RoundedRectangleBorder(),
this.animationDuration = kThemeChangeDuration, this.animationDuration = kThemeChangeDuration,
MaterialTapTargetSize materialTapTargetSize,
this.child, this.child,
}) : assert(shape != null), }) : this.materialTapTargetSize = materialTapTargetSize ?? MaterialTapTargetSize.padded,
assert(shape != null),
assert(elevation != null), assert(elevation != null),
assert(highlightElevation != null), assert(highlightElevation != null),
assert(disabledElevation != null), assert(disabledElevation != null),
...@@ -58,10 +61,6 @@ class RawMaterialButton extends StatefulWidget { ...@@ -58,10 +61,6 @@ class RawMaterialButton extends StatefulWidget {
/// If this is set to null, the button will be disabled, see [enabled]. /// If this is set to null, the button will be disabled, see [enabled].
final VoidCallback onPressed; 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] /// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged]
/// callback. /// callback.
final ValueChanged<bool> onHighlightChanged; final ValueChanged<bool> onHighlightChanged;
...@@ -138,6 +137,15 @@ class RawMaterialButton extends StatefulWidget { ...@@ -138,6 +137,15 @@ class RawMaterialButton extends StatefulWidget {
/// property to a non-null value. /// property to a non-null value.
bool get enabled => onPressed != null; 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 @override
_RawMaterialButtonState createState() => new _RawMaterialButtonState(); _RawMaterialButtonState createState() => new _RawMaterialButtonState();
} }
...@@ -186,18 +194,23 @@ class _RawMaterialButtonState extends State<RawMaterialButton> { ...@@ -186,18 +194,23 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
), ),
), ),
); );
BoxConstraints constraints;
if (widget.outerPadding != null) { switch (widget.materialTapTargetSize) {
result = new GestureDetector( case MaterialTapTargetSize.padded:
behavior: HitTestBehavior.translucent, constraints = const BoxConstraints(minWidth: 48.0, minHeight: 48.0);
excludeFromSemantics: true, break;
onTap: widget.onPressed, case MaterialTapTargetSize.shrinkWrap:
child: new Padding( constraints = const BoxConstraints();
padding: widget.outerPadding, break;
child: result
),
);
} }
result = new _ButtonRedirectingHitDetectionWidget(
constraints: constraints,
child: new Center(
child: result,
widthFactor: 1.0,
heightFactor: 1.0,
),
);
return new Semantics( return new Semantics(
container: true, container: true,
...@@ -248,6 +261,7 @@ class MaterialButton extends StatelessWidget { ...@@ -248,6 +261,7 @@ class MaterialButton extends StatelessWidget {
this.minWidth, this.minWidth,
this.height, this.height,
this.padding, this.padding,
this.materialTapTargetSize,
@required this.onPressed, @required this.onPressed,
this.child this.child
}) : super(key: key); }) : super(key: key);
...@@ -353,6 +367,15 @@ class MaterialButton extends StatelessWidget { ...@@ -353,6 +367,15 @@ class MaterialButton extends StatelessWidget {
/// {@macro flutter.widgets.child} /// {@macro flutter.widgets.child}
final Widget 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 /// 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. /// enable a button, set its [onPressed] property to a non-null value.
bool get enabled => onPressed != null; bool get enabled => onPressed != null;
...@@ -412,6 +435,7 @@ class MaterialButton extends StatelessWidget { ...@@ -412,6 +435,7 @@ class MaterialButton extends StatelessWidget {
), ),
shape: buttonTheme.shape, shape: buttonTheme.shape,
child: child, child: child,
materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
); );
} }
...@@ -421,3 +445,38 @@ class MaterialButton extends StatelessWidget { ...@@ -421,3 +445,38 @@ class MaterialButton extends StatelessWidget {
properties.add(new FlagProperty('enabled', value: enabled, ifFalse: 'disabled')); 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));
}
}
...@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
import 'toggleable.dart'; import 'toggleable.dart';
/// A material design checkbox. /// A material design checkbox.
...@@ -58,6 +59,7 @@ class Checkbox extends StatefulWidget { ...@@ -58,6 +59,7 @@ class Checkbox extends StatefulWidget {
this.tristate = false, this.tristate = false,
@required this.onChanged, @required this.onChanged,
this.activeColor, this.activeColor,
this.materialTapTargetSize,
}) : assert(tristate != null), }) : assert(tristate != null),
assert(tristate || value != null), assert(tristate || value != null),
super(key: key); super(key: key);
...@@ -113,6 +115,15 @@ class Checkbox extends StatefulWidget { ...@@ -113,6 +115,15 @@ class Checkbox extends StatefulWidget {
/// If tristate is false (the default), [value] must not be null. /// If tristate is false (the default), [value] must not be null.
final bool tristate; 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. /// The width of a checkbox widget.
static const double width = 18.0; static const double width = 18.0;
...@@ -125,12 +136,23 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -125,12 +136,23 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(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( return new _CheckboxRenderObjectWidget(
value: widget.value, value: widget.value,
tristate: widget.tristate, tristate: widget.tristate,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor, activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor, inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
onChanged: widget.onChanged, onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this, vsync: this,
); );
} }
...@@ -145,6 +167,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -145,6 +167,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
@required this.inactiveColor, @required this.inactiveColor,
@required this.onChanged, @required this.onChanged,
@required this.vsync, @required this.vsync,
@required this.additionalConstraints,
}) : assert(tristate != null), }) : assert(tristate != null),
assert(tristate || value != null), assert(tristate || value != null),
assert(activeColor != null), assert(activeColor != null),
...@@ -158,6 +181,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -158,6 +181,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
final Color inactiveColor; final Color inactiveColor;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final TickerProvider vsync; final TickerProvider vsync;
final BoxConstraints additionalConstraints;
@override @override
_RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox( _RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox(
...@@ -167,6 +191,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -167,6 +191,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
vsync: vsync, vsync: vsync,
additionalConstraints: additionalConstraints,
); );
@override @override
...@@ -177,6 +202,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -177,6 +202,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
..activeColor = activeColor ..activeColor = activeColor
..inactiveColor = inactiveColor ..inactiveColor = inactiveColor
..onChanged = onChanged ..onChanged = onChanged
..additionalConstraints = additionalConstraints
..vsync = vsync; ..vsync = vsync;
} }
} }
...@@ -191,6 +217,7 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -191,6 +217,7 @@ class _RenderCheckbox extends RenderToggleable {
bool tristate, bool tristate,
Color activeColor, Color activeColor,
Color inactiveColor, Color inactiveColor,
BoxConstraints additionalConstraints,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
@required TickerProvider vsync, @required TickerProvider vsync,
}): _oldValue = value, }): _oldValue = value,
...@@ -200,7 +227,7 @@ class _RenderCheckbox extends RenderToggleable { ...@@ -200,7 +227,7 @@ class _RenderCheckbox extends RenderToggleable {
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), additionalConstraints: additionalConstraints,
vsync: vsync, vsync: vsync,
); );
......
...@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'checkbox.dart'; import 'checkbox.dart';
import 'list_tile.dart'; import 'list_tile.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
/// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label. /// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label.
/// ///
...@@ -173,6 +174,7 @@ class CheckboxListTile extends StatelessWidget { ...@@ -173,6 +174,7 @@ class CheckboxListTile extends StatelessWidget {
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
activeColor: activeColor, activeColor: activeColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
); );
Widget leading, trailing; Widget leading, trailing;
switch (controlAffinity) { switch (controlAffinity) {
......
...@@ -17,6 +17,7 @@ import 'ink_well.dart'; ...@@ -17,6 +17,7 @@ import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
import 'tooltip.dart'; import 'tooltip.dart';
// Some design constants // Some design constants
...@@ -99,6 +100,15 @@ abstract class ChipAttributes { ...@@ -99,6 +100,15 @@ abstract class ChipAttributes {
/// By default, this is 4 logical pixels at the beginning and the end of the /// By default, this is 4 logical pixels at the beginning and the end of the
/// label, and zero on top and bottom. /// label, and zero on top and bottom.
EdgeInsetsGeometry get labelPadding; 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. /// An interface for material design chips that can be deleted.
...@@ -423,6 +433,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri ...@@ -423,6 +433,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
this.shape, this.shape,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
this.materialTapTargetSize,
}) : assert(label != null), }) : assert(label != null),
super(key: key); super(key: key);
...@@ -448,6 +459,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri ...@@ -448,6 +459,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
final Color deleteIconColor; final Color deleteIconColor;
@override @override
final String deleteButtonTooltipMessage; final String deleteButtonTooltipMessage;
@override
final MaterialTapTargetSize materialTapTargetSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -465,6 +478,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri ...@@ -465,6 +478,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
shape: shape, shape: shape,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
padding: padding, padding: padding,
materialTapTargetSize: materialTapTargetSize,
isEnabled: true, isEnabled: true,
); );
} }
...@@ -547,6 +561,7 @@ class InputChip extends StatelessWidget ...@@ -547,6 +561,7 @@ class InputChip extends StatelessWidget
this.shape, this.shape,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
this.materialTapTargetSize,
}) : assert(selected != null), }) : assert(selected != null),
assert(isEnabled != null), assert(isEnabled != null),
assert(label != null), assert(label != null),
...@@ -588,6 +603,8 @@ class InputChip extends StatelessWidget ...@@ -588,6 +603,8 @@ class InputChip extends StatelessWidget
final Color backgroundColor; final Color backgroundColor;
@override @override
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
@override
final MaterialTapTargetSize materialTapTargetSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -611,6 +628,7 @@ class InputChip extends StatelessWidget ...@@ -611,6 +628,7 @@ class InputChip extends StatelessWidget
shape: shape, shape: shape,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
padding: padding, padding: padding,
materialTapTargetSize: materialTapTargetSize,
isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null), isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null),
); );
} }
...@@ -689,6 +707,7 @@ class ChoiceChip extends StatelessWidget ...@@ -689,6 +707,7 @@ class ChoiceChip extends StatelessWidget
this.shape, this.shape,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
this.materialTapTargetSize,
}) : assert(selected != null), }) : assert(selected != null),
assert(label != null), assert(label != null),
super(key: key); super(key: key);
...@@ -717,6 +736,8 @@ class ChoiceChip extends StatelessWidget ...@@ -717,6 +736,8 @@ class ChoiceChip extends StatelessWidget
final Color backgroundColor; final Color backgroundColor;
@override @override
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
@override
final MaterialTapTargetSize materialTapTargetSize;
@override @override
bool get isEnabled => onSelected != null; bool get isEnabled => onSelected != null;
...@@ -741,6 +762,7 @@ class ChoiceChip extends StatelessWidget ...@@ -741,6 +762,7 @@ class ChoiceChip extends StatelessWidget
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
padding: padding, padding: padding,
isEnabled: isEnabled, isEnabled: isEnabled,
materialTapTargetSize: materialTapTargetSize,
); );
} }
} }
...@@ -852,6 +874,7 @@ class FilterChip extends StatelessWidget ...@@ -852,6 +874,7 @@ class FilterChip extends StatelessWidget
this.shape, this.shape,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
this.materialTapTargetSize,
}) : assert(selected != null), }) : assert(selected != null),
assert(label != null), assert(label != null),
super(key: key); super(key: key);
...@@ -880,6 +903,8 @@ class FilterChip extends StatelessWidget ...@@ -880,6 +903,8 @@ class FilterChip extends StatelessWidget
final Color backgroundColor; final Color backgroundColor;
@override @override
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
@override
final MaterialTapTargetSize materialTapTargetSize;
@override @override
bool get isEnabled => onSelected != null; bool get isEnabled => onSelected != null;
...@@ -901,6 +926,7 @@ class FilterChip extends StatelessWidget ...@@ -901,6 +926,7 @@ class FilterChip extends StatelessWidget
selectedColor: selectedColor, selectedColor: selectedColor,
padding: padding, padding: padding,
isEnabled: isEnabled, isEnabled: isEnabled,
materialTapTargetSize: materialTapTargetSize,
); );
} }
} }
...@@ -966,6 +992,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip ...@@ -966,6 +992,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
this.shape, this.shape,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
this.materialTapTargetSize,
}) : assert(label != null), }) : assert(label != null),
assert( assert(
onPressed != null, onPressed != null,
...@@ -992,6 +1019,8 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip ...@@ -992,6 +1019,8 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
final Color backgroundColor; final Color backgroundColor;
@override @override
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
@override
final MaterialTapTargetSize materialTapTargetSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -1007,6 +1036,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip ...@@ -1007,6 +1036,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
padding: padding, padding: padding,
labelPadding: labelPadding, labelPadding: labelPadding,
isEnabled: true, isEnabled: true,
materialTapTargetSize: materialTapTargetSize
); );
} }
} }
...@@ -1076,6 +1106,7 @@ class RawChip extends StatefulWidget ...@@ -1076,6 +1106,7 @@ class RawChip extends StatefulWidget
this.tooltip, this.tooltip,
this.shape, this.shape,
this.backgroundColor, this.backgroundColor,
this.materialTapTargetSize,
}) : assert(label != null), }) : assert(label != null),
assert(isEnabled != null), assert(isEnabled != null),
deleteIcon = deleteIcon ?? _kDefaultDeleteIcon, deleteIcon = deleteIcon ?? _kDefaultDeleteIcon,
...@@ -1117,6 +1148,8 @@ class RawChip extends StatefulWidget ...@@ -1117,6 +1148,8 @@ class RawChip extends StatefulWidget
final Color backgroundColor; final Color backgroundColor;
@override @override
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
@override
final MaterialTapTargetSize materialTapTargetSize;
/// Whether or not to show a check mark when [selected] is true. /// Whether or not to show a check mark when [selected] is true.
/// ///
...@@ -1368,7 +1401,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1368,7 +1401,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
final TextDirection textDirection = Directionality.of(context); final TextDirection textDirection = Directionality.of(context);
final ShapeBorder shape = widget.shape ?? chipTheme.shape; final ShapeBorder shape = widget.shape ?? chipTheme.shape;
return new Material( Widget result = new Material(
elevation: isTapping ? _kPressElevation : 0.0, elevation: isTapping ? _kPressElevation : 0.0,
animationDuration: pressedAnimationDuration, animationDuration: pressedAnimationDuration,
shape: shape, shape: shape,
...@@ -1428,6 +1461,68 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1428,6 +1461,68 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
), ),
), ),
); );
BoxConstraints constraints;
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
case MaterialTapTargetSize.padded:
constraints = const BoxConstraints(minHeight: 48.0);
break;
case MaterialTapTargetSize.shrinkWrap:
constraints = const BoxConstraints();
break;
}
result = _ChipRedirectingHitDetectionWidget(
constraints: constraints,
child: new Center(
child: result,
widthFactor: 1.0,
heightFactor: 1.0,
),
);
return new Semantics(
container: true,
selected: widget.selected,
enabled: canTap ? widget.isEnabled : null,
child: result,
);
}
}
/// Redirects the [position.dy] passed to [RenderBox.hitTest] to the vertical
/// center of the widget.
///
/// The primary purpose of this widget is to allow padding around the [RawChip]
/// to trigger the child ink feature without increasing the size of the material.
class _ChipRedirectingHitDetectionWidget extends SingleChildRenderObjectWidget {
const _ChipRedirectingHitDetectionWidget({
Key key,
Widget child,
this.constraints,
}) : super(key: key, child: child);
final BoxConstraints constraints;
@override
RenderObject createRenderObject(BuildContext context) {
return new _RenderChipRedirectingHitDetection(constraints);
}
@override
void updateRenderObject(BuildContext context, covariant _RenderChipRedirectingHitDetection renderObject) {
renderObject.additionalConstraints = constraints;
}
}
class _RenderChipRedirectingHitDetection extends RenderConstrainedBox {
_RenderChipRedirectingHitDetection(BoxConstraints additionalConstraints) : super(additionalConstraints: additionalConstraints);
@override
bool hitTest(HitTestResult result, {Offset position}) {
if (!size.contains(position))
return false;
// Only redirects hit detection which occurs above and below the render object.
// In order to make this assumption true, I have removed the minimum width
// constraints, since any reasonable chip would be at least that wide.
return child.hitTest(result, position: new Offset(position.dx, size.height / 2));
} }
} }
...@@ -1932,6 +2027,28 @@ class _RenderChip extends RenderBox { ...@@ -1932,6 +2027,28 @@ class _RenderChip extends RenderBox {
return new Size(deleteIconWidth, deleteIconHeight); return new Size(deleteIconWidth, deleteIconHeight);
} }
@override
bool hitTest(HitTestResult result, {Offset position}) {
if (!size.contains(position))
return false;
RenderBox hitTestChild;
switch (textDirection) {
case TextDirection.ltr:
if (position.dx / size.width > 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 @override
void performLayout() { void performLayout() {
final BoxConstraints contentConstraints = constraints.loosen(); final BoxConstraints contentConstraints = constraints.loosen();
...@@ -2250,20 +2367,4 @@ class _RenderChip extends RenderBox { ...@@ -2250,20 +2367,4 @@ class _RenderChip extends RenderBox {
@override @override
bool hitTestSelf(Offset position) => deleteButtonRect.contains(position) || pressRect.contains(position); 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;
}
} }
...@@ -9,6 +9,7 @@ import 'button.dart'; ...@@ -9,6 +9,7 @@ import 'button.dart';
import 'button_theme.dart'; import 'button_theme.dart';
import 'colors.dart'; import 'colors.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
/// A material design "flat button". /// A material design "flat button".
/// ///
...@@ -62,6 +63,7 @@ class FlatButton extends StatelessWidget { ...@@ -62,6 +63,7 @@ class FlatButton extends StatelessWidget {
this.colorBrightness, this.colorBrightness,
this.padding, this.padding,
this.shape, this.shape,
this.materialTapTargetSize,
@required this.child, @required this.child,
}) : super(key: key); }) : super(key: key);
...@@ -85,6 +87,7 @@ class FlatButton extends StatelessWidget { ...@@ -85,6 +87,7 @@ class FlatButton extends StatelessWidget {
this.splashColor, this.splashColor,
this.colorBrightness, this.colorBrightness,
this.shape, this.shape,
this.materialTapTargetSize,
@required Widget icon, @required Widget icon,
@required Widget label, @required Widget label,
}) : assert(icon != null), }) : assert(icon != null),
...@@ -185,6 +188,15 @@ class FlatButton extends StatelessWidget { ...@@ -185,6 +188,15 @@ class FlatButton extends StatelessWidget {
/// Defaults to the theme's brightness, [ThemeData.brightness]. /// Defaults to the theme's brightness, [ThemeData.brightness].
final Brightness colorBrightness; 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. /// The widget below this widget in the tree.
/// ///
/// Typically a [Text] widget in all caps. /// Typically a [Text] widget in all caps.
...@@ -290,6 +302,7 @@ class FlatButton extends StatelessWidget { ...@@ -290,6 +302,7 @@ class FlatButton extends StatelessWidget {
splashColor: _getSplashColor(theme, buttonTheme), splashColor: _getSplashColor(theme, buttonTheme),
elevation: 0.0, elevation: 0.0,
highlightElevation: 0.0, highlightElevation: 0.0,
materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
padding: padding ?? buttonTheme.padding, padding: padding ?? buttonTheme.padding,
constraints: buttonTheme.constraints, constraints: buttonTheme.constraints,
shape: shape ?? buttonTheme.shape, shape: shape ?? buttonTheme.shape,
...@@ -311,5 +324,6 @@ class FlatButton extends StatelessWidget { ...@@ -311,5 +324,6 @@ class FlatButton extends StatelessWidget {
properties.add(new DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null)); properties.add(new DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null)); properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null)); properties.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(new DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
} }
} }
...@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'button.dart'; import 'button.dart';
import 'scaffold.dart'; import 'scaffold.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
import 'tooltip.dart'; import 'tooltip.dart';
const BoxConstraints _kSizeConstraints = const BoxConstraints.tightFor( const BoxConstraints _kSizeConstraints = const BoxConstraints.tightFor(
...@@ -67,6 +68,7 @@ class FloatingActionButton extends StatefulWidget { ...@@ -67,6 +68,7 @@ class FloatingActionButton extends StatefulWidget {
@required this.onPressed, @required this.onPressed,
this.mini = false, this.mini = false,
this.shape = const CircleBorder(), this.shape = const CircleBorder(),
this.materialTapTargetSize,
this.isExtended = false, this.isExtended = false,
}) : assert(elevation != null), }) : assert(elevation != null),
assert(highlightElevation != null), assert(highlightElevation != null),
...@@ -92,6 +94,7 @@ class FloatingActionButton extends StatefulWidget { ...@@ -92,6 +94,7 @@ class FloatingActionButton extends StatefulWidget {
@required this.onPressed, @required this.onPressed,
this.shape = const StadiumBorder(), this.shape = const StadiumBorder(),
this.isExtended = true, this.isExtended = true,
this.materialTapTargetSize,
@required Widget icon, @required Widget icon,
@required Widget label, @required Widget label,
}) : assert(elevation != null), }) : assert(elevation != null),
...@@ -196,6 +199,15 @@ class FloatingActionButton extends StatefulWidget { ...@@ -196,6 +199,15 @@ class FloatingActionButton extends StatefulWidget {
/// floating action buttons are scaled and faded in. /// floating action buttons are scaled and faded in.
final bool isExtended; 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; final BoxConstraints _sizeConstraints;
@override @override
...@@ -241,7 +253,7 @@ class _FloatingActionButtonState extends State<FloatingActionButton> { ...@@ -241,7 +253,7 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
onHighlightChanged: _handleHighlightChanged, onHighlightChanged: _handleHighlightChanged,
elevation: _highlight ? widget.highlightElevation : widget.elevation, elevation: _highlight ? widget.highlightElevation : widget.elevation,
constraints: widget._sizeConstraints, constraints: widget._sizeConstraints,
outerPadding: widget.mini ? const EdgeInsets.all(4.0) : null, materialTapTargetSize: widget.materialTapTargetSize ?? theme.materialTapTargetSize,
fillColor: widget.backgroundColor ?? theme.accentColor, fillColor: widget.backgroundColor ?? theme.accentColor,
textStyle: theme.accentTextTheme.button.copyWith( textStyle: theme.accentTextTheme.button.copyWith(
color: foregroundColor, color: foregroundColor,
......
...@@ -31,7 +31,7 @@ abstract class InputBorder extends ShapeBorder { ...@@ -31,7 +31,7 @@ abstract class InputBorder extends ShapeBorder {
/// No input border. /// No input border.
/// ///
/// Use this value with [InputDecoration.border] to specify that no 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. /// its border to this value.
static const InputBorder none = const _NoInputBorder(); static const InputBorder none = const _NoInputBorder();
......
...@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
import 'toggleable.dart'; import 'toggleable.dart';
const double _kOuterRadius = 8.0; const double _kOuterRadius = 8.0;
...@@ -54,7 +55,8 @@ class Radio<T> extends StatefulWidget { ...@@ -54,7 +55,8 @@ class Radio<T> extends StatefulWidget {
@required this.value, @required this.value,
@required this.groupValue, @required this.groupValue,
@required this.onChanged, @required this.onChanged,
this.activeColor this.activeColor,
this.materialTapTargetSize,
}) : super(key: key); }) : super(key: key);
/// The value represented by this radio button. /// The value represented by this radio button.
...@@ -96,6 +98,15 @@ class Radio<T> extends StatefulWidget { ...@@ -96,6 +98,15 @@ class Radio<T> extends StatefulWidget {
/// Defaults to [ThemeData.toggleableActiveColor]. /// Defaults to [ThemeData.toggleableActiveColor].
final Color activeColor; 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 @override
_RadioState<T> createState() => new _RadioState<T>(); _RadioState<T> createState() => new _RadioState<T>();
} }
...@@ -116,11 +127,22 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -116,11 +127,22 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(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( return new _RadioRenderObjectWidget(
selected: widget.value == widget.groupValue, selected: widget.value == widget.groupValue,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor, activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData), inactiveColor: _getInactiveColor(themeData),
onChanged: _enabled ? _handleChanged : null, onChanged: _enabled ? _handleChanged : null,
additionalConstraints: additionalConstraints,
vsync: this, vsync: this,
); );
} }
...@@ -132,6 +154,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -132,6 +154,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
@required this.selected, @required this.selected,
@required this.activeColor, @required this.activeColor,
@required this.inactiveColor, @required this.inactiveColor,
@required this.additionalConstraints,
this.onChanged, this.onChanged,
@required this.vsync, @required this.vsync,
}) : assert(selected != null), }) : assert(selected != null),
...@@ -145,6 +168,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -145,6 +168,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
final Color activeColor; final Color activeColor;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final TickerProvider vsync; final TickerProvider vsync;
final BoxConstraints additionalConstraints;
@override @override
_RenderRadio createRenderObject(BuildContext context) => new _RenderRadio( _RenderRadio createRenderObject(BuildContext context) => new _RenderRadio(
...@@ -153,6 +177,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -153,6 +177,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
vsync: vsync, vsync: vsync,
additionalConstraints: additionalConstraints,
); );
@override @override
...@@ -162,6 +187,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -162,6 +187,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
..activeColor = activeColor ..activeColor = activeColor
..inactiveColor = inactiveColor ..inactiveColor = inactiveColor
..onChanged = onChanged ..onChanged = onChanged
..additionalConstraints = additionalConstraints
..vsync = vsync; ..vsync = vsync;
} }
} }
...@@ -172,6 +198,7 @@ class _RenderRadio extends RenderToggleable { ...@@ -172,6 +198,7 @@ class _RenderRadio extends RenderToggleable {
Color activeColor, Color activeColor,
Color inactiveColor, Color inactiveColor,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
BoxConstraints additionalConstraints,
@required TickerProvider vsync, @required TickerProvider vsync,
}): super( }): super(
value: value, value: value,
...@@ -179,7 +206,7 @@ class _RenderRadio extends RenderToggleable { ...@@ -179,7 +206,7 @@ class _RenderRadio extends RenderToggleable {
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), additionalConstraints: additionalConstraints,
vsync: vsync, vsync: vsync,
); );
......
...@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'list_tile.dart'; import 'list_tile.dart';
import 'radio.dart'; import 'radio.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
/// A [ListTile] with a [Radio]. In other words, a radio button with a label. /// A [ListTile] with a [Radio]. In other words, a radio button with a label.
/// ///
...@@ -198,6 +199,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -198,6 +199,7 @@ class RadioListTile<T> extends StatelessWidget {
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged, onChanged: onChanged,
activeColor: activeColor, activeColor: activeColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
); );
Widget leading, trailing; Widget leading, trailing;
switch (controlAffinity) { switch (controlAffinity) {
......
...@@ -10,6 +10,7 @@ import 'button_theme.dart'; ...@@ -10,6 +10,7 @@ import 'button_theme.dart';
import 'colors.dart'; import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
/// A material design "raised button". /// A material design "raised button".
/// ///
...@@ -62,6 +63,7 @@ class RaisedButton extends StatelessWidget { ...@@ -62,6 +63,7 @@ class RaisedButton extends StatelessWidget {
this.disabledElevation = 0.0, this.disabledElevation = 0.0,
this.padding, this.padding,
this.shape, this.shape,
this.materialTapTargetSize,
this.animationDuration = kThemeChangeDuration, this.animationDuration = kThemeChangeDuration,
this.child, this.child,
}) : assert(elevation != null), }) : assert(elevation != null),
...@@ -94,6 +96,7 @@ class RaisedButton extends StatelessWidget { ...@@ -94,6 +96,7 @@ class RaisedButton extends StatelessWidget {
this.highlightElevation = 8.0, this.highlightElevation = 8.0,
this.disabledElevation = 0.0, this.disabledElevation = 0.0,
this.shape, this.shape,
this.materialTapTargetSize,
this.animationDuration = kThemeChangeDuration, this.animationDuration = kThemeChangeDuration,
@required Widget icon, @required Widget icon,
@required Widget label, @required Widget label,
...@@ -289,6 +292,15 @@ class RaisedButton extends StatelessWidget { ...@@ -289,6 +292,15 @@ class RaisedButton extends StatelessWidget {
/// The default value is [kThemeChangeDuration]. /// The default value is [kThemeChangeDuration].
final Duration animationDuration; 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) { Brightness _getBrightness(ThemeData theme) {
return colorBrightness ?? theme.brightness; return colorBrightness ?? theme.brightness;
} }
...@@ -380,6 +392,7 @@ class RaisedButton extends StatelessWidget { ...@@ -380,6 +392,7 @@ class RaisedButton extends StatelessWidget {
shape: shape ?? buttonTheme.shape, shape: shape ?? buttonTheme.shape,
animationDuration: animationDuration, animationDuration: animationDuration,
child: child, child: child,
materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
); );
} }
......
...@@ -12,8 +12,17 @@ import 'constants.dart'; ...@@ -12,8 +12,17 @@ import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'shadows.dart'; import 'shadows.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
import 'toggleable.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. /// A material design switch.
/// ///
/// Used to toggle the on/off state of a single setting. /// Used to toggle the on/off state of a single setting.
...@@ -54,7 +63,8 @@ class Switch extends StatefulWidget { ...@@ -54,7 +63,8 @@ class Switch extends StatefulWidget {
this.inactiveThumbColor, this.inactiveThumbColor,
this.inactiveTrackColor, this.inactiveTrackColor,
this.activeThumbImage, this.activeThumbImage,
this.inactiveThumbImage this.inactiveThumbImage,
this.materialTapTargetSize,
}) : super(key: key); }) : super(key: key);
/// Whether this switch is on or off. /// Whether this switch is on or off.
...@@ -112,6 +122,15 @@ class Switch extends StatefulWidget { ...@@ -112,6 +122,15 @@ class Switch extends StatefulWidget {
/// An image to use on the thumb of this switch when the switch is off. /// An image to use on the thumb of this switch when the switch is off.
final ImageProvider inactiveThumbImage; 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 @override
_SwitchState createState() => new _SwitchState(); _SwitchState createState() => new _SwitchState();
...@@ -142,6 +161,16 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -142,6 +161,16 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400); inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12); 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( return new _SwitchRenderObjectWidget(
value: widget.value, value: widget.value,
...@@ -153,6 +182,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -153,6 +182,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context), configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged, onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this, vsync: this,
); );
} }
...@@ -171,6 +201,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -171,6 +201,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.configuration, this.configuration,
this.onChanged, this.onChanged,
this.vsync, this.vsync,
this.additionalConstraints,
}) : super(key: key); }) : super(key: key);
final bool value; final bool value;
...@@ -183,6 +214,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -183,6 +214,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final ImageConfiguration configuration; final ImageConfiguration configuration;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final TickerProvider vsync; final TickerProvider vsync;
final BoxConstraints additionalConstraints;
@override @override
_RenderSwitch createRenderObject(BuildContext context) { _RenderSwitch createRenderObject(BuildContext context) {
...@@ -197,6 +229,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -197,6 +229,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
configuration: configuration, configuration: configuration,
onChanged: onChanged, onChanged: onChanged,
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
additionalConstraints: additionalConstraints,
vsync: vsync, vsync: vsync,
); );
} }
...@@ -214,17 +247,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -214,17 +247,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..configuration = configuration ..configuration = configuration
..onChanged = onChanged ..onChanged = onChanged
..textDirection = Directionality.of(context) ..textDirection = Directionality.of(context)
..additionalConstraints = additionalConstraints
..vsync = vsync; ..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 { class _RenderSwitch extends RenderToggleable {
_RenderSwitch({ _RenderSwitch({
bool value, bool value,
...@@ -235,6 +262,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -235,6 +262,7 @@ class _RenderSwitch extends RenderToggleable {
Color activeTrackColor, Color activeTrackColor,
Color inactiveTrackColor, Color inactiveTrackColor,
ImageConfiguration configuration, ImageConfiguration configuration,
BoxConstraints additionalConstraints,
@required TextDirection textDirection, @required TextDirection textDirection,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
@required TickerProvider vsync, @required TickerProvider vsync,
...@@ -251,7 +279,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -251,7 +279,7 @@ class _RenderSwitch extends RenderToggleable {
activeColor: activeColor, activeColor: activeColor,
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
onChanged: onChanged, onChanged: onChanged,
size: const Size(_kSwitchWidth, _kSwitchHeight), additionalConstraints: additionalConstraints,
vsync: vsync, vsync: vsync,
) { ) {
_drag = new HorizontalDragGestureRecognizer() _drag = new HorizontalDragGestureRecognizer()
......
...@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'list_tile.dart'; import 'list_tile.dart';
import 'switch.dart'; import 'switch.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
/// A [ListTile] with a [Switch]. In other words, a switch with a label. /// A [ListTile] with a [Switch]. In other words, a switch with a label.
/// ///
...@@ -171,6 +172,7 @@ class SwitchListTile extends StatelessWidget { ...@@ -171,6 +172,7 @@ class SwitchListTile extends StatelessWidget {
activeColor: activeColor, activeColor: activeColor,
activeThumbImage: activeThumbImage, activeThumbImage: activeThumbImage,
inactiveThumbImage: inactiveThumbImage, inactiveThumbImage: inactiveThumbImage,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
); );
return new MergeSemantics( return new MergeSemantics(
child: ListTileTheme.merge( child: ListTileTheme.merge(
......
...@@ -36,6 +36,42 @@ const Color _kLightThemeSplashColor = const Color(0x66C8C8C8); ...@@ -36,6 +36,42 @@ const Color _kLightThemeSplashColor = const Color(0x66C8C8C8);
const Color _kDarkThemeHighlightColor = const Color(0x40CCCCCC); const Color _kDarkThemeHighlightColor = const Color(0x40CCCCCC);
const Color _kDarkThemeSplashColor = 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. /// Holds the color and typography values for a material design theme.
/// ///
/// Use this class to configure a [Theme] widget. /// Use this class to configure a [Theme] widget.
...@@ -105,7 +141,9 @@ class ThemeData extends Diagnosticable { ...@@ -105,7 +141,9 @@ class ThemeData extends Diagnosticable {
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
ChipThemeData chipTheme, ChipThemeData chipTheme,
TargetPlatform platform, TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
}) { }) {
materialTapTargetSize ??= MaterialTapTargetSize.padded;
brightness ??= Brightness.light; brightness ??= Brightness.light;
final bool isDark = brightness == Brightness.dark; final bool isDark = brightness == Brightness.dark;
primarySwatch ??= Colors.blue; primarySwatch ??= Colors.blue;
...@@ -208,6 +246,7 @@ class ThemeData extends Diagnosticable { ...@@ -208,6 +246,7 @@ class ThemeData extends Diagnosticable {
sliderTheme: sliderTheme, sliderTheme: sliderTheme,
chipTheme: chipTheme, chipTheme: chipTheme,
platform: platform, platform: platform,
materialTapTargetSize: materialTapTargetSize,
); );
} }
...@@ -257,6 +296,7 @@ class ThemeData extends Diagnosticable { ...@@ -257,6 +296,7 @@ class ThemeData extends Diagnosticable {
@required this.sliderTheme, @required this.sliderTheme,
@required this.chipTheme, @required this.chipTheme,
@required this.platform, @required this.platform,
@required this.materialTapTargetSize,
}) : assert(brightness != null), }) : assert(brightness != null),
assert(primaryColor != null), assert(primaryColor != null),
assert(primaryColorBrightness != null), assert(primaryColorBrightness != null),
...@@ -294,7 +334,8 @@ class ThemeData extends Diagnosticable { ...@@ -294,7 +334,8 @@ class ThemeData extends Diagnosticable {
assert(accentIconTheme != null), assert(accentIconTheme != null),
assert(sliderTheme != null), assert(sliderTheme != null),
assert(chipTheme != null), assert(chipTheme != null),
assert(platform != null); assert(platform != null),
assert(materialTapTargetSize != null);
/// A default light blue theme. /// A default light blue theme.
/// ///
...@@ -483,6 +524,9 @@ class ThemeData extends Diagnosticable { ...@@ -483,6 +524,9 @@ class ThemeData extends Diagnosticable {
/// Defaults to the current platform. /// Defaults to the current platform.
final TargetPlatform 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. /// Creates a copy of this theme but with the given fields replaced with the new values.
ThemeData copyWith({ ThemeData copyWith({
Brightness brightness, Brightness brightness,
...@@ -524,6 +568,7 @@ class ThemeData extends Diagnosticable { ...@@ -524,6 +568,7 @@ class ThemeData extends Diagnosticable {
SliderThemeData sliderTheme, SliderThemeData sliderTheme,
ChipThemeData chipTheme, ChipThemeData chipTheme,
TargetPlatform platform, TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
}) { }) {
return new ThemeData.raw( return new ThemeData.raw(
brightness: brightness ?? this.brightness, brightness: brightness ?? this.brightness,
...@@ -565,6 +610,7 @@ class ThemeData extends Diagnosticable { ...@@ -565,6 +610,7 @@ class ThemeData extends Diagnosticable {
sliderTheme: sliderTheme ?? this.sliderTheme, sliderTheme: sliderTheme ?? this.sliderTheme,
chipTheme: chipTheme ?? this.chipTheme, chipTheme: chipTheme ?? this.chipTheme,
platform: platform ?? this.platform, platform: platform ?? this.platform,
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
); );
} }
...@@ -692,6 +738,7 @@ class ThemeData extends Diagnosticable { ...@@ -692,6 +738,7 @@ class ThemeData extends Diagnosticable {
sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t), sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t), chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t),
platform: t < 0.5 ? a.platform : b.platform, platform: t < 0.5 ? a.platform : b.platform,
materialTapTargetSize: t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
); );
} }
...@@ -736,7 +783,8 @@ class ThemeData extends Diagnosticable { ...@@ -736,7 +783,8 @@ class ThemeData extends Diagnosticable {
(otherData.accentIconTheme == accentIconTheme) && (otherData.accentIconTheme == accentIconTheme) &&
(otherData.sliderTheme == sliderTheme) && (otherData.sliderTheme == sliderTheme) &&
(otherData.chipTheme == chipTheme) && (otherData.chipTheme == chipTheme) &&
(otherData.platform == platform); (otherData.platform == platform) &&
(otherData.materialTapTargetSize == materialTapTargetSize);
} }
@override @override
...@@ -780,6 +828,7 @@ class ThemeData extends Diagnosticable { ...@@ -780,6 +828,7 @@ class ThemeData extends Diagnosticable {
sliderTheme, sliderTheme,
chipTheme, chipTheme,
platform, platform,
materialTapTargetSize
), ),
); );
} }
...@@ -824,6 +873,7 @@ class ThemeData extends Diagnosticable { ...@@ -824,6 +873,7 @@ class ThemeData extends Diagnosticable {
properties.add(new DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme)); properties.add(new DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme));
properties.add(new DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme)); properties.add(new DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme));
properties.add(new DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme)); properties.add(new DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme));
properties.add(new DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize));
} }
} }
......
...@@ -17,6 +17,7 @@ import 'feedback.dart'; ...@@ -17,6 +17,7 @@ import 'feedback.dart';
import 'flat_button.dart'; import 'flat_button.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart';
import 'time.dart'; import 'time.dart';
import 'typography.dart'; import 'typography.dart';
...@@ -29,11 +30,15 @@ enum _TimePickerMode { hour, minute } ...@@ -29,11 +30,15 @@ enum _TimePickerMode { hour, minute }
const double _kTimePickerHeaderPortraitHeight = 96.0; const double _kTimePickerHeaderPortraitHeight = 96.0;
const double _kTimePickerHeaderLandscapeWidth = 168.0; const double _kTimePickerHeaderLandscapeWidth = 168.0;
const double _kTimePickerWidthPortrait = 328.0; const double _kTimePickerWidthPortrait = 328.0;
const double _kTimePickerWidthLandscape = 512.0; const double _kTimePickerWidthLandscape = 512.0;
const double _kTimePickerHeightPortrait = 484.0; const double _kTimePickerHeightPortrait = 496.0;
const double _kTimePickerHeightLandscape = 304.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 /// The horizontal gap between the day period fragment and the fragment
/// positioned next to it horizontally. /// positioned next to it horizontally.
...@@ -1575,12 +1580,25 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -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); assert(orientation != null);
switch (orientation) { switch (orientation) {
case Orientation.portrait: case Orientation.portrait:
return new SizedBox( return new SizedBox(
width: _kTimePickerWidthPortrait, width: _kTimePickerWidthPortrait,
height: _kTimePickerHeightPortrait, height: timePickerHeightPortrait,
child: new Column( child: new Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
...@@ -1595,7 +1613,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ...@@ -1595,7 +1613,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> {
case Orientation.landscape: case Orientation.landscape:
return new SizedBox( return new SizedBox(
width: _kTimePickerWidthLandscape, width: _kTimePickerWidthLandscape,
height: _kTimePickerHeightLandscape, height: timePickerHeightLandscape,
child: new Row( child: new Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
......
...@@ -26,10 +26,10 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -26,10 +26,10 @@ abstract class RenderToggleable extends RenderConstrainedBox {
RenderToggleable({ RenderToggleable({
@required bool value, @required bool value,
bool tristate = false, bool tristate = false,
Size size,
@required Color activeColor, @required Color activeColor,
@required Color inactiveColor, @required Color inactiveColor,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
BoxConstraints additionalConstraints,
@required TickerProvider vsync, @required TickerProvider vsync,
}) : assert(tristate != null), }) : assert(tristate != null),
assert(tristate || value != null), assert(tristate || value != null),
...@@ -42,7 +42,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -42,7 +42,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
_inactiveColor = inactiveColor, _inactiveColor = inactiveColor,
_onChanged = onChanged, _onChanged = onChanged,
_vsync = vsync, _vsync = vsync,
super(additionalConstraints: new BoxConstraints.tight(size)) { super(additionalConstraints: additionalConstraints) {
_tap = new TapGestureRecognizer() _tap = new TapGestureRecognizer()
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTap = _handleTap ..onTap = _handleTap
......
...@@ -39,8 +39,8 @@ void main() { ...@@ -39,8 +39,8 @@ void main() {
SemanticsAction.tap, SemanticsAction.tap,
], ],
label: 'ABC', label: 'ABC',
rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: new Matrix4.translationValues(356.0, 282.0, 0.0), transform: new Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isButton, SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState, SemanticsFlag.hasEnabledState,
...@@ -79,8 +79,8 @@ void main() { ...@@ -79,8 +79,8 @@ void main() {
SemanticsAction.tap, SemanticsAction.tap,
], ],
label: 'ABC', label: 'ABC',
rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: new Matrix4.translationValues(356.0, 282.0, 0.0), transform: new Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isButton, SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState, SemanticsFlag.hasEnabledState,
...@@ -113,7 +113,7 @@ void main() { ...@@ -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))); expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0)));
// textScaleFactor expands text, but not button. // textScaleFactor expands text, but not button.
...@@ -134,7 +134,7 @@ void main() { ...@@ -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. // Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(#12357): Update this test when text rendering is fixed. // TODO(#12357): Update this test when text rendering is fixed.
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0])); expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0]));
...@@ -162,7 +162,7 @@ void main() { ...@@ -162,7 +162,7 @@ void main() {
// Scaled text rendering is different on Linux and Mac by one pixel. // Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(#12357): Update this test when text rendering is fixed. // TODO(#12357): Update this test when text rendering is fixed.
expect(tester.getSize(find.byType(FlatButton)).width, isIn(<double>[158.0, 159.0])); expect(tester.getSize(find.byType(FlatButton)).width, isIn(<double>[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(<double>[126.0, 127.0])); expect(tester.getSize(find.byType(Text)).width, isIn(<double>[126.0, 127.0]));
expect(tester.getSize(find.byType(Text)).height, equals(42.0)); expect(tester.getSize(find.byType(Text)).height, equals(42.0));
}); });
...@@ -187,7 +187,9 @@ void main() { ...@@ -187,7 +187,9 @@ void main() {
new Directionality( new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Theme( child: new Theme(
data: new ThemeData(), data: new ThemeData(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: buttonWidget, child: buttonWidget,
), ),
), ),
...@@ -233,6 +235,7 @@ void main() { ...@@ -233,6 +235,7 @@ void main() {
data: new ThemeData( data: new ThemeData(
highlightColor: themeHighlightColor1, highlightColor: themeHighlightColor1,
splashColor: themeSplashColor1, splashColor: themeSplashColor1,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
child: buttonWidget, child: buttonWidget,
), ),
...@@ -260,6 +263,7 @@ void main() { ...@@ -260,6 +263,7 @@ void main() {
data: new ThemeData( data: new ThemeData(
highlightColor: themeHighlightColor2, highlightColor: themeHighlightColor2,
splashColor: themeSplashColor2, splashColor: themeSplashColor2,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
child: buttonWidget, // same widget, so does not get updated because of us child: buttonWidget, // same widget, so does not get updated because of us
), ),
...@@ -279,7 +283,7 @@ void main() { ...@@ -279,7 +283,7 @@ void main() {
testWidgets('Disabled MaterialButton has same semantic size as enabled and exposes disabled semantics', (WidgetTester tester) async { testWidgets('Disabled MaterialButton has same semantic size as enabled and exposes disabled semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); 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 // Button is in center of screen
final Matrix4 expectedButtonTransform = new Matrix4.identity() final Matrix4 expectedButtonTransform = new Matrix4.identity()
..translate( ..translate(
...@@ -354,4 +358,136 @@ void main() { ...@@ -354,4 +358,136 @@ void main() {
semantics.dispose(); 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));
});
} }
...@@ -16,13 +16,38 @@ void main() { ...@@ -16,13 +16,38 @@ void main() {
debugResetSemanticsIdCounter(); debugResetSemanticsIdCounter();
}); });
testWidgets('Checkbox size is 40x40', (WidgetTester tester) async { testWidgets('Checkbox size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new Material( new Theme(
child: new Center( data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded),
child: new Checkbox( child: new Directionality(
value: false, textDirection: TextDirection.ltr,
onChanged: (bool newValue) { }, 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) {},
),
),
), ),
), ),
), ),
......
...@@ -67,6 +67,44 @@ void main() { ...@@ -67,6 +67,44 @@ void main() {
expect(find.byType(Text), findsOneWidget); 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 { testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
......
...@@ -55,7 +55,7 @@ void main() { ...@@ -55,7 +55,7 @@ void main() {
new Directionality( new Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Theme( child: new Theme(
data: new ThemeData(), data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: new Container( child: new Container(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: new OutlineButton( child: new OutlineButton(
...@@ -140,8 +140,8 @@ void main() { ...@@ -140,8 +140,8 @@ void main() {
SemanticsAction.tap, SemanticsAction.tap,
], ],
label: 'ABC', label: 'ABC',
rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: new Matrix4.translationValues(356.0, 282.0, 0.0), transform: new Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isButton, SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState, SemanticsFlag.hasEnabledState,
...@@ -175,7 +175,7 @@ void main() { ...@@ -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))); expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0)));
// textScaleFactor expands text, but not button. // textScaleFactor expands text, but not button.
...@@ -196,7 +196,7 @@ void main() { ...@@ -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. // Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(#12357): Update this test when text rendering is fixed. // TODO(#12357): Update this test when text rendering is fixed.
expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0])); expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0]));
...@@ -224,7 +224,7 @@ void main() { ...@@ -224,7 +224,7 @@ void main() {
// Scaled text rendering is different on Linux and Mac by one pixel. // Scaled text rendering is different on Linux and Mac by one pixel.
// TODO(#12357): Update this test when text rendering is fixed. // TODO(#12357): Update this test when text rendering is fixed.
expect(tester.getSize(find.byType(FlatButton)).width, isIn(<double>[158.0, 159.0])); expect(tester.getSize(find.byType(FlatButton)).width, isIn(<double>[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(<double>[126.0, 127.0])); expect(tester.getSize(find.byType(Text)).width, isIn(<double>[126.0, 127.0]));
expect(tester.getSize(find.byType(Text)).height, equals(42.0)); expect(tester.getSize(find.byType(Text)).height, equals(42.0));
}); });
......
...@@ -64,23 +64,50 @@ void main() { ...@@ -64,23 +64,50 @@ void main() {
expect(log, isEmpty); expect(log, isEmpty);
}); });
testWidgets('Radio size is 40x40', (WidgetTester tester) async { testWidgets('Radio size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
final Key key = new UniqueKey(); 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 Radio<bool>(
key: key1,
groupValue: true,
value: true,
onChanged: (bool newValue) {},
),
),
),
),
),
);
expect(tester.getSize(find.byKey(key1)), const Size(48.0, 48.0));
final Key key2 = new UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(
new Material( new Theme(
child: new Center( data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: new Radio<int>( child: new Directionality(
key: key, textDirection: TextDirection.ltr,
value: 1, child: new Material(
groupValue: 2, child: new Center(
onChanged: (int newValue) { }, child: new Radio<bool>(
key: key2,
groupValue: true,
value: true,
onChanged: (bool newValue) {},
),
),
), ),
), ),
), ),
); );
expect(tester.getSize(find.byKey(key)), const Size(40.0, 40.0)); expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0));
}); });
......
...@@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; ...@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { void main() {
testWidgets('outerPadding expands hit test area', (WidgetTester tester) async { testWidgets('materialTapTargetSize.padded expands hit test area', (WidgetTester tester) async {
int pressed = 0; int pressed = 0;
await tester.pumpWidget(new RawMaterialButton( await tester.pumpWidget(new RawMaterialButton(
...@@ -13,23 +14,23 @@ void main() { ...@@ -13,23 +14,23 @@ void main() {
pressed++; pressed++;
}, },
constraints: new BoxConstraints.tight(const Size(10.0, 10.0)), 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), 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); 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); final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
new Center( new Center(
child: new RawMaterialButton( child: new RawMaterialButton(
onPressed: () {}, onPressed: () {},
constraints: new BoxConstraints.tight(const Size(10.0, 10.0)), 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), child: const Text('+', textDirection: TextDirection.ltr),
), ),
), ),
...@@ -50,7 +51,7 @@ void main() { ...@@ -50,7 +51,7 @@ void main() {
], ],
label: '+', label: '+',
textDirection: TextDirection.ltr, 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: <TestSemantics>[], children: <TestSemantics>[],
), ),
] ]
...@@ -58,4 +59,57 @@ void main() { ...@@ -58,4 +59,57 @@ void main() {
semantics.dispose(); 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
...@@ -542,7 +542,7 @@ void main() { ...@@ -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<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
}); });
}); });
......
...@@ -341,10 +341,10 @@ void main() { ...@@ -341,10 +341,10 @@ void main() {
final Offset snackBarBottomRight = snackBarBox.localToGlobal(snackBarBox.size.bottomRight(Offset.zero)); final Offset snackBarBottomRight = snackBarBox.localToGlobal(snackBarBox.size.bottomRight(Offset.zero));
expect(textBottomLeft.dx - snackBarBottomLeft.dx, 24.0 + 10.0); // margin + left padding 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(actionTextBottomLeft.dx - textBottomRight.dx, 24.0);
expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 24.0 + 30.0); // margin + right padding 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 { testWidgets('SnackBar is positioned above BottomNavigationBar', (WidgetTester tester) async {
...@@ -398,10 +398,10 @@ void main() { ...@@ -398,10 +398,10 @@ void main() {
final Offset snackBarBottomRight = snackBarBox.localToGlobal(snackBarBox.size.bottomRight(Offset.zero)); final Offset snackBarBottomRight = snackBarBox.localToGlobal(snackBarBox.size.bottomRight(Offset.zero));
expect(textBottomLeft.dx - snackBarBottomLeft.dx, 24.0 + 10.0); // margin + left padding 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(actionTextBottomLeft.dx - textBottomRight.dx, 24.0);
expect(snackBarBottomRight.dx - actionTextBottomRight.dx, 24.0 + 30.0); // margin + right padding 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 { testWidgets('SnackBarClosedReason', (WidgetTester tester) async {
......
...@@ -43,6 +43,46 @@ void main() { ...@@ -43,6 +43,46 @@ void main() {
expect(value, isTrue); 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 { testWidgets('Switch can drag (LTR)', (WidgetTester tester) async {
bool value = false; bool value = false;
......
...@@ -103,6 +103,12 @@ void main() { ...@@ -103,6 +103,12 @@ void main() {
expect(darkTheme.accentTextTheme.title.color, typography.white.title.color); 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', () { test('Can control fontFamily', () {
final ThemeData themeData = new ThemeData(fontFamily: 'Ahem'); final ThemeData themeData = new ThemeData(fontFamily: 'Ahem');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment