Allow IconButton to have smaller sizes (#47457)
diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart
index 8862793..43e8730 100644
--- a/packages/flutter/lib/src/material/button.dart
+++ b/packages/flutter/lib/src/material/button.dart
@@ -366,10 +366,7 @@
final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(widget.textStyle?.color, _states);
final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment;
- final BoxConstraints effectiveConstraints = widget.constraints.copyWith(
- minWidth: widget.constraints.minWidth != null ? (widget.constraints.minWidth + densityAdjustment.dx).clamp(0.0, double.infinity) as double : null,
- minHeight: widget.constraints.minWidth != null ? (widget.constraints.minHeight + densityAdjustment.dy).clamp(0.0, double.infinity) as double : null,
- );
+ final BoxConstraints effectiveConstraints = widget.visualDensity.effectiveConstraints(widget.constraints);
final EdgeInsetsGeometry padding = widget.padding.add(
EdgeInsets.only(
left: densityAdjustment.dx,
diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart
index 6b3ce2f..2d510da 100644
--- a/packages/flutter/lib/src/material/icon_button.dart
+++ b/packages/flutter/lib/src/material/icon_button.dart
@@ -154,6 +154,7 @@
this.autofocus = false,
this.tooltip,
this.enableFeedback = true,
+ this.constraints,
}) : assert(iconSize != null),
assert(padding != null),
assert(alignment != null),
@@ -288,6 +289,26 @@
/// * [Feedback] for providing platform-specific feedback to certain actions.
final bool enableFeedback;
+ /// Optional size constraints for the button.
+ ///
+ /// When unspecified, defaults to:
+ /// ```dart
+ /// const BoxConstraints(
+ /// minWidth: kMinInteractiveDimension,
+ /// minHeight: kMinInteractiveDimension,
+ /// )
+ /// ```
+ /// where [kMinInteractiveDimension] is 48.0, and then with visual density
+ /// applied.
+ ///
+ /// The default constraints ensure that the button is accessible.
+ /// Specifying this parameter enables creation of buttons smaller than
+ /// the minimum size, but it is not recommended.
+ ///
+ /// The visual density uses the [visualDensity] parameter if specified,
+ /// and `Theme.of(context).visualDensity` otherwise.
+ final BoxConstraints constraints;
+
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
@@ -298,9 +319,16 @@
else
currentColor = disabledColor ?? theme.disabledColor;
- final Offset densityAdjustment = (visualDensity ?? theme.visualDensity).baseSizeAdjustment;
+ final VisualDensity effectiveVisualDensity = visualDensity ?? theme.visualDensity;
+
+ final BoxConstraints unadjustedConstraints = constraints ?? const BoxConstraints(
+ minWidth: _kMinButtonSize,
+ minHeight: _kMinButtonSize,
+ );
+ final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);
+
Widget result = ConstrainedBox(
- constraints: BoxConstraints(minWidth: _kMinButtonSize + densityAdjustment.dx, minHeight: _kMinButtonSize + densityAdjustment.dy),
+ constraints: adjustedConstraints,
child: Padding(
padding: padding,
child: SizedBox(
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index 5281031..b81f743 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -1815,6 +1815,16 @@
);
}
+ /// Return a copy of [constraints] whose minimum width and height have been
+ /// updated with the [baseSizeAdjustment].
+ BoxConstraints effectiveConstraints(BoxConstraints constraints){
+ assert(constraints != null && constraints.debugAssertIsValid());
+ return constraints.copyWith(
+ minWidth: (constraints.minWidth + baseSizeAdjustment.dx).clamp(0.0, double.infinity).toDouble(),
+ minHeight: (constraints.minHeight + baseSizeAdjustment.dy).clamp(0.0, double.infinity).toDouble(),
+ );
+ }
+
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart
index a93f923..094c950 100644
--- a/packages/flutter/test/material/icon_button_test.dart
+++ b/packages/flutter/test/material/icon_button_test.dart
@@ -75,6 +75,69 @@
expect(iconButton.size, const Size(70.0, 70.0));
});
+ testWidgets('Small icons with non-null constraints can be <48dp', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ wrap(
+ child: IconButton(
+ iconSize: 10.0,
+ onPressed: mockOnPressedFunction,
+ icon: const Icon(Icons.link),
+ constraints: const BoxConstraints(),
+ ),
+ ),
+ );
+
+ final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
+
+ // By default IconButton has a padding of 8.0 on all sides, so both
+ // width and height are 10.0 + 2 * 8.0 = 26.0
+ expect(iconButton.size, const Size(26.0, 26.0));
+ });
+
+ testWidgets('Small icons with non-null constraints and custom padding can be <48dp', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ wrap(
+ child: IconButton(
+ iconSize: 10.0,
+ padding: const EdgeInsets.all(3.0),
+ onPressed: mockOnPressedFunction,
+ icon: const Icon(Icons.link),
+ constraints: const BoxConstraints(),
+ ),
+ ),
+ );
+
+ final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
+
+ // This IconButton has a padding of 3.0 on all sides, so both
+ // width and height are 10.0 + 2 * 3.0 = 16.0
+ expect(iconButton.size, const Size(16.0, 16.0));
+ });
+
+ testWidgets('Small icons comply with VisualDensity requirements', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ wrap(
+ child: Theme(
+ data: ThemeData(visualDensity: const VisualDensity(horizontal: 1, vertical: -1)),
+ child: IconButton(
+ iconSize: 10.0,
+ onPressed: mockOnPressedFunction,
+ icon: const Icon(Icons.link),
+ constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0),
+ ),
+ ),
+ ),
+ );
+
+ final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
+
+ // VisualDensity(horizontal: 1, vertical: -1) increases the icon's
+ // width by 4 pixels and decreases its height by 4 pixels, giving
+ // final width 32.0 + 4.0 = 36.0 and
+ // final height 32.0 - 4.0 = 28.0
+ expect(iconButton.size, const Size(36.0, 28.0));
+ });
+
testWidgets('test default icon buttons are constrained', (WidgetTester tester) async {
await tester.pumpWidget(
wrap(