diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 218ca95469..3b5b8e9150 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -351,9 +351,19 @@ class ListTile extends StatelessWidget { } TextStyle _titleTextStyle(ThemeData theme, ListTileTheme tileTheme) { - final TextStyle style = tileTheme?.style == ListTileStyle.drawer - ? theme.textTheme.body2 - : theme.textTheme.subhead; + TextStyle style; + if (tileTheme != null) { + switch (tileTheme.style) { + case ListTileStyle.drawer: + style = theme.textTheme.body2; + break; + case ListTileStyle.list: + style = theme.textTheme.subhead; + break; + } + } else { + style = theme.textTheme.subhead; + } final Color color = _textColor(theme, tileTheme, style.color); return _denseLayout(tileTheme) ? style.copyWith(fontSize: 13.0, color: color) @@ -441,11 +451,16 @@ class ListTile extends StatelessWidget { return new InkWell( onTap: enabled ? onTap : null, onLongPress: enabled ? onLongPress : null, - child: new Container( - height: tileHeight, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: new Row(children: children), - ) + child: new ConstrainedBox( + constraints: new BoxConstraints(minHeight: tileHeight), + child: new Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: new UnconstrainedBox( + constrainedAxis: Axis.horizontal, + child: new Row(children: children), + ), + ) + ), ); } } diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index 2641c308e8..12bc2c2831 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -607,10 +607,28 @@ class RenderUnconstrainedBox extends RenderAligningShiftedBox with DebugOverflow RenderUnconstrainedBox({ @required AlignmentGeometry alignment, @required TextDirection textDirection, + Axis constrainedAxis, RenderBox child, }) : assert(alignment != null), - super.mixin(alignment, textDirection, child); + _constrainedAxis = constrainedAxis, + super.mixin(alignment, textDirection, child); + /// The axis to retain constraints on, if any. + /// + /// If not set, or set to null (the default), neither axis will retain its + /// constraints. If set to [Axis.vertical], then vertical constraints will + /// be retained, and if set to [Axis.horizontal], then horizontal constraints + /// will be retained. + Axis get constrainedAxis => _constrainedAxis; + Axis _constrainedAxis; + set constrainedAxis(Axis value) { + assert(value != null); + if (_constrainedAxis == value) + return; + _constrainedAxis = value; + markNeedsLayout(); + } + Rect _overflowContainerRect = Rect.zero; Rect _overflowChildRect = Rect.zero; bool _isOverflowing = false; @@ -618,8 +636,26 @@ class RenderUnconstrainedBox extends RenderAligningShiftedBox with DebugOverflow @override void performLayout() { if (child != null) { - // Let the child lay itself out at it's "natural" size. - child.layout(const BoxConstraints(), parentUsesSize: true); + // Let the child lay itself out at it's "natural" size, but if + // constrainedAxis is non-null, keep any constraints on that axis. + if (constrainedAxis != null) { + switch (constrainedAxis) { + case Axis.horizontal: + child.layout(new BoxConstraints( + maxWidth: constraints.maxWidth, minWidth: constraints.minWidth), + parentUsesSize: true, + ); + break; + case Axis.vertical: + child.layout(new BoxConstraints( + maxHeight: constraints.maxHeight, minHeight: constraints.minHeight), + parentUsesSize: true, + ); + break; + } + } else { + child.layout(const BoxConstraints(), parentUsesSize: true); + } size = constraints.constrain(child.size); alignChild(); final BoxParentData childParentData = child.parentData; diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index a72b4ff052..124f3b2f50 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1644,6 +1644,7 @@ class UnconstrainedBox extends SingleChildRenderObjectWidget { Widget child, this.textDirection, this.alignment: Alignment.center, + this.constrainedAxis, }) : assert(alignment != null), super(key: key, child: child); @@ -1662,23 +1663,34 @@ class UnconstrainedBox extends SingleChildRenderObjectWidget { /// * [AlignmentDirectional] for [Directionality]-aware alignments. final AlignmentGeometry alignment; + /// The axis to retain constraints on, if any. + /// + /// If not set, or set to null (the default), neither axis will retain its + /// constraints. If set to [Axis.vertical], then vertical constraints will + /// be retained, and if set to [Axis.horizontal], then horizontal constraints + /// will be retained. + final Axis constrainedAxis; + @override void updateRenderObject(BuildContext context, covariant RenderUnconstrainedBox renderObject) { renderObject ..textDirection = textDirection ?? Directionality.of(context) - ..alignment = alignment; + ..alignment = alignment + ..constrainedAxis = constrainedAxis; } @override RenderUnconstrainedBox createRenderObject(BuildContext context) => new RenderUnconstrainedBox( textDirection: textDirection ?? Directionality.of(context), alignment: alignment, + constrainedAxis: constrainedAxis, ); @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add(new DiagnosticsProperty('alignment', alignment)); + description.add(new DiagnosticsProperty('constrainedAxis', null)); description.add(new DiagnosticsProperty('textDirection', textDirection, defaultValue: null)); } } diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index b868dd4325..ebf467a25d 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -45,20 +45,25 @@ void main() { testWidgets('ListTile geometry (LTR)', (WidgetTester tester) async { // See https://material.io/guidelines/components/lists.html + final Key leadingKey = new GlobalKey(); + final Key trailingKey = new GlobalKey(); bool hasSubtitle; - Widget buildFrame({ bool dense: false, bool isTwoLine: false, bool isThreeLine: false }) { + Widget buildFrame({ bool dense: false, bool isTwoLine: false, bool isThreeLine: false, double textScaleFactor: 1.0 }) { hasSubtitle = isTwoLine || isThreeLine; return new MaterialApp( - home: new Material( - child: new Center( - child: new ListTile( - leading: const Text('leading'), - title: const Text('title'), - subtitle: hasSubtitle ? const Text('subtitle') : null, - trailing: const Text('trailing'), - dense: dense, - isThreeLine: isThreeLine, + home: new MediaQuery( + data: new MediaQueryData(textScaleFactor: textScaleFactor), + child: new Material( + child: new Center( + child: new ListTile( + leading: new Container(key: leadingKey, width: 24.0, height: 24.0), + title: const Text('title'), + subtitle: hasSubtitle ? const Text('subtitle') : null, + trailing: new Container(key: trailingKey, width: 24.0, height: 24.0), + dense: dense, + isThreeLine: isThreeLine, + ), ), ), ), @@ -66,36 +71,39 @@ void main() { } void testChildren() { - expect(find.text('leading'), findsOneWidget); + expect(find.byKey(leadingKey), findsOneWidget); expect(find.text('title'), findsOneWidget); if (hasSubtitle) expect(find.text('subtitle'), findsOneWidget); - expect(find.text('trailing'), findsOneWidget); + expect(find.byKey(trailingKey), findsOneWidget); } double left(String text) => tester.getTopLeft(find.text(text)).dx; - double right(String text) => tester.getTopRight(find.text(text)).dx; double top(String text) => tester.getTopLeft(find.text(text)).dy; double bottom(String text) => tester.getBottomLeft(find.text(text)).dy; - double width(String text) => tester.getSize(find.text(text)).width; - double height(String text) => tester.getSize(find.text(text)).height; + + double leftKey(Key key) => tester.getTopLeft(find.byKey(key)).dx; + double rightKey(Key key) => tester.getTopRight(find.byKey(key)).dx; + double widthKey(Key key) => tester.getSize(find.byKey(key)).width; + double heightKey(Key key) => tester.getSize(find.byKey(key)).height; + // 16.0 padding to the left and right of the leading and trailing widgets void testHorizontalGeometry() { - expect(left('leading'), 16.0); + expect(leftKey(leadingKey), 16.0); expect(left('title'), 72.0); if (hasSubtitle) expect(left('subtitle'), 72.0); - expect(left('title'), right('leading') + 16.0); - expect(right('trailing'), 800.0 - 16.0); - expect(width('trailing'), 112.0); + expect(left('title'), rightKey(leadingKey) + 32.0); + expect(rightKey(trailingKey), 800.0 - 16.0); + expect(widthKey(trailingKey), 24.0); } void testVerticalGeometry(double expectedHeight) { expect(tester.getSize(find.byType(ListTile)), new Size(800.0, expectedHeight)); if (hasSubtitle) expect(top('subtitle'), bottom('title')); - expect(height('trailing'), 14.0); // Fits on one line (doesn't wrap) + expect(heightKey(trailingKey), 24.0); } await tester.pumpWidget(buildFrame()); @@ -127,8 +135,39 @@ void main() { testChildren(); testHorizontalGeometry(); testVerticalGeometry(76.0); + + await tester.pumpWidget(buildFrame(textScaleFactor: 4.0)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(64.0); + + await tester.pumpWidget(buildFrame(dense: true, textScaleFactor: 4.0)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(64.0); + + await tester.pumpWidget(buildFrame(isTwoLine: true, textScaleFactor: 4.0)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(120.0); + + await tester.pumpWidget(buildFrame(isTwoLine: true, dense: true, textScaleFactor: 4.0)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(120.0); + + await tester.pumpWidget(buildFrame(isThreeLine: true, textScaleFactor: 4.0)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(120.0); + + await tester.pumpWidget(buildFrame(isThreeLine: true, dense: true, textScaleFactor: 4.0)); + testChildren(); + testHorizontalGeometry(); + testVerticalGeometry(120.0); }); + testWidgets('ListTile geometry (RTL)', (WidgetTester tester) async { await tester.pumpWidget(const Directionality( textDirection: TextDirection.rtl, diff --git a/packages/flutter/test/rendering/box_test.dart b/packages/flutter/test/rendering/box_test.dart index 266b7de338..87b31bee56 100644 --- a/packages/flutter/test/rendering/box_test.dart +++ b/packages/flutter/test/rendering/box_test.dart @@ -14,7 +14,8 @@ void main() { decoration: new BoxDecoration( color: const Color(0xFF00FF00), gradient: new RadialGradient( - center: Alignment.topLeft, radius: 1.8, + center: Alignment.topLeft, + radius: 1.8, colors: [Colors.yellow[500], Colors.blue[500]], ), boxShadow: kElevationToShadow[3], @@ -71,14 +72,17 @@ void main() { ); expect(coloredBox, hasAGoodToStringDeep); - expect(coloredBox.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( - 'RenderDecoratedBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n' - ' parentData: MISSING\n' - ' constraints: MISSING\n' - ' size: MISSING\n' - ' decoration: BoxDecoration:\n' - ' \n' - ' configuration: ImageConfiguration()\n')); + expect( + coloredBox.toStringDeep(minLevel: DiagnosticLevel.info), + equalsIgnoringHashCodes( + 'RenderDecoratedBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n' + ' parentData: MISSING\n' + ' constraints: MISSING\n' + ' size: MISSING\n' + ' decoration: BoxDecoration:\n' + ' \n' + ' configuration: ImageConfiguration()\n'), + ); final RenderBox paddingBox = new RenderPadding( padding: const EdgeInsets.all(10.0), @@ -202,4 +206,52 @@ void main() { ' textDirection: ltr\n'), ); }); + + test('honors constrainedAxis=Axis.horizontal', () { + final RenderConstrainedBox flexible = + new RenderConstrainedBox(additionalConstraints: const BoxConstraints.expand(height: 200.0)); + final RenderUnconstrainedBox unconstrained = new RenderUnconstrainedBox( + constrainedAxis: Axis.horizontal, + textDirection: TextDirection.ltr, + child: new RenderFlex( + direction: Axis.horizontal, + textDirection: TextDirection.ltr, + children: [flexible], + ), + alignment: Alignment.center, + ); + final FlexParentData flexParentData = flexible.parentData; + flexParentData.flex = 1; + flexParentData.fit = FlexFit.tight; + + final BoxConstraints viewport = const BoxConstraints(maxWidth: 100.0); + layout(unconstrained, constraints: viewport); + + expect(unconstrained.size.width, equals(100.0), reason: 'constrained width'); + expect(unconstrained.size.height, equals(200.0), reason: 'unconstrained height'); + }); + + test('honors constrainedAxis=Axis.vertical', () { + final RenderConstrainedBox flexible = + new RenderConstrainedBox(additionalConstraints: const BoxConstraints.expand(width: 200.0)); + final RenderUnconstrainedBox unconstrained = new RenderUnconstrainedBox( + constrainedAxis: Axis.vertical, + textDirection: TextDirection.ltr, + child: new RenderFlex( + direction: Axis.vertical, + textDirection: TextDirection.ltr, + children: [flexible], + ), + alignment: Alignment.center, + ); + final FlexParentData flexParentData = flexible.parentData; + flexParentData.flex = 1; + flexParentData.fit = FlexFit.tight; + + final BoxConstraints viewport = const BoxConstraints(maxHeight: 100.0); + layout(unconstrained, constraints: viewport); + + expect(unconstrained.size.width, equals(200.0), reason: 'unconstrained width'); + expect(unconstrained.size.height, equals(100.0), reason: 'constrained height'); + }); }