Text field height attempt 2 (#29250)

Adds the `minLines` and `expands` parameters for controlling text height.  The original PR was reverted, so this one contains a few extra fixes for the tests that were broken.
diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart
index 98b26f3..4f2a3b4 100644
--- a/packages/flutter/lib/src/cupertino/text_field.dart
+++ b/packages/flutter/lib/src/cupertino/text_field.dart
@@ -140,6 +140,8 @@
   ///
   /// See also:
   ///
+  ///  * [minLines]
+  ///  * [expands], to allow the widget to size itself to its parent's height.
   ///  * [maxLength], which discusses the precise meaning of "number of
   ///    characters" and how it may differ from the intuitive meaning.
   const CupertinoTextField({
@@ -164,6 +166,8 @@
     this.obscureText = false,
     this.autocorrect = true,
     this.maxLines = 1,
+    this.minLines,
+    this.expands = false,
     this.maxLength,
     this.maxLengthEnforced = true,
     this.onChanged,
@@ -183,6 +187,16 @@
        assert(maxLengthEnforced != null),
        assert(scrollPadding != null),
        assert(maxLines == null || maxLines > 0),
+       assert(minLines == null || minLines > 0),
+       assert(
+         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
+         'minLines can\'t be greater than maxLines',
+       ),
+       assert(expands != null),
+       assert(
+         !expands || (maxLines == null && minLines == null),
+         'minLines and maxLines must be null when expands is true.',
+       ),
        assert(maxLength == null || maxLength > 0),
        assert(clearButtonMode != null),
        assert(prefixMode != null),
@@ -290,6 +304,12 @@
   /// {@macro flutter.widgets.editableText.maxLines}
   final int maxLines;
 
+  /// {@macro flutter.widgets.editableText.minLines}
+  final int minLines;
+
+  /// {@macro flutter.widgets.editableText.expands}
+  final bool expands;
+
   /// The maximum number of characters (Unicode scalar values) to allow in the
   /// text field.
   ///
@@ -405,6 +425,8 @@
     properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
     properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: false));
     properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
+    properties.add(IntProperty('minLines', minLines, defaultValue: null));
+    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
     properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
     properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced'));
     properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null));
@@ -662,6 +684,8 @@
           obscureText: widget.obscureText,
           autocorrect: widget.autocorrect,
           maxLines: widget.maxLines,
+          minLines: widget.minLines,
+          expands: widget.expands,
           selectionColor: _kSelectionHighlightColor,
           selectionControls: cupertinoTextSelectionControls,
           onChanged: widget.onChanged,
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index ca11f11..a561a48 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -552,14 +552,18 @@
     @required TextDirection textDirection,
     @required TextBaseline textBaseline,
     @required bool isFocused,
+    @required bool expands,
   }) : assert(decoration != null),
        assert(textDirection != null),
        assert(textBaseline != null),
+       assert(expands != null),
        _decoration = decoration,
        _textDirection = textDirection,
        _textBaseline = textBaseline,
-       _isFocused = isFocused;
+       _isFocused = isFocused,
+       _expands = expands;
 
+  static const double subtextGap = 8.0;
   final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{};
   final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{};
 
@@ -709,6 +713,16 @@
     markNeedsSemanticsUpdate();
   }
 
+  bool get expands => _expands;
+  bool _expands = false;
+  set expands(bool value) {
+    assert(value != null);
+    if (_expands == value)
+      return;
+    _expands = value;
+    markNeedsLayout();
+  }
+
   @override
   void attach(PipelineOwner owner) {
     super.attach(owner);
@@ -804,34 +818,31 @@
 
   EdgeInsets get contentPadding => decoration.contentPadding;
 
-  // Returns a value used by performLayout to position all
-  // of the renderers. This method applies layout to all of the renderers
-  // except the container. For convenience, the container is laid out
-  // in performLayout().
-  _RenderDecorationLayout _layout(BoxConstraints layoutConstraints) {
-    final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{};
-    BoxConstraints boxConstraints = layoutConstraints.loosen();
-    double aboveBaseline = 0.0;
-    double belowBaseline = 0.0;
-    void layoutLineBox(RenderBox box) {
-      if (box == null)
-        return;
-      box.layout(boxConstraints, parentUsesSize: true);
-      final double baseline = box.getDistanceToBaseline(textBaseline);
-      assert(baseline != null && baseline >= 0.0);
-      boxToBaseline[box] = baseline;
-      aboveBaseline = math.max(baseline, aboveBaseline);
-      belowBaseline = math.max(box.size.height - baseline, belowBaseline);
+  // Lay out the given box if needed, and return its baseline
+  double _layoutLineBox(RenderBox box, BoxConstraints constraints) {
+    if (box == null) {
+      return 0.0;
     }
-    layoutLineBox(prefix);
-    layoutLineBox(suffix);
+    box.layout(constraints, parentUsesSize: true);
+    final double baseline = box.getDistanceToBaseline(textBaseline);
+    assert(baseline != null && baseline >= 0.0);
+    return baseline;
+  }
 
-    if (icon != null)
-      icon.layout(boxConstraints, parentUsesSize: true);
-    if (prefixIcon != null)
-      prefixIcon.layout(boxConstraints, parentUsesSize: true);
-    if (suffixIcon != null)
-      suffixIcon.layout(boxConstraints, parentUsesSize: true);
+  // Returns a value used by performLayout to position all of the renderers.
+  // This method applies layout to all of the renderers except the container.
+  // For convenience, the container is laid out in performLayout().
+  _RenderDecorationLayout _layout(BoxConstraints layoutConstraints) {
+    // Margin on each side of subtext (counter and helperError)
+    final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{};
+    final BoxConstraints boxConstraints = layoutConstraints.loosen();
+
+    // Layout all the widgets used by InputDecorator
+    boxToBaseline[prefix] = _layoutLineBox(prefix, boxConstraints);
+    boxToBaseline[suffix] = _layoutLineBox(suffix, boxConstraints);
+    boxToBaseline[icon] = _layoutLineBox(icon, boxConstraints);
+    boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, boxConstraints);
+    boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, boxConstraints);
 
     final double inputWidth = math.max(0.0, constraints.maxWidth - (
       _boxSize(icon).width
@@ -841,72 +852,144 @@
       + _boxSize(suffix).width
       + _boxSize(suffixIcon).width
       + contentPadding.right));
+    boxToBaseline[label] = _layoutLineBox(
+      label,
+      boxConstraints.copyWith(maxWidth: inputWidth),
+    );
+    boxToBaseline[hint] = _layoutLineBox(
+      hint,
+      boxConstraints.copyWith(minWidth: inputWidth, maxWidth: inputWidth),
+    );
+    boxToBaseline[counter] = _layoutLineBox(counter, boxConstraints);
 
-    boxConstraints = boxConstraints.copyWith(maxWidth: inputWidth);
-    if (label != null) {
-      if (decoration.alignLabelWithHint) {
-        // The label is aligned with the hint, at the baseline
-        layoutLineBox(label);
-      } else {
-        // The label is centered, not baseline aligned
-        label.layout(boxConstraints, parentUsesSize: true);
-      }
-    }
-
-    boxConstraints = boxConstraints.copyWith(minWidth: inputWidth);
-    layoutLineBox(hint);
-    layoutLineBox(input);
-
-    double inputBaseline = contentPadding.top + aboveBaseline;
-    double containerHeight = contentPadding.top
-      + aboveBaseline
-      + belowBaseline
-      + contentPadding.bottom;
-
-    if (label != null) {
-      // floatingLabelHeight includes the vertical gap between the inline
-      // elements and the floating label.
-      containerHeight += decoration.floatingLabelHeight;
-      inputBaseline += decoration.floatingLabelHeight;
-    }
-
-    containerHeight = math.max(
-      containerHeight,
-      math.max(
-        _boxSize(suffixIcon).height,
-        _boxSize(prefixIcon).height));
-
-    // Inline text within an outline border is centered within the container
-    // less 2.0 dps at the top to account for the vertical space occupied
-    // by the floating label.
-    final double outlineBaseline = aboveBaseline +
-      (containerHeight - (2.0 + aboveBaseline + belowBaseline)) / 2.0;
-
-    double subtextBaseline = 0.0;
-    double subtextHeight = 0.0;
-    if (helperError != null || counter != null) {
-      boxConstraints = layoutConstraints.loosen();
-      aboveBaseline = 0.0;
-      belowBaseline = 0.0;
-      layoutLineBox(counter);
-
-      // The helper or error text can occupy the full width less the space
-      // occupied by the icon and counter.
-      boxConstraints = boxConstraints.copyWith(
+    // The helper or error text can occupy the full width less the space
+    // occupied by the icon and counter.
+    boxToBaseline[helperError] = _layoutLineBox(
+      helperError,
+      boxConstraints.copyWith(
         maxWidth: math.max(0.0, boxConstraints.maxWidth
           - _boxSize(icon).width
           - _boxSize(counter).width
           - contentPadding.horizontal,
         ),
-      );
-      layoutLineBox(helperError);
+      ),
+    );
 
-      if (aboveBaseline + belowBaseline > 0.0) {
-        const double subtextGap = 8.0;
-        subtextBaseline = containerHeight + subtextGap + aboveBaseline;
-        subtextHeight = subtextGap + aboveBaseline + belowBaseline;
-      }
+    // The height of the input needs to accommodate label above and counter and
+    // helperError below, when they exist.
+    final double labelHeight = label == null
+      ? 0
+      : decoration.floatingLabelHeight;
+    final double topHeight = decoration.border.isOutline
+      ? math.max(labelHeight - boxToBaseline[label], 0)
+      : labelHeight;
+    final double counterHeight = counter == null
+      ? 0
+      : boxToBaseline[counter] + subtextGap;
+    final bool helperErrorExists = helperError?.size != null
+        && helperError.size.height > 0;
+    final double helperErrorHeight = !helperErrorExists
+      ? 0
+      : helperError.size.height + subtextGap;
+    final double bottomHeight = math.max(
+      counterHeight,
+      helperErrorHeight,
+    );
+    boxToBaseline[input] = _layoutLineBox(
+      input,
+      boxConstraints.deflate(EdgeInsets.only(
+        top: contentPadding.top + topHeight,
+        bottom: contentPadding.bottom + bottomHeight,
+      )).copyWith(
+        minWidth: inputWidth,
+        maxWidth: inputWidth,
+      ),
+    );
+
+    // The field can be occupied by a hint or by the input itself
+    final double hintHeight = hint == null ? 0 : hint.size.height;
+    final double inputDirectHeight = input == null ? 0 : input.size.height;
+    final double inputHeight = math.max(hintHeight, inputDirectHeight);
+    final double inputInternalBaseline = math.max(
+      boxToBaseline[input],
+      boxToBaseline[hint],
+    );
+
+    // Calculate the amount that prefix/suffix affects height above and below
+    // the input.
+    final double prefixHeight = prefix == null ? 0 : prefix.size.height;
+    final double suffixHeight = suffix == null ? 0 : suffix.size.height;
+    final double fixHeight = math.max(
+      boxToBaseline[prefix],
+      boxToBaseline[suffix],
+    );
+    final double fixAboveInput = math.max(0, fixHeight - inputInternalBaseline);
+    final double fixBelowBaseline = math.max(
+      prefixHeight - boxToBaseline[prefix],
+      suffixHeight - boxToBaseline[suffix],
+    );
+    final double fixBelowInput = math.max(
+      0,
+      fixBelowBaseline - (inputHeight - inputInternalBaseline),
+    );
+
+    // Calculate the height of the input text container.
+    final double prefixIconHeight = prefixIcon == null ? 0 : prefixIcon.size.height;
+    final double suffixIconHeight = suffixIcon == null ? 0 : suffixIcon.size.height;
+    final double fixIconHeight = math.max(prefixIconHeight, suffixIconHeight);
+    final double contentHeight = math.max(
+      fixIconHeight,
+      topHeight
+      + contentPadding.top
+      + fixAboveInput
+      + inputHeight
+      + fixBelowInput
+      + contentPadding.bottom,
+    );
+    final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight;
+    final double containerHeight = expands
+      ? maxContainerHeight
+      : math.min(contentHeight, maxContainerHeight);
+
+    // Always position the prefix/suffix in the same place (baseline).
+    final double overflow = math.max(0, contentHeight - maxContainerHeight);
+    final double baselineAdjustment = fixAboveInput - overflow;
+
+    // The baselines that will be used to draw the actual input text content.
+    final double inputBaseline = contentPadding.top
+      + topHeight
+      + inputInternalBaseline
+      + baselineAdjustment;
+    // The text in the input when an outline border is present is centered
+    // within the container less 2.0 dps at the top to account for the vertical
+    // space occupied by the floating label.
+    final double outlineBaseline = inputInternalBaseline
+      + baselineAdjustment / 2
+      + (containerHeight - (2.0 + inputHeight)) / 2.0;
+
+    // Find the positions of the text below the input when it exists.
+    double subtextCounterBaseline = 0;
+    double subtextHelperBaseline = 0;
+    double subtextCounterHeight = 0;
+    double subtextHelperHeight = 0;
+    if (counter != null) {
+      subtextCounterBaseline =
+        containerHeight + subtextGap + boxToBaseline[counter];
+      subtextCounterHeight = counter.size.height + subtextGap;
     }
+    if (helperErrorExists) {
+      subtextHelperBaseline =
+        containerHeight + subtextGap + boxToBaseline[helperError];
+      subtextHelperHeight = helperErrorHeight;
+    }
+    final double subtextBaseline = math.max(
+      subtextCounterBaseline,
+      subtextHelperBaseline,
+    );
+    final double subtextHeight = math.max(
+      subtextCounterHeight,
+      subtextHelperHeight,
+    );
 
     return _RenderDecorationLayout(
       boxToBaseline: boxToBaseline,
@@ -959,7 +1042,7 @@
   double computeMinIntrinsicHeight(double width) {
     double subtextHeight = _lineHeight(width, <RenderBox>[helperError, counter]);
     if (subtextHeight > 0.0)
-      subtextHeight += 8.0;
+      subtextHeight += subtextGap;
     return contentPadding.top
       + (label == null ? 0.0 : decoration.floatingLabelHeight)
       + _lineHeight(width, <RenderBox>[prefix, input, suffix])
@@ -1370,15 +1453,18 @@
     @required this.textDirection,
     @required this.textBaseline,
     @required this.isFocused,
+    @required this.expands,
   }) : assert(decoration != null),
        assert(textDirection != null),
        assert(textBaseline != null),
+       assert(expands != null),
        super(key: key);
 
   final _Decoration decoration;
   final TextDirection textDirection;
   final TextBaseline textBaseline;
   final bool isFocused;
+  final bool expands;
 
   @override
   _RenderDecorationElement createElement() => _RenderDecorationElement(this);
@@ -1390,6 +1476,7 @@
       textDirection: textDirection,
       textBaseline: textBaseline,
       isFocused: isFocused,
+      expands: expands,
     );
   }
 
@@ -1399,6 +1486,7 @@
      ..decoration = decoration
      ..textDirection = textDirection
      ..textBaseline = textBaseline
+     ..expands = expands
      ..isFocused = isFocused;
   }
 }
@@ -1461,6 +1549,7 @@
     this.baseStyle,
     this.textAlign,
     this.isFocused = false,
+    this.expands = false,
     this.isEmpty = false,
     this.child,
   }) : assert(isFocused != null),
@@ -1495,6 +1584,19 @@
   /// Defaults to false.
   final bool isFocused;
 
+  /// If true, the height of the input field will be as large as possible.
+  ///
+  /// If wrapped in a widget that constrains its child's height, like Expanded
+  /// or SizedBox, the input field will only be affected if [expands] is set to
+  /// true.
+  ///
+  /// See [TextField.minLines] and [TextField.maxLines] for related ways to
+  /// affect the height of an input. When [expands] is true, both must be null
+  /// in order to avoid ambiguity in determining the height.
+  ///
+  /// Defaults to false.
+  final bool expands;
+
   /// Whether the input field is empty.
   ///
   /// Determines the position of the label text and whether to display the hint
@@ -1533,6 +1635,7 @@
     properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration));
     properties.add(DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null));
     properties.add(DiagnosticsProperty<bool>('isFocused', isFocused));
+    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
     properties.add(DiagnosticsProperty<bool>('isEmpty', isEmpty));
   }
 }
@@ -1928,6 +2031,7 @@
       textDirection: textDirection,
       textBaseline: textBaseline,
       isFocused: isFocused,
+      expands: widget.expands,
     );
   }
 }
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index 9b8b141..6399488 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -147,6 +147,8 @@
     this.obscureText = false,
     this.autocorrect = true,
     this.maxLines = 1,
+    this.minLines,
+    this.expands = false,
     this.maxLength,
     this.maxLengthEnforced = true,
     this.onChanged,
@@ -171,6 +173,16 @@
        assert(scrollPadding != null),
        assert(dragStartBehavior != null),
        assert(maxLines == null || maxLines > 0),
+       assert(minLines == null || minLines > 0),
+       assert(
+         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
+         'minLines can\'t be greater than maxLines',
+       ),
+       assert(expands != null),
+       assert(
+         !expands || (maxLines == null && minLines == null),
+         'minLines and maxLines must be null when expands is true.',
+       ),
        assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
        keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
        super(key: key);
@@ -269,6 +281,12 @@
   /// {@macro flutter.widgets.editableText.maxLines}
   final int maxLines;
 
+  /// {@macro flutter.widgets.editableText.minLines}
+  final int minLines;
+
+  /// {@macro flutter.widgets.editableText.expands}
+  final bool expands;
+
   /// If [maxLength] is set to this value, only the "current input length"
   /// part of the character counter is shown.
   static const int noMaxLength = -1;
@@ -457,6 +475,8 @@
     properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
     properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
     properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
+    properties.add(IntProperty('minLines', minLines, defaultValue: null));
+    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
     properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
     properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
     properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
@@ -851,6 +871,8 @@
         obscureText: widget.obscureText,
         autocorrect: widget.autocorrect,
         maxLines: widget.maxLines,
+        minLines: widget.minLines,
+        expands: widget.expands,
         selectionColor: themeData.textSelectionColor,
         selectionControls: widget.selectionEnabled ? textSelectionControls : null,
         onChanged: widget.onChanged,
@@ -883,6 +905,7 @@
             textAlign: widget.textAlign,
             isFocused: focusNode.hasFocus,
             isEmpty: controller.value.text.isEmpty,
+            expands: widget.expands,
             child: child,
           );
         },
diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart
index 3db75f9..1362e51 100644
--- a/packages/flutter/lib/src/material/text_form_field.dart
+++ b/packages/flutter/lib/src/material/text_form_field.dart
@@ -89,6 +89,8 @@
     bool autovalidate = false,
     bool maxLengthEnforced = true,
     int maxLines = 1,
+    int minLines,
+    bool expands = false,
     int maxLength,
     VoidCallback onEditingComplete,
     ValueChanged<String> onFieldSubmitted,
@@ -112,6 +114,16 @@
        assert(maxLengthEnforced != null),
        assert(scrollPadding != null),
        assert(maxLines == null || maxLines > 0),
+       assert(minLines == null || minLines > 0),
+       assert(
+         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
+         'minLines can\'t be greater than maxLines',
+       ),
+       assert(expands != null),
+       assert(
+         !expands || (maxLines == null && minLines == null),
+         'minLines and maxLines must be null when expands is true.',
+       ),
        assert(maxLength == null || maxLength > 0),
        assert(enableInteractiveSelection != null),
        super(
@@ -141,6 +153,8 @@
         autocorrect: autocorrect,
         maxLengthEnforced: maxLengthEnforced,
         maxLines: maxLines,
+        minLines: minLines,
+        expands: expands,
         maxLength: maxLength,
         onChanged: field.didChange,
         onEditingComplete: onEditingComplete,
diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart
index eb4d494..283d734 100644
--- a/packages/flutter/lib/src/rendering/box.dart
+++ b/packages/flutter/lib/src/rendering/box.dart
@@ -1801,9 +1801,9 @@
         testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', double.infinity);
         testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', double.infinity);
         if (constraints.hasBoundedWidth)
-          testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', constraints.maxWidth);
+          testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', constraints.maxHeight);
         if (constraints.hasBoundedHeight)
-          testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', constraints.maxHeight);
+          testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', constraints.maxWidth);
 
         // TODO(ianh): Test that values are internally consistent in more ways than the above.
 
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 9ad43f0..2485239 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -144,6 +144,8 @@
     ValueNotifier<bool> showCursor,
     bool hasFocus,
     int maxLines = 1,
+    int minLines,
+    bool expands = false,
     StrutStyle strutStyle,
     Color selectionColor,
     double textScaleFactor = 1.0,
@@ -165,6 +167,16 @@
   }) : assert(textAlign != null),
        assert(textDirection != null, 'RenderEditable created without a textDirection.'),
        assert(maxLines == null || maxLines > 0),
+       assert(minLines == null || minLines > 0),
+       assert(
+         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
+         'minLines can\'t be greater than maxLines',
+       ),
+       assert(expands != null),
+       assert(
+         !expands || (maxLines == null && minLines == null),
+         'minLines and maxLines must be null when expands is true.',
+       ),
        assert(textScaleFactor != null),
        assert(offset != null),
        assert(ignorePointer != null),
@@ -186,6 +198,8 @@
        _showCursor = showCursor ?? ValueNotifier<bool>(false),
        _hasFocus = hasFocus ?? false,
        _maxLines = maxLines,
+       _minLines = minLines,
+       _expands = expands,
        _selectionColor = selectionColor,
        _selection = selection,
        _offset = offset,
@@ -735,6 +749,29 @@
     markNeedsTextLayout();
   }
 
+  /// {@macro flutter.widgets.editableText.minLines}
+  int get minLines => _minLines;
+  int _minLines;
+  /// The value may be null. If it is not null, then it must be greater than zero.
+  set minLines(int value) {
+    assert(value == null || value > 0);
+    if (minLines == value)
+      return;
+    _minLines = value;
+    markNeedsTextLayout();
+  }
+
+  /// {@macro flutter.widgets.editableText.expands}
+  bool get expands => _expands;
+  bool _expands;
+  set expands(bool value) {
+    assert(value != null);
+    if (expands == value)
+      return;
+    _expands = value;
+    markNeedsTextLayout();
+  }
+
   /// The color to use when painting the selection.
   Color get selectionColor => _selectionColor;
   Color _selectionColor;
@@ -1194,8 +1231,28 @@
   double get preferredLineHeight => _textPainter.preferredLineHeight;
 
   double _preferredHeight(double width) {
-    if (maxLines != null)
+    // Lock height to maxLines if needed
+    final bool lockedMax = maxLines != null && minLines == null;
+    final bool lockedBoth = minLines != null && minLines == maxLines;
+    final bool singleLine = maxLines == 1;
+    if (singleLine || lockedMax || lockedBoth) {
       return preferredLineHeight * maxLines;
+    }
+
+    // Clamp height to minLines or maxLines if needed
+    final bool minLimited = minLines != null && minLines > 1;
+    final bool maxLimited = maxLines != null;
+    if (minLimited || maxLimited) {
+      _layoutText(width);
+      if (minLimited && _textPainter.height < preferredLineHeight * minLines) {
+        return preferredLineHeight * minLines;
+      }
+      if (maxLimited && _textPainter.height > preferredLineHeight * maxLines) {
+        return preferredLineHeight * maxLines;
+      }
+    }
+
+    // Set the height based on the content
     if (width == double.infinity) {
       final String text = _textPainter.text.toPlainText();
       int lines = 1;
@@ -1658,6 +1715,8 @@
     properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor));
     properties.add(DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor));
     properties.add(IntProperty('maxLines', maxLines));
+    properties.add(IntProperty('minLines', minLines));
+    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
     properties.add(DiagnosticsProperty<Color>('selectionColor', selectionColor));
     properties.add(DoubleProperty('textScaleFactor', textScaleFactor));
     properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index 1012034..d6a0b04 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -278,6 +278,8 @@
     this.locale,
     this.textScaleFactor,
     this.maxLines = 1,
+    this.minLines,
+    this.expands = false,
     this.autofocus = false,
     this.selectionColor,
     this.selectionControls,
@@ -310,6 +312,16 @@
        assert(backgroundCursorColor != null),
        assert(textAlign != null),
        assert(maxLines == null || maxLines > 0),
+       assert(minLines == null || minLines > 0),
+       assert(
+         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
+         'minLines can\'t be greater than maxLines',
+       ),
+       assert(expands != null),
+       assert(
+         !expands || (maxLines == null && minLines == null),
+         'minLines and maxLines must be null when expands is true.',
+       ),
        assert(autofocus != null),
        assert(rendererIgnoresPointer != null),
        assert(scrollPadding != null),
@@ -465,12 +477,78 @@
   /// container will start with enough vertical space for one line and
   /// automatically grow to accommodate additional lines as they are entered.
   ///
-  /// If it is not null, the value must be greater than zero. If it is greater
-  /// than 1, it will take up enough horizontal space to accommodate that number
-  /// of lines.
+  /// If this is not null, the value must be greater than zero, and it will lock
+  /// the input to the given number of lines and take up enough horizontal space
+  /// to accommodate that number of lines. Setting [minLines] as well allows the
+  /// input to grow between the indicated range.
+  ///
+  /// The full set of behaviors possible with [minLines] and [maxLines] are as
+  /// follows. These examples apply equally to `TextField`, `TextFormField`, and
+  /// `EditableText`.
+  ///
+  /// Input that occupies a single line and scrolls horizontally as needed.
+  /// ```dart
+  /// TextField()
+  /// ```
+  ///
+  /// Input whose height grows from one line up to as many lines as needed for
+  /// the text that was entered. If a height limit is imposed by its parent, it
+  /// will scroll vertically when its height reaches that limit.
+  /// ```dart
+  /// TextField(maxLines: null)
+  /// ```
+  ///
+  /// The input's height is large enough for the given number of lines. If
+  /// additional lines are entered the input scrolls vertically.
+  /// ```dart
+  /// TextField(maxLines: 2)
+  /// ```
+  ///
+  /// Input whose height grows with content between a min and max. An infinite
+  /// max is possible with `maxLines: null`.
+  /// ```dart
+  /// TextField(minLines: 2, maxLines: 4)
+  /// ```
   /// {@endtemplate}
   final int maxLines;
 
+  /// {@template flutter.widgets.editableText.minLines}
+  /// The minimum number of lines to occupy when the content spans fewer lines.
+
+  /// When [maxLines] is set as well, the height will grow between the indicated
+  /// range of lines. When [maxLines] is null, it will grow as high as needed,
+  /// starting from [minLines].
+  ///
+  /// See the examples in [maxLines] for the complete picture of how [maxLines]
+  /// and [minLines] interact to produce various behaviors.
+  ///
+  /// Defaults to null.
+  /// {@endtemplate}
+  final int minLines;
+
+  /// {@template flutter.widgets.editableText.expands}
+  /// Whether this widget's height will be sized to fill its parent.
+  ///
+  /// If set to true and wrapped in a parent widget like [Expanded] or
+  /// [SizedBox], the input will expand to fill the parent.
+  ///
+  /// [maxLines] and [minLines] must both be null when this is set to true,
+  /// otherwise an error is thrown.
+  ///
+  /// Defaults to false.
+  ///
+  /// See the examples in [maxLines] for the complete picture of how [maxLines],
+  /// [minLines], and [expands] interact to produce various behaviors.
+  ///
+  /// Input that matches the height of its parent
+  /// ```dart
+  /// Expanded(
+  ///   child: TextField(maxLines: null, expands: true),
+  /// )
+  /// ```
+  /// {@endtemplate}
+  final bool expands;
+
   /// {@template flutter.widgets.editableText.autofocus}
   /// Whether this text field should focus itself if nothing else is already
   /// focused.
@@ -676,6 +754,8 @@
     properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
     properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
     properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
+    properties.add(IntProperty('minLines', minLines, defaultValue: null));
+    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
     properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
     properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null));
   }
@@ -795,7 +875,7 @@
         // If this is a multiline EditableText, do nothing for a "newline"
         // action; The newline is already inserted. Otherwise, finalize
         // editing.
-        if (widget.maxLines == 1)
+        if (!_isMultiline)
           _finalizeEditing(true);
         break;
       case TextInputAction.done:
@@ -1333,6 +1413,8 @@
                   : _cursorVisibilityNotifier,
               hasFocus: _hasFocus,
               maxLines: widget.maxLines,
+              minLines: widget.minLines,
+              expands: widget.expands,
               strutStyle: widget.strutStyle,
               selectionColor: widget.selectionColor,
               textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
@@ -1403,6 +1485,8 @@
     this.showCursor,
     this.hasFocus,
     this.maxLines,
+    this.minLines,
+    this.expands,
     this.strutStyle,
     this.selectionColor,
     this.textScaleFactor,
@@ -1433,6 +1517,8 @@
   final ValueNotifier<bool> showCursor;
   final bool hasFocus;
   final int maxLines;
+  final int minLines;
+  final bool expands;
   final StrutStyle strutStyle;
   final Color selectionColor;
   final double textScaleFactor;
@@ -1462,6 +1548,8 @@
       showCursor: showCursor,
       hasFocus: hasFocus,
       maxLines: maxLines,
+      minLines: minLines,
+      expands: expands,
       strutStyle: strutStyle,
       selectionColor: selectionColor,
       textScaleFactor: textScaleFactor,
@@ -1492,6 +1580,8 @@
       ..showCursor = showCursor
       ..hasFocus = hasFocus
       ..maxLines = maxLines
+      ..minLines = minLines
+      ..expands = expands
       ..strutStyle = strutStyle
       ..selectionColor = selectionColor
       ..textScaleFactor = textScaleFactor
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart
index 101a1eb..8e8cba8 100644
--- a/packages/flutter/test/material/input_decorator_test.dart
+++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -993,6 +993,100 @@
     expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopRight(find.byKey(sKey)).dx));
   });
 
+  testWidgets('InputDecorator tall prefix', (WidgetTester tester) async {
+    const Key pKey = Key('p');
+    await tester.pumpWidget(
+      buildInputDecorator(
+        // isEmpty: false (default)
+        // isFocused: false (default)
+        decoration: InputDecoration(
+          prefix: Container(
+            key: pKey,
+            height: 100,
+            width: 10,
+          ),
+          filled: true,
+        ),
+        // Set the fontSize so that everything works out to whole numbers.
+        child: const Text(
+          'text',
+          style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+        ),
+      ),
+    );
+
+    // Overall height for this InputDecorator is ~127.2dps because
+    // the prefix is 100dps tall, but it aligns with the input's baseline,
+    // overlapping the input a bit.
+    //   12 - top padding
+    //  100 - total height of prefix
+    //  -16 - input prefix overlap (distance input top to baseline, not exact)
+    //   20 - input text (ahem font size 16dps)
+    //    0 - bottom prefix/suffix padding
+    //   12 - bottom padding
+
+    expect(tester.getSize(find.byType(InputDecorator)).width, 800.0);
+    expect(tester.getSize(find.byType(InputDecorator)).height, closeTo(128.0, .0001));
+    expect(tester.getSize(find.text('text')).height, 20.0);
+    expect(tester.getSize(find.byKey(pKey)).height, 100.0);
+    expect(tester.getTopLeft(find.text('text')).dy, closeTo(96, .0001)); // 12 + 100 - 16
+    expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
+
+    // layout is a row: [prefix text suffix]
+    expect(tester.getTopLeft(find.byKey(pKey)).dx, 12.0);
+    expect(tester.getTopRight(find.byKey(pKey)).dx, tester.getTopLeft(find.text('text')).dx);
+  });
+
+  testWidgets('InputDecorator tall prefix with border', (WidgetTester tester) async {
+    const Key pKey = Key('p');
+    await tester.pumpWidget(
+      buildInputDecorator(
+        // isEmpty: false (default)
+        // isFocused: false (default)
+        decoration: InputDecoration(
+          border: const OutlineInputBorder(),
+          prefix: Container(
+            key: pKey,
+            height: 100,
+            width: 10,
+          ),
+          filled: true,
+        ),
+        // Set the fontSize so that everything works out to whole numbers.
+        child: const Text(
+          'text',
+          style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+        ),
+      ),
+    );
+
+    // Overall height for this InputDecorator is ~127.2dps because
+    // the prefix is 100dps tall, but it aligns with the input's baseline,
+    // overlapping the input a bit.
+    //   24 - top padding
+    //  100 - total height of prefix
+    //  -16 - input prefix overlap (distance input top to baseline, not exact)
+    //   20 - input text (ahem font size 16dps)
+    //    0 - bottom prefix/suffix padding
+    //   16 - bottom padding
+    // When a border is present, the input text and prefix/suffix are centered
+    // within the input. Here, that will be content of height 106, including 2
+    // extra pixels of space, centered within an input of height 144. That gives
+    // 19 pixels of space on each side of the content, so the prefix is
+    // positioned at 19, and the text is at 19+100-16=103.
+
+    expect(tester.getSize(find.byType(InputDecorator)).width, 800.0);
+    expect(tester.getSize(find.byType(InputDecorator)).height, closeTo(144, .0001));
+    expect(tester.getSize(find.text('text')).height, 20.0);
+    expect(tester.getSize(find.byKey(pKey)).height, 100.0);
+    expect(tester.getTopLeft(find.text('text')).dy, closeTo(103, .0001));
+    expect(tester.getTopLeft(find.byKey(pKey)).dy, 19.0);
+
+    // layout is a row: [prefix text suffix]
+    expect(tester.getTopLeft(find.byKey(pKey)).dx, 12.0);
+    expect(tester.getTopRight(find.byKey(pKey)).dx, tester.getTopLeft(find.text('text')).dx);
+  });
+
   testWidgets('InputDecorator prefixIcon/suffixIcon', (WidgetTester tester) async {
     await tester.pumpWidget(
       buildInputDecorator(
@@ -1075,7 +1169,6 @@
     expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0);
   });
 
-
   testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
     await tester.pumpWidget(
       buildInputDecorator(
@@ -1463,12 +1556,12 @@
   testWidgets('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async {
     await tester.pumpWidget(
       buildInputDecorator(
-      // isFocused: false (default)
-      isEmpty: true,
-      decoration: const InputDecoration(
-        border: OutlineInputBorder(borderSide: BorderSide.none),
-        hasFloatingPlaceholder: false,
-        labelText: 'label',
+        // isFocused: false (default)
+        isEmpty: true,
+        decoration: const InputDecoration(
+          border: OutlineInputBorder(borderSide: BorderSide.none),
+          hasFloatingPlaceholder: false,
+          labelText: 'label',
         ),
       ),
     );
diff --git a/packages/flutter/test/material/text_field_focus_test.dart b/packages/flutter/test/material/text_field_focus_test.dart
index 68c1611..2ec69c5 100644
--- a/packages/flutter/test/material/text_field_focus_test.dart
+++ b/packages/flutter/test/material/text_field_focus_test.dart
@@ -6,6 +6,49 @@
 import 'package:flutter/material.dart';
 
 void main() {
+  testWidgets('Dialog interaction', (WidgetTester tester) async {
+    expect(tester.testTextInput.isVisible, isFalse);
+
+    await tester.pumpWidget(
+      const MaterialApp(
+        home: Material(
+          child: Center(
+            child: TextField(
+              autofocus: true,
+            ),
+          ),
+        ),
+      ),
+    );
+
+    expect(tester.testTextInput.isVisible, isTrue);
+
+    final BuildContext context = tester.element(find.byType(TextField));
+
+    showDialog<void>(
+      context: context,
+      builder: (BuildContext context) => const SimpleDialog(title: Text('Dialog')),
+    );
+
+    await tester.pump();
+
+    expect(tester.testTextInput.isVisible, isFalse);
+
+    Navigator.of(tester.element(find.text('Dialog'))).pop();
+    await tester.pump();
+
+    expect(tester.testTextInput.isVisible, isFalse);
+
+    await tester.tap(find.byType(TextField));
+    await tester.idle();
+
+    expect(tester.testTextInput.isVisible, isTrue);
+
+    await tester.pumpWidget(Container());
+
+    expect(tester.testTextInput.isVisible, isFalse);
+  });
+
   testWidgets('Request focus shows keyboard', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
 
@@ -87,49 +130,6 @@
     await tester.pumpWidget(Container());
 
     expect(tester.testTextInput.isVisible, isFalse);
-  });
-
-  testWidgets('Dialog interaction', (WidgetTester tester) async {
-    expect(tester.testTextInput.isVisible, isFalse);
-
-    await tester.pumpWidget(
-      const MaterialApp(
-        home: Material(
-          child: Center(
-            child: TextField(
-              autofocus: true,
-            ),
-          ),
-        ),
-      ),
-    );
-
-    expect(tester.testTextInput.isVisible, isTrue);
-
-    final BuildContext context = tester.element(find.byType(TextField));
-
-    showDialog<void>(
-      context: context,
-      builder: (BuildContext context) => const SimpleDialog(title: Text('Dialog')),
-    );
-
-    await tester.pump();
-
-    expect(tester.testTextInput.isVisible, isFalse);
-
-    Navigator.of(tester.element(find.text('Dialog'))).pop();
-    await tester.pump();
-
-    expect(tester.testTextInput.isVisible, isFalse);
-
-    await tester.tap(find.byType(TextField));
-    await tester.idle();
-
-    expect(tester.testTextInput.isVisible, isTrue);
-
-    await tester.pumpWidget(Container());
-
-    expect(tester.testTextInput.isVisible, isFalse);
   }, skip: true); // https://github.com/flutter/flutter/issues/29384.
 
   testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 7cd6d3b..1e38ea3 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -174,6 +174,24 @@
     debugResetSemanticsIdCounter();
   });
 
+  final Key textFieldKey = UniqueKey();
+  Widget textFieldBuilder({
+    int maxLines = 1,
+    int minLines,
+  }) {
+    return boilerplate(
+      child: TextField(
+        key: textFieldKey,
+        style: const TextStyle(color: Colors.black, fontSize: 34.0),
+        maxLines: maxLines,
+        minLines: minLines,
+        decoration: const InputDecoration(
+          hintText: 'Placeholder',
+        ),
+      ),
+    );
+  }
+
   testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
     final VoidCallback onEditingComplete = () { };
 
@@ -883,23 +901,8 @@
     expect(controller.selection.isCollapsed, false);
   });
 
-  testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
-    final Key textFieldKey = UniqueKey();
-
-    Widget builder(int maxLines) {
-      return boilerplate(
-        child: TextField(
-          key: textFieldKey,
-          style: const TextStyle(color: Colors.black, fontSize: 34.0),
-          maxLines: maxLines,
-          decoration: const InputDecoration(
-            hintText: 'Placeholder',
-          ),
-        ),
-      );
-    }
-
-    await tester.pumpWidget(builder(null));
+  testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
+    await tester.pumpWidget(textFieldBuilder());
 
     RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
 
@@ -907,46 +910,361 @@
     final Size emptyInputSize = inputBox.size;
 
     await tester.enterText(find.byType(TextField), 'No wrapping here.');
-    await tester.pumpWidget(builder(null));
+    await tester.pumpWidget(textFieldBuilder());
     expect(findInputBox(), equals(inputBox));
     expect(inputBox.size, equals(emptyInputSize));
 
-    await tester.pumpWidget(builder(3));
+    // Even when entering multiline text, TextField doesn't grow. It's a single
+    // line input.
+    await tester.enterText(find.byType(TextField), kThreeLines);
+    await tester.pumpWidget(textFieldBuilder());
     expect(findInputBox(), equals(inputBox));
-    expect(inputBox.size, greaterThan(emptyInputSize));
+    expect(inputBox.size, equals(emptyInputSize));
+
+    // maxLines: 3 makes the TextField 3 lines tall
+    await tester.enterText(find.byType(TextField), '');
+    await tester.pumpWidget(textFieldBuilder(maxLines: 3));
+    expect(findInputBox(), equals(inputBox));
+    expect(inputBox.size.height, greaterThan(emptyInputSize.height));
+    expect(inputBox.size.width, emptyInputSize.width);
 
     final Size threeLineInputSize = inputBox.size;
 
+    // Filling with 3 lines of text stays the same size
     await tester.enterText(find.byType(TextField), kThreeLines);
-    await tester.pumpWidget(builder(null));
-    expect(findInputBox(), equals(inputBox));
-    expect(inputBox.size, greaterThan(emptyInputSize));
-
-    await tester.enterText(find.byType(TextField), kThreeLines);
-    await tester.pumpWidget(builder(null));
+    await tester.pumpWidget(textFieldBuilder(maxLines: 3));
     expect(findInputBox(), equals(inputBox));
     expect(inputBox.size, threeLineInputSize);
 
     // An extra line won't increase the size because we max at 3.
     await tester.enterText(find.byType(TextField), kMoreThanFourLines);
-    await tester.pumpWidget(builder(3));
+    await tester.pumpWidget(textFieldBuilder(maxLines: 3));
     expect(findInputBox(), equals(inputBox));
     expect(inputBox.size, threeLineInputSize);
 
     // But now it will... but it will max at four
     await tester.enterText(find.byType(TextField), kMoreThanFourLines);
-    await tester.pumpWidget(builder(4));
+    await tester.pumpWidget(textFieldBuilder(maxLines: 4));
     expect(findInputBox(), equals(inputBox));
-    expect(inputBox.size, greaterThan(threeLineInputSize));
+    expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
+    expect(inputBox.size.width, threeLineInputSize.width);
 
     final Size fourLineInputSize = inputBox.size;
 
     // Now it won't max out until the end
-    await tester.pumpWidget(builder(null));
+    await tester.enterText(find.byType(TextField), '');
+    await tester.pumpWidget(textFieldBuilder(maxLines: null));
     expect(findInputBox(), equals(inputBox));
-    expect(inputBox.size, greaterThan(fourLineInputSize));
+    expect(inputBox.size, equals(emptyInputSize));
+    await tester.enterText(find.byType(TextField), kThreeLines);
+    await tester.pump();
+    expect(inputBox.size, equals(threeLineInputSize));
+    await tester.enterText(find.byType(TextField), kMoreThanFourLines);
+    await tester.pump();
+    expect(inputBox.size.height, greaterThan(fourLineInputSize.height));
+    expect(inputBox.size.width, fourLineInputSize.width);
   });
 
+  testWidgets('TextField height with minLines and maxLines', (WidgetTester tester) async {
+    await tester.pumpWidget(textFieldBuilder());
+
+    RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
+
+    final RenderBox inputBox = findInputBox();
+    final Size emptyInputSize = inputBox.size;
+
+    await tester.enterText(find.byType(TextField), 'No wrapping here.');
+    await tester.pumpWidget(textFieldBuilder());
+    expect(findInputBox(), equals(inputBox));
+    expect(inputBox.size, equals(emptyInputSize));
+
+    // min and max set to same value locks height to value.
+    await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 3));
+    expect(findInputBox(), equals(inputBox));
+    expect(inputBox.size.height, greaterThan(emptyInputSize.height));
+    expect(inputBox.size.width, emptyInputSize.width);
+
+    final Size threeLineInputSize = inputBox.size;
+
+    // maxLines: null with minLines set grows beyond minLines
+    await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: null));
+    expect(findInputBox(), equals(inputBox));
+    expect(inputBox.size, threeLineInputSize);
+    await tester.enterText(find.byType(TextField), kMoreThanFourLines);
+    await tester.pump();
+    expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
+    expect(inputBox.size.width, threeLineInputSize.width);
+
+    // With minLines and maxLines set, input will expand through the range
+    await tester.enterText(find.byType(TextField), '');
+    await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 4));
+    expect(findInputBox(), equals(inputBox));
+    expect(inputBox.size, equals(threeLineInputSize));
+    await tester.enterText(find.byType(TextField), kMoreThanFourLines);
+    await tester.pump();
+    expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
+    expect(inputBox.size.width, threeLineInputSize.width);
+
+    // minLines can't be greater than maxLines.
+    expect(() async {
+      await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 2));
+    }, throwsAssertionError);
+    expect(() async {
+      await tester.pumpWidget(textFieldBuilder(minLines: 3));
+    }, throwsAssertionError);
+
+    // maxLines defaults to 1 and can't be less than minLines
+    expect(() async {
+      await tester.pumpWidget(textFieldBuilder(minLines: 3));
+    }, throwsAssertionError);
+  });
+
+  testWidgets('Multiline text when wrapped in Expanded', (WidgetTester tester) async {
+    Widget expandedTextFieldBuilder({
+      int maxLines = 1,
+      int minLines,
+      bool expands = false,
+    }) {
+      return boilerplate(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            Expanded(
+              child: TextField(
+                key: textFieldKey,
+                style: const TextStyle(color: Colors.black, fontSize: 34.0),
+                maxLines: maxLines,
+                minLines: minLines,
+                expands: expands,
+                decoration: const InputDecoration(
+                  hintText: 'Placeholder',
+                ),
+              ),
+            ),
+          ],
+        ),
+      );
+    }
+
+    await tester.pumpWidget(expandedTextFieldBuilder());
+
+    RenderBox findBorder() {
+      return tester.renderObject(find.descendant(
+        of: find.byType(InputDecorator),
+        matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
+      ));
+    }
+    final RenderBox border = findBorder();
+
+    // Without expanded: true and maxLines: null, the TextField does not expand
+    // to fill its parent when wrapped in an Expanded widget.
+    final Size unexpandedInputSize = border.size;
+
+    // It does expand to fill its parent when expands: true, maxLines: null, and
+    // it's wrapped in an Expanded widget.
+    await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: null));
+    expect(border.size.height, greaterThan(unexpandedInputSize.height));
+    expect(border.size.width, unexpandedInputSize.width);
+
+    // min/maxLines that is not null and expands: true contradict each other.
+    expect(() async {
+      await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: 4));
+    }, throwsAssertionError);
+    expect(() async {
+      await tester.pumpWidget(expandedTextFieldBuilder(expands: true, minLines: 1, maxLines: null));
+    }, throwsAssertionError);
+  });
+
+  // Regression test for https://github.com/flutter/flutter/pull/29093
+  testWidgets('Multiline text when wrapped in IntrinsicHeight', (WidgetTester tester) async {
+    final Key intrinsicHeightKey = UniqueKey();
+    Widget intrinsicTextFieldBuilder(bool wrapInIntrinsic) {
+      final TextFormField textField = TextFormField(
+        key: textFieldKey,
+        style: const TextStyle(color: Colors.black, fontSize: 34.0),
+        maxLines: null,
+        decoration: const InputDecoration(
+          counterText: 'I am counter',
+        ),
+      );
+      final Widget widget = wrapInIntrinsic
+        ? IntrinsicHeight(key: intrinsicHeightKey, child: textField)
+        : textField;
+      return boilerplate(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[widget],
+        ),
+      );
+    }
+
+    await tester.pumpWidget(intrinsicTextFieldBuilder(false));
+    expect(find.byKey(intrinsicHeightKey), findsNothing);
+
+    RenderBox findEditableText() => tester.renderObject(find.byType(EditableText));
+    RenderBox editableText = findEditableText();
+    final Size unwrappedEditableTextSize = editableText.size;
+
+    // Wrapping in IntrinsicHeight should not affect the height of the input
+    await tester.pumpWidget(intrinsicTextFieldBuilder(true));
+    editableText = findEditableText();
+    expect(editableText.size.height, unwrappedEditableTextSize.height);
+    expect(editableText.size.width, unwrappedEditableTextSize.width);
+  });
+
+  // Regression test for https://github.com/flutter/flutter/pull/29093
+  testWidgets('errorText empty string', (WidgetTester tester) async {
+    Widget textFormFieldBuilder(String errorText) {
+      return boilerplate(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            TextFormField(
+              key: textFieldKey,
+              maxLength: 3,
+              maxLengthEnforced: false,
+              decoration: InputDecoration(
+                counterText: '',
+                errorText: errorText,
+              ),
+            ),
+          ],
+        ),
+      );
+    }
+
+    await tester.pumpWidget(textFormFieldBuilder(null));
+
+    RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
+    final RenderBox inputBox = findInputBox();
+    final Size errorNullInputSize = inputBox.size;
+
+    // Setting errorText causes the input's height to increase to accommodate it
+    await tester.pumpWidget(textFormFieldBuilder('im errorText'));
+    expect(inputBox, findInputBox());
+    expect(inputBox.size.height, greaterThan(errorNullInputSize.height));
+    expect(inputBox.size.width, errorNullInputSize.width);
+    final Size errorInputSize = inputBox.size;
+
+    // Setting errorText to an empty string causes the input's height to
+    // increase to accommodate it, even though it's not displayed.
+    // This may or may not be ideal behavior, but it is legacy behavior and
+    // there are visual tests that rely on it (see Github issue referenced at
+    // the top of this test). A counterText of empty string does not affect
+    // input height, however.
+    await tester.pumpWidget(textFormFieldBuilder(''));
+    expect(inputBox, findInputBox());
+    expect(inputBox.size.height, errorInputSize.height);
+    expect(inputBox.size.width, errorNullInputSize.width);
+  });
+
+  testWidgets('Growable TextField when content height exceeds parent', (WidgetTester tester) async {
+    const double height = 200.0;
+    const double padding = 24.0;
+
+    Widget containedTextFieldBuilder({
+      Widget counter,
+      String helperText,
+      String labelText,
+      Widget prefix,
+    }) {
+      return boilerplate(
+        child: Container(
+          height: height,
+          child: TextField(
+            key: textFieldKey,
+            maxLines: null,
+            decoration: InputDecoration(
+              counter: counter,
+              helperText: helperText,
+              labelText: labelText,
+              prefix: prefix,
+            ),
+          ),
+        ),
+      );
+    }
+
+    await tester.pumpWidget(containedTextFieldBuilder());
+    RenderBox findEditableText() => tester.renderObject(find.byType(EditableText));
+
+    final RenderBox inputBox = findEditableText();
+
+    // With no decoration and when overflowing with content, the EditableText
+    // takes up the full height minus the padding, so the input fits perfectly
+    // inside the parent.
+    await tester.enterText(find.byType(TextField), 'a\n' * 11);
+    await tester.pump();
+    expect(findEditableText(), equals(inputBox));
+    expect(inputBox.size.height, height - padding);
+
+    // Adding a counter causes the EditableText to shrink to fit the counter
+    // inside the parent as well.
+    const double counterHeight = 40.0;
+    const double subtextGap = 8.0;
+    const double counterSpace = counterHeight + subtextGap;
+    await tester.pumpWidget(containedTextFieldBuilder(
+      counter: Container(height: counterHeight),
+    ));
+    expect(findEditableText(), equals(inputBox));
+    expect(inputBox.size.height, height - padding - counterSpace);
+
+    // Including helperText causes the EditableText to shrink to fit the text
+    // inside the parent as well.
+    await tester.pumpWidget(containedTextFieldBuilder(
+      helperText: 'I am helperText',
+    ));
+    expect(findEditableText(), equals(inputBox));
+    const double helperTextSpace = 12.0;
+    expect(inputBox.size.height, height - padding - helperTextSpace - subtextGap);
+
+    // When both helperText and counter are present, EditableText shrinks by the
+    // height of the taller of the two in order to fit both within the parent.
+    await tester.pumpWidget(containedTextFieldBuilder(
+      counter: Container(height: counterHeight),
+      helperText: 'I am helperText',
+    ));
+    expect(findEditableText(), equals(inputBox));
+    expect(inputBox.size.height, height - padding - counterSpace);
+
+    // When a label is present, EditableText shrinks to fit it at the top so
+    // that the bottom of the input still lines up perfectly with the parent.
+    await tester.pumpWidget(containedTextFieldBuilder(
+      labelText: 'I am labelText',
+    ));
+    const double labelSpace = 16.0;
+    expect(findEditableText(), equals(inputBox));
+    expect(inputBox.size.height, height - padding - labelSpace);
+
+    // When decoration is present on the top and bottom, EditableText shrinks to
+    // fit both inside the parent independently.
+    await tester.pumpWidget(containedTextFieldBuilder(
+      counter: Container(height: counterHeight),
+      labelText: 'I am labelText',
+    ));
+    expect(findEditableText(), equals(inputBox));
+    expect(inputBox.size.height, height - padding - counterSpace - labelSpace);
+
+    // When a prefix or suffix is present in an input that's full of content,
+    // it is ignored and allowed to expand beyond the top of the input. Other
+    // top and bottom decoration is still respected.
+    await tester.pumpWidget(containedTextFieldBuilder(
+      counter: Container(height: counterHeight),
+      labelText: 'I am labelText',
+      prefix: Container(
+        width: 10,
+        height: 60,
+      ),
+    ));
+    expect(findEditableText(), equals(inputBox));
+    expect(
+      inputBox.size.height,
+      height
+      - padding
+      - labelSpace
+      - counterSpace,
+    );
+  });
 
   testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async {
     final Key textFieldKey = UniqueKey();
diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart
index 196eb31..f54ba7a 100644
--- a/packages/flutter/test/rendering/editable_test.dart
+++ b/packages/flutter/test/rendering/editable_test.dart
@@ -56,6 +56,7 @@
         ' │ cursorColor: null\n'
         ' │ showCursor: ValueNotifier<bool>#00000(false)\n'
         ' │ maxLines: 1\n'
+        ' │ minLines: null\n'
         ' │ selectionColor: null\n'
         ' │ textScaleFactor: 1.0\n'
         ' │ locale: ja_JP\n'