Unverified Commit 77a528c4 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Re-land #18369 (#19082)

parent 779eea68
......@@ -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<bool> 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<RawMaterialButton> {
),
),
);
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,41 @@ 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 if the child hit test would have failed otherwise.
///
/// 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;
if (child.hitTest(result, position: position))
return true;
return child.hitTest(result, position: size.center(Offset.zero));
}
}
......@@ -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<Checkbox> 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<bool> 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<bool> 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,
);
......
......@@ -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) {
......
......@@ -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<RawChip> with TickerProviderStateMixin<RawChip
final TextDirection textDirection = Directionality.of(context);
final ShapeBorder shape = widget.shape ?? chipTheme.shape;
return new Material(
Widget result = new Material(
elevation: isTapping ? _kPressElevation : 0.0,
animationDuration: pressedAnimationDuration,
shape: shape,
......@@ -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 {
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
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;
}
}
......@@ -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<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, 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';
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<FloatingActionButton> {
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,
......
......@@ -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();
......
......@@ -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<T> 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<T> 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<T> createState() => new _RadioState<T>();
}
......@@ -116,11 +127,22 @@ class _RadioState<T> extends State<Radio<T>> 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<bool> 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<bool> 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,
);
......
......@@ -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<T> extends StatelessWidget {
groupValue: groupValue,
onChanged: onChanged,
activeColor: activeColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
Widget leading, trailing;
switch (controlAffinity) {
......
......@@ -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,
);
}
......
......@@ -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<Switch> 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<Switch> 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<bool> 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<bool> 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()
......
......@@ -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(
......
......@@ -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<IconThemeData>('accentIconTheme', accentIconTheme));
properties.add(new DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme));
properties.add(new DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme));
properties.add(new DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize));
}
}
......
......@@ -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,
......
......@@ -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<bool> 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
......
......@@ -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>[
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>[
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(<double>[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(<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)).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));
});
}
......@@ -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) {},
),
),
),
),
),
......
......@@ -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(
......
......@@ -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>[
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(<double>[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(<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)).height, equals(42.0));
});
......
......@@ -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 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(
new Material(
child: new Center(
child: new Radio<int>(
key: key,
value: 1,
groupValue: 2,
onChanged: (int newValue) { },
new Theme(
data: new ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: new Directionality(
textDirection: TextDirection.ltr,
child: new Material(
child: new Center(
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';
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: <TestSemantics>[],
),
]
......@@ -58,4 +59,110 @@ 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();
});
testWidgets('off-center child is hit testable', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
home: new Column(
children: <Widget>[
new RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.padded,
onPressed: () {},
child: new Container(
width: 400.0,
height: 400.0,
child: new Column(
mainAxisAlignment: MainAxisAlignment.end,
children: const <Widget>[
const SizedBox(
height: 50.0,
width: 400.0,
child: const Text('Material'),
),
],
),
),
),
]),
),
);
expect(find.text('Material').hitTestable(), findsOneWidget);
});
testWidgets('smaller child is hit testable', (WidgetTester tester) async {
const Key key = const Key('test');
await tester.pumpWidget(
new MaterialApp(
home: new Column(
children: <Widget>[
new RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.padded,
onPressed: () {},
child: new SizedBox(
key: key,
width: 8.0,
height: 8.0,
child: new Container(
color: const Color(0xFFAABBCC),
),
),
),
]),
),
);
expect(find.byKey(key).hitTestable(), findsOneWidget);
});
}
\ No newline at end of file
......@@ -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));
});
});
......
......@@ -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 {
......
......@@ -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;
......
......@@ -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');
......
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