From c88320458ea86da379aec43dcd24057ad1d79652 Mon Sep 17 00:00:00 2001 From: Tianguang Date: Wed, 15 Jan 2020 11:38:02 -0500 Subject: [PATCH] Allow IconButton to have smaller sizes (#47457) --- packages/flutter/lib/src/material/button.dart | 5 +- .../flutter/lib/src/material/icon_button.dart | 32 +++++++++- .../flutter/lib/src/material/theme_data.dart | 10 +++ .../test/material/icon_button_test.dart | 63 +++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart index 8862793c61..43e8730ca1 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -366,10 +366,7 @@ class _RawMaterialButtonState extends State { final Color effectiveTextColor = MaterialStateProperty.resolveAs(widget.textStyle?.color, _states); final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs(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 6b3ce2fa69..2d510da0a4 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -154,6 +154,7 @@ class IconButton extends StatelessWidget { this.autofocus = false, this.tooltip, this.enableFeedback = true, + this.constraints, }) : assert(iconSize != null), assert(padding != null), assert(alignment != null), @@ -288,6 +289,26 @@ class IconButton extends StatelessWidget { /// * [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 @@ class IconButton extends StatelessWidget { 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 5281031041..b81f743141 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -1815,6 +1815,16 @@ class VisualDensity extends Diagnosticable { ); } + /// 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 a93f923db0..094c9501a2 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -75,6 +75,69 @@ void main() { 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(