Unverified Commit 185da9b0 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add Density API to ThemeData, implement for buttons. (#43547)

* Add a density attribute to ThemeData

* Simplify tests

* Review changes (Hans)
parent 1ac17c14
......@@ -52,6 +52,7 @@ class RawMaterialButton extends StatefulWidget {
this.highlightElevation = 8.0,
this.disabledElevation = 0.0,
this.padding = EdgeInsets.zero,
this.visualDensity = const VisualDensity(),
this.constraints = const BoxConstraints(minWidth: 88.0, minHeight: 36.0),
this.shape = const RoundedRectangleBorder(),
this.animationDuration = kThemeChangeDuration,
......@@ -207,6 +208,16 @@ class RawMaterialButton extends StatefulWidget {
/// The internal padding for the button's [child].
final EdgeInsetsGeometry padding;
/// Defines how compact the button's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all widgets
/// within a [Theme].
final VisualDensity visualDensity;
/// Defines the button's size.
///
/// Typically used to constrain the button's minimum size.
......@@ -354,9 +365,22 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
Widget build(BuildContext context) {
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) : null,
minHeight: widget.constraints.minWidth != null ? (widget.constraints.minHeight + densityAdjustment.dy).clamp(0.0, double.infinity) : null,
);
final EdgeInsetsGeometry padding = widget.padding.add(
EdgeInsets.only(
left: densityAdjustment.dx,
top: densityAdjustment.dy,
right: densityAdjustment.dx,
bottom: densityAdjustment.dy,
),
).clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
final Widget result = ConstrainedBox(
constraints: widget.constraints,
constraints: effectiveConstraints,
child: Material(
elevation: _effectiveElevation,
textStyle: widget.textStyle?.copyWith(color: effectiveTextColor),
......@@ -383,7 +407,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
child: IconTheme.merge(
data: IconThemeData(color: effectiveTextColor),
child: Container(
padding: widget.padding,
padding: padding,
child: Center(
widthFactor: 1.0,
heightFactor: 1.0,
......@@ -397,7 +421,12 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
Size minSize;
switch (widget.materialTapTargetSize) {
case MaterialTapTargetSize.padded:
minSize = const Size(48.0, 48.0);
minSize = Size(
kMinInteractiveDimension + densityAdjustment.dx,
kMinInteractiveDimension + densityAdjustment.dy,
);
assert(minSize.width >= 0.0);
assert(minSize.height >= 0.0);
break;
case MaterialTapTargetSize.shrinkWrap:
minSize = Size.zero;
......
......@@ -115,6 +115,7 @@ class FlatButton extends MaterialButton {
Color splashColor,
Brightness colorBrightness,
EdgeInsetsGeometry padding,
VisualDensity visualDensity,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
FocusNode focusNode,
......@@ -139,6 +140,7 @@ class FlatButton extends MaterialButton {
splashColor: splashColor,
colorBrightness: colorBrightness,
padding: padding,
visualDensity: visualDensity,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -199,6 +201,7 @@ class FlatButton extends MaterialButton {
highlightElevation: buttonTheme.getHighlightElevation(this),
disabledElevation: buttonTheme.getDisabledElevation(this),
padding: buttonTheme.getPadding(this),
visualDensity: visualDensity ?? theme.visualDensity,
constraints: buttonTheme.getConstraints(this),
shape: buttonTheme.getShape(this),
clipBehavior: clipBehavior,
......
......@@ -69,6 +69,7 @@ class MaterialButton extends StatelessWidget {
this.highlightElevation,
this.disabledElevation,
this.padding,
this.visualDensity,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -311,6 +312,16 @@ class MaterialButton extends StatelessWidget {
/// [ButtonThemeData.padding].
final EdgeInsetsGeometry padding;
/// Defines how compact the button's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [density] for all widgets
/// within a [Theme].
final VisualDensity visualDensity;
/// The shape of the button's [Material].
///
/// The button's highlight and splash are clipped to this shape. If the
......@@ -387,6 +398,7 @@ class MaterialButton extends StatelessWidget {
hoverElevation: buttonTheme.getHoverElevation(this),
highlightElevation: buttonTheme.getHighlightElevation(this),
padding: buttonTheme.getPadding(this),
visualDensity: visualDensity ?? theme.visualDensity,
constraints: buttonTheme.getConstraints(this).copyWith(
minWidth: minWidth,
minHeight: height,
......@@ -416,6 +428,7 @@ class MaterialButton extends StatelessWidget {
properties.add(ColorProperty('splashColor', splashColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
......
......@@ -11,6 +11,7 @@ import 'material_button.dart';
import 'material_state.dart';
import 'raised_button.dart';
import 'theme.dart';
import 'theme_data.dart';
// The total time to make the button's fill color opaque and change
// its elevation. Only applies when highlightElevation > 0.0.
......@@ -75,6 +76,7 @@ class OutlineButton extends MaterialButton {
this.disabledBorderColor,
this.highlightedBorderColor,
EdgeInsetsGeometry padding,
VisualDensity visualDensity,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
FocusNode focusNode,
......@@ -97,6 +99,7 @@ class OutlineButton extends MaterialButton {
splashColor: splashColor,
highlightElevation: highlightElevation,
padding: padding,
visualDensity: visualDensity,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -129,6 +132,7 @@ class OutlineButton extends MaterialButton {
Color disabledBorderColor,
BorderSide borderSide,
EdgeInsetsGeometry padding,
VisualDensity visualDensity,
ShapeBorder shape,
Clip clipBehavior,
FocusNode focusNode,
......@@ -187,6 +191,7 @@ class OutlineButton extends MaterialButton {
disabledBorderColor: disabledBorderColor,
highlightedBorderColor: highlightedBorderColor ?? buttonTheme.colorScheme.primary,
padding: buttonTheme.getPadding(this),
visualDensity: visualDensity,
shape: buttonTheme.getShape(this),
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -225,6 +230,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
Color disabledBorderColor,
BorderSide borderSide,
EdgeInsetsGeometry padding,
VisualDensity visualDensity,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
FocusNode focusNode,
......@@ -253,6 +259,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
highlightedBorderColor: highlightedBorderColor,
borderSide: borderSide,
padding: padding,
visualDensity: visualDensity,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -287,6 +294,7 @@ class _OutlineButton extends StatefulWidget {
this.disabledBorderColor,
@required this.highlightedBorderColor,
this.padding,
this.visualDensity,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -314,6 +322,7 @@ class _OutlineButton extends StatefulWidget {
final Color disabledBorderColor;
final Color highlightedBorderColor;
final EdgeInsetsGeometry padding;
final VisualDensity visualDensity;
final ShapeBorder shape;
final Clip clipBehavior;
final FocusNode focusNode;
......@@ -435,6 +444,8 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
......@@ -456,6 +467,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
highlightElevation: _getHighlightElevation(),
onHighlightChanged: _handleHighlightChanged,
padding: widget.padding,
visualDensity: widget.visualDensity ?? theme.visualDensity,
shape: _OutlineBorder(
shape: widget.shape,
side: _getOutline(),
......
......@@ -126,6 +126,7 @@ class RaisedButton extends MaterialButton {
double highlightElevation,
double disabledElevation,
EdgeInsetsGeometry padding,
VisualDensity visualDensity,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
FocusNode focusNode,
......@@ -161,6 +162,7 @@ class RaisedButton extends MaterialButton {
highlightElevation: highlightElevation,
disabledElevation: disabledElevation,
padding: padding,
visualDensity: visualDensity,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -227,6 +229,7 @@ class RaisedButton extends MaterialButton {
highlightElevation: buttonTheme.getHighlightElevation(this),
disabledElevation: buttonTheme.getDisabledElevation(this),
padding: buttonTheme.getPadding(this),
visualDensity: visualDensity ?? theme.visualDensity,
constraints: buttonTheme.getConstraints(this),
shape: buttonTheme.getShape(this),
focusNode: focusNode,
......
......@@ -34,6 +34,18 @@ abstract class EdgeInsetsGeometry {
double get _start;
double get _top;
/// An [EdgeInsetsGeometry] with infinite offsets in each direction.
///
/// Can be used as an infinite upper bound for [clamp].
static const EdgeInsetsGeometry infinity = _MixedEdgeInsets.fromLRSETB(
double.infinity,
double.infinity,
double.infinity,
double.infinity,
double.infinity,
double.infinity,
);
/// Whether every dimension is non-negative.
bool get isNonNegative {
return _left >= 0.0
......@@ -146,6 +158,19 @@ abstract class EdgeInsetsGeometry {
);
}
/// Returns the a new [EdgeInsetsGeometry] object with all values greater than
/// or equal to `min`, and less than or equal to `max`.
EdgeInsetsGeometry clamp(EdgeInsetsGeometry min, EdgeInsetsGeometry max) {
return _MixedEdgeInsets.fromLRSETB(
_left.clamp(min._left, max._left),
_right.clamp(min._right, max._right),
_start.clamp(min._start, max._start),
_end.clamp(min._end, max._end),
_top.clamp(min._top, max._top),
_bottom.clamp(min._bottom, max._bottom),
);
}
/// Returns the [EdgeInsetsGeometry] object with each dimension negated.
///
/// This is the same as multiplying the object by -1.0.
......
......@@ -690,6 +690,53 @@ void main() {
await tester.longPress(flatButton);
expect(didLongPressButton, isTrue);
});
testWidgets('FlatButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: FlatButton(
visualDensity: visualDensity,
key: key,
onPressed: () {},
child: useText ? const Text('Text') : Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(132, 100)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(156, 124)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(108, 100)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(88, 48)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(112, 60)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(76, 36)));
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
......@@ -728,4 +728,51 @@ void main() {
);
expect(tester.widget<Material>(rawButtonMaterial).shape, const StadiumBorder());
});
testWidgets('MaterialButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: MaterialButton(
visualDensity: visualDensity,
key: key,
onPressed: () {},
child: useText ? const Text('Text') : Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(132, 100)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(156, 124)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(108, 100)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(88, 48)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(112, 60)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(76, 36)));
});
}
......@@ -978,6 +978,53 @@ void main() {
await tester.longPress(outlineButton);
expect(didLongPressButton, isTrue);
});
testWidgets('OutlineButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: OutlineButton(
visualDensity: visualDensity,
key: key,
onPressed: () {},
child: useText ? const Text('Text') : Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(132, 100)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(156, 124)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(108, 100)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(88, 48)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(112, 60)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(76, 36)));
});
}
PhysicalModelLayer _findPhysicalLayer(Element element) {
......
......@@ -557,6 +557,53 @@ void main() {
paintsExactlyCountTimes(#clipPath, 0),
);
});
testWidgets('RaisedButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: RaisedButton(
visualDensity: visualDensity,
key: key,
onPressed: () {},
child: useText ? const Text('Text') : Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(132, 100)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(156, 124)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(108, 100)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(88, 48)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(112, 60)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(76, 36)));
});
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
......
......@@ -286,7 +286,7 @@ void main() {
expect(find.byKey(key).hitTestable(), findsOneWidget);
});
testWidgets('$RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async {
testWidgets('RawMaterialButton can be expanded by parent constraints', (WidgetTester tester) async {
const Key key = Key('test');
await tester.pumpWidget(
MaterialApp(
......@@ -306,7 +306,7 @@ void main() {
expect(tester.getSize(find.byKey(key)), const Size(800.0, 48.0));
});
testWidgets('$RawMaterialButton handles focus', (WidgetTester tester) async {
testWidgets('RawMaterialButton handles focus', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Button Focus');
const Key key = Key('test');
const Color focusColor = Color(0xff00ff00);
......@@ -334,7 +334,7 @@ void main() {
expect(box, paints..rect(color: focusColor));
});
testWidgets('$RawMaterialButton loses focus when disabled.', (WidgetTester tester) async {
testWidgets('RawMaterialButton loses focus when disabled.', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'RawMaterialButton');
await tester.pumpWidget(
MaterialApp(
......@@ -368,7 +368,7 @@ void main() {
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets("Disabled $RawMaterialButton can't be traversed to when disabled.", (WidgetTester tester) async {
testWidgets("Disabled RawMaterialButton can't be traversed to when disabled.", (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: '$RawMaterialButton 1');
final FocusNode focusNode2 = FocusNode(debugLabel: '$RawMaterialButton 2');
......@@ -406,7 +406,7 @@ void main() {
expect(focusNode2.hasPrimaryFocus, isFalse);
});
testWidgets('$RawMaterialButton handles hover', (WidgetTester tester) async {
testWidgets('RawMaterialButton handles hover', (WidgetTester tester) async {
const Key key = Key('test');
const Color hoverColor = Color(0xff00ff00);
......@@ -510,4 +510,51 @@ void main() {
await tester.longPress(rawMaterialButton);
expect(didLongPressButton, isTrue);
});
testWidgets('RawMaterialButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test');
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
return await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: RawMaterialButton(
visualDensity: visualDensity,
key: key,
onPressed: () {},
child: useText ? const Text('Text') : Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
),
);
}
await buildTest(const VisualDensity());
final RenderBox box = tester.renderObject(find.byKey(key));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(100, 100)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(124, 124)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle();
expect(box.size, equals(const Size(100, 100)));
await buildTest(const VisualDensity(), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(88, 48)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(100, 60)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
await tester.pumpAndSettle();
expect(box.size, equals(const Size(76, 36)));
});
}
......@@ -201,6 +201,7 @@ void main() {
final ThemeData theme = ThemeData.raw(
brightness: Brightness.dark,
visualDensity: const VisualDensity(),
primaryColor: Colors.black,
primaryColorBrightness: Brightness.dark,
primaryColorLight: Colors.black,
......@@ -279,6 +280,7 @@ void main() {
final ThemeData otherTheme = ThemeData.raw(
brightness: Brightness.light,
visualDensity: const VisualDensity(),
primaryColor: Colors.white,
primaryColorBrightness: Brightness.light,
primaryColorLight: Colors.white,
......
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