Unverified Commit 6d8f5399 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

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.
parent 7bed378e
...@@ -140,6 +140,8 @@ class CupertinoTextField extends StatefulWidget { ...@@ -140,6 +140,8 @@ class CupertinoTextField extends StatefulWidget {
/// ///
/// See also: /// 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 /// * [maxLength], which discusses the precise meaning of "number of
/// characters" and how it may differ from the intuitive meaning. /// characters" and how it may differ from the intuitive meaning.
const CupertinoTextField({ const CupertinoTextField({
...@@ -164,6 +166,8 @@ class CupertinoTextField extends StatefulWidget { ...@@ -164,6 +166,8 @@ class CupertinoTextField extends StatefulWidget {
this.obscureText = false, this.obscureText = false,
this.autocorrect = true, this.autocorrect = true,
this.maxLines = 1, this.maxLines = 1,
this.minLines,
this.expands = false,
this.maxLength, this.maxLength,
this.maxLengthEnforced = true, this.maxLengthEnforced = true,
this.onChanged, this.onChanged,
...@@ -183,6 +187,16 @@ class CupertinoTextField extends StatefulWidget { ...@@ -183,6 +187,16 @@ class CupertinoTextField extends StatefulWidget {
assert(maxLengthEnforced != null), assert(maxLengthEnforced != null),
assert(scrollPadding != null), assert(scrollPadding != null),
assert(maxLines == null || maxLines > 0), 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(maxLength == null || maxLength > 0),
assert(clearButtonMode != null), assert(clearButtonMode != null),
assert(prefixMode != null), assert(prefixMode != null),
...@@ -290,6 +304,12 @@ class CupertinoTextField extends StatefulWidget { ...@@ -290,6 +304,12 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.maxLines} /// {@macro flutter.widgets.editableText.maxLines}
final int 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 /// The maximum number of characters (Unicode scalar values) to allow in the
/// text field. /// text field.
/// ///
...@@ -405,6 +425,8 @@ class CupertinoTextField extends StatefulWidget { ...@@ -405,6 +425,8 @@ class CupertinoTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: false)); properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: false));
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); 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(IntProperty('maxLength', maxLength, defaultValue: null));
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced')); properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced'));
properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null)); properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null));
...@@ -662,6 +684,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -662,6 +684,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
obscureText: widget.obscureText, obscureText: widget.obscureText,
autocorrect: widget.autocorrect, autocorrect: widget.autocorrect,
maxLines: widget.maxLines, maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: _kSelectionHighlightColor, selectionColor: _kSelectionHighlightColor,
selectionControls: cupertinoTextSelectionControls, selectionControls: cupertinoTextSelectionControls,
onChanged: widget.onChanged, onChanged: widget.onChanged,
......
...@@ -552,14 +552,18 @@ class _RenderDecoration extends RenderBox { ...@@ -552,14 +552,18 @@ class _RenderDecoration extends RenderBox {
@required TextDirection textDirection, @required TextDirection textDirection,
@required TextBaseline textBaseline, @required TextBaseline textBaseline,
@required bool isFocused, @required bool isFocused,
@required bool expands,
}) : assert(decoration != null), }) : assert(decoration != null),
assert(textDirection != null), assert(textDirection != null),
assert(textBaseline != null), assert(textBaseline != null),
assert(expands != null),
_decoration = decoration, _decoration = decoration,
_textDirection = textDirection, _textDirection = textDirection,
_textBaseline = textBaseline, _textBaseline = textBaseline,
_isFocused = isFocused; _isFocused = isFocused,
_expands = expands;
static const double subtextGap = 8.0;
final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{}; final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{};
final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{}; final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{};
...@@ -709,6 +713,16 @@ class _RenderDecoration extends RenderBox { ...@@ -709,6 +713,16 @@ class _RenderDecoration extends RenderBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
bool get expands => _expands;
bool _expands = false;
set expands(bool value) {
assert(value != null);
if (_expands == value)
return;
_expands = value;
markNeedsLayout();
}
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
...@@ -804,34 +818,31 @@ class _RenderDecoration extends RenderBox { ...@@ -804,34 +818,31 @@ class _RenderDecoration extends RenderBox {
EdgeInsets get contentPadding => decoration.contentPadding; EdgeInsets get contentPadding => decoration.contentPadding;
// Returns a value used by performLayout to position all // Lay out the given box if needed, and return its baseline
// of the renderers. This method applies layout to all of the renderers double _layoutLineBox(RenderBox box, BoxConstraints constraints) {
// except the container. For convenience, the container is laid out if (box == null) {
// in performLayout(). return 0.0;
}
box.layout(constraints, parentUsesSize: true);
final double baseline = box.getDistanceToBaseline(textBaseline);
assert(baseline != null && baseline >= 0.0);
return baseline;
}
// 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) { _RenderDecorationLayout _layout(BoxConstraints layoutConstraints) {
// Margin on each side of subtext (counter and helperError)
final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{}; final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{};
BoxConstraints boxConstraints = layoutConstraints.loosen(); final 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);
}
layoutLineBox(prefix);
layoutLineBox(suffix);
if (icon != null) // Layout all the widgets used by InputDecorator
icon.layout(boxConstraints, parentUsesSize: true); boxToBaseline[prefix] = _layoutLineBox(prefix, boxConstraints);
if (prefixIcon != null) boxToBaseline[suffix] = _layoutLineBox(suffix, boxConstraints);
prefixIcon.layout(boxConstraints, parentUsesSize: true); boxToBaseline[icon] = _layoutLineBox(icon, boxConstraints);
if (suffixIcon != null) boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, boxConstraints);
suffixIcon.layout(boxConstraints, parentUsesSize: true); boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, boxConstraints);
final double inputWidth = math.max(0.0, constraints.maxWidth - ( final double inputWidth = math.max(0.0, constraints.maxWidth - (
_boxSize(icon).width _boxSize(icon).width
...@@ -841,72 +852,144 @@ class _RenderDecoration extends RenderBox { ...@@ -841,72 +852,144 @@ class _RenderDecoration extends RenderBox {
+ _boxSize(suffix).width + _boxSize(suffix).width
+ _boxSize(suffixIcon).width + _boxSize(suffixIcon).width
+ contentPadding.right)); + 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); // The helper or error text can occupy the full width less the space
if (label != null) { // occupied by the icon and counter.
if (decoration.alignLabelWithHint) { boxToBaseline[helperError] = _layoutLineBox(
// The label is aligned with the hint, at the baseline helperError,
layoutLineBox(label); boxConstraints.copyWith(
} 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(
maxWidth: math.max(0.0, boxConstraints.maxWidth maxWidth: math.max(0.0, boxConstraints.maxWidth
- _boxSize(icon).width - _boxSize(icon).width
- _boxSize(counter).width - _boxSize(counter).width
- contentPadding.horizontal, - contentPadding.horizontal,
), ),
); ),
layoutLineBox(helperError); );
if (aboveBaseline + belowBaseline > 0.0) { // The height of the input needs to accommodate label above and counter and
const double subtextGap = 8.0; // helperError below, when they exist.
subtextBaseline = containerHeight + subtextGap + aboveBaseline; final double labelHeight = label == null
subtextHeight = subtextGap + aboveBaseline + belowBaseline; ? 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( return _RenderDecorationLayout(
boxToBaseline: boxToBaseline, boxToBaseline: boxToBaseline,
...@@ -959,7 +1042,7 @@ class _RenderDecoration extends RenderBox { ...@@ -959,7 +1042,7 @@ class _RenderDecoration extends RenderBox {
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
double subtextHeight = _lineHeight(width, <RenderBox>[helperError, counter]); double subtextHeight = _lineHeight(width, <RenderBox>[helperError, counter]);
if (subtextHeight > 0.0) if (subtextHeight > 0.0)
subtextHeight += 8.0; subtextHeight += subtextGap;
return contentPadding.top return contentPadding.top
+ (label == null ? 0.0 : decoration.floatingLabelHeight) + (label == null ? 0.0 : decoration.floatingLabelHeight)
+ _lineHeight(width, <RenderBox>[prefix, input, suffix]) + _lineHeight(width, <RenderBox>[prefix, input, suffix])
...@@ -1370,15 +1453,18 @@ class _Decorator extends RenderObjectWidget { ...@@ -1370,15 +1453,18 @@ class _Decorator extends RenderObjectWidget {
@required this.textDirection, @required this.textDirection,
@required this.textBaseline, @required this.textBaseline,
@required this.isFocused, @required this.isFocused,
@required this.expands,
}) : assert(decoration != null), }) : assert(decoration != null),
assert(textDirection != null), assert(textDirection != null),
assert(textBaseline != null), assert(textBaseline != null),
assert(expands != null),
super(key: key); super(key: key);
final _Decoration decoration; final _Decoration decoration;
final TextDirection textDirection; final TextDirection textDirection;
final TextBaseline textBaseline; final TextBaseline textBaseline;
final bool isFocused; final bool isFocused;
final bool expands;
@override @override
_RenderDecorationElement createElement() => _RenderDecorationElement(this); _RenderDecorationElement createElement() => _RenderDecorationElement(this);
...@@ -1390,6 +1476,7 @@ class _Decorator extends RenderObjectWidget { ...@@ -1390,6 +1476,7 @@ class _Decorator extends RenderObjectWidget {
textDirection: textDirection, textDirection: textDirection,
textBaseline: textBaseline, textBaseline: textBaseline,
isFocused: isFocused, isFocused: isFocused,
expands: expands,
); );
} }
...@@ -1399,6 +1486,7 @@ class _Decorator extends RenderObjectWidget { ...@@ -1399,6 +1486,7 @@ class _Decorator extends RenderObjectWidget {
..decoration = decoration ..decoration = decoration
..textDirection = textDirection ..textDirection = textDirection
..textBaseline = textBaseline ..textBaseline = textBaseline
..expands = expands
..isFocused = isFocused; ..isFocused = isFocused;
} }
} }
...@@ -1461,6 +1549,7 @@ class InputDecorator extends StatefulWidget { ...@@ -1461,6 +1549,7 @@ class InputDecorator extends StatefulWidget {
this.baseStyle, this.baseStyle,
this.textAlign, this.textAlign,
this.isFocused = false, this.isFocused = false,
this.expands = false,
this.isEmpty = false, this.isEmpty = false,
this.child, this.child,
}) : assert(isFocused != null), }) : assert(isFocused != null),
...@@ -1495,6 +1584,19 @@ class InputDecorator extends StatefulWidget { ...@@ -1495,6 +1584,19 @@ class InputDecorator extends StatefulWidget {
/// Defaults to false. /// Defaults to false.
final bool isFocused; 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. /// Whether the input field is empty.
/// ///
/// Determines the position of the label text and whether to display the hint /// Determines the position of the label text and whether to display the hint
...@@ -1533,6 +1635,7 @@ class InputDecorator extends StatefulWidget { ...@@ -1533,6 +1635,7 @@ class InputDecorator extends StatefulWidget {
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration)); properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration));
properties.add(DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null)); properties.add(DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('isFocused', isFocused)); properties.add(DiagnosticsProperty<bool>('isFocused', isFocused));
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('isEmpty', isEmpty)); properties.add(DiagnosticsProperty<bool>('isEmpty', isEmpty));
} }
} }
...@@ -1928,6 +2031,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1928,6 +2031,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
textDirection: textDirection, textDirection: textDirection,
textBaseline: textBaseline, textBaseline: textBaseline,
isFocused: isFocused, isFocused: isFocused,
expands: widget.expands,
); );
} }
} }
......
...@@ -147,6 +147,8 @@ class TextField extends StatefulWidget { ...@@ -147,6 +147,8 @@ class TextField extends StatefulWidget {
this.obscureText = false, this.obscureText = false,
this.autocorrect = true, this.autocorrect = true,
this.maxLines = 1, this.maxLines = 1,
this.minLines,
this.expands = false,
this.maxLength, this.maxLength,
this.maxLengthEnforced = true, this.maxLengthEnforced = true,
this.onChanged, this.onChanged,
...@@ -171,6 +173,16 @@ class TextField extends StatefulWidget { ...@@ -171,6 +173,16 @@ class TextField extends StatefulWidget {
assert(scrollPadding != null), assert(scrollPadding != null),
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
assert(maxLines == null || maxLines > 0), 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), assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
super(key: key); super(key: key);
...@@ -269,6 +281,12 @@ class TextField extends StatefulWidget { ...@@ -269,6 +281,12 @@ class TextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.maxLines} /// {@macro flutter.widgets.editableText.maxLines}
final int 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" /// If [maxLength] is set to this value, only the "current input length"
/// part of the character counter is shown. /// part of the character counter is shown.
static const int noMaxLength = -1; static const int noMaxLength = -1;
...@@ -457,6 +475,8 @@ class TextField extends StatefulWidget { ...@@ -457,6 +475,8 @@ class TextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true)); properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); 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(IntProperty('maxLength', maxLength, defaultValue: null));
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced')); properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null)); properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
...@@ -851,6 +871,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -851,6 +871,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
obscureText: widget.obscureText, obscureText: widget.obscureText,
autocorrect: widget.autocorrect, autocorrect: widget.autocorrect,
maxLines: widget.maxLines, maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: themeData.textSelectionColor, selectionColor: themeData.textSelectionColor,
selectionControls: widget.selectionEnabled ? textSelectionControls : null, selectionControls: widget.selectionEnabled ? textSelectionControls : null,
onChanged: widget.onChanged, onChanged: widget.onChanged,
...@@ -883,6 +905,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -883,6 +905,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
textAlign: widget.textAlign, textAlign: widget.textAlign,
isFocused: focusNode.hasFocus, isFocused: focusNode.hasFocus,
isEmpty: controller.value.text.isEmpty, isEmpty: controller.value.text.isEmpty,
expands: widget.expands,
child: child, child: child,
); );
}, },
......
...@@ -89,6 +89,8 @@ class TextFormField extends FormField<String> { ...@@ -89,6 +89,8 @@ class TextFormField extends FormField<String> {
bool autovalidate = false, bool autovalidate = false,
bool maxLengthEnforced = true, bool maxLengthEnforced = true,
int maxLines = 1, int maxLines = 1,
int minLines,
bool expands = false,
int maxLength, int maxLength,
VoidCallback onEditingComplete, VoidCallback onEditingComplete,
ValueChanged<String> onFieldSubmitted, ValueChanged<String> onFieldSubmitted,
...@@ -112,6 +114,16 @@ class TextFormField extends FormField<String> { ...@@ -112,6 +114,16 @@ class TextFormField extends FormField<String> {
assert(maxLengthEnforced != null), assert(maxLengthEnforced != null),
assert(scrollPadding != null), assert(scrollPadding != null),
assert(maxLines == null || maxLines > 0), 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(maxLength == null || maxLength > 0),
assert(enableInteractiveSelection != null), assert(enableInteractiveSelection != null),
super( super(
...@@ -141,6 +153,8 @@ class TextFormField extends FormField<String> { ...@@ -141,6 +153,8 @@ class TextFormField extends FormField<String> {
autocorrect: autocorrect, autocorrect: autocorrect,
maxLengthEnforced: maxLengthEnforced, maxLengthEnforced: maxLengthEnforced,
maxLines: maxLines, maxLines: maxLines,
minLines: minLines,
expands: expands,
maxLength: maxLength, maxLength: maxLength,
onChanged: field.didChange, onChanged: field.didChange,
onEditingComplete: onEditingComplete, onEditingComplete: onEditingComplete,
......
...@@ -1801,9 +1801,9 @@ abstract class RenderBox extends RenderObject { ...@@ -1801,9 +1801,9 @@ abstract class RenderBox extends RenderObject {
testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', double.infinity); testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', double.infinity);
testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', double.infinity); testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', double.infinity);
if (constraints.hasBoundedWidth) if (constraints.hasBoundedWidth)
testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', constraints.maxWidth); testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', constraints.maxHeight);
if (constraints.hasBoundedHeight) 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. // TODO(ianh): Test that values are internally consistent in more ways than the above.
......
...@@ -144,6 +144,8 @@ class RenderEditable extends RenderBox { ...@@ -144,6 +144,8 @@ class RenderEditable extends RenderBox {
ValueNotifier<bool> showCursor, ValueNotifier<bool> showCursor,
bool hasFocus, bool hasFocus,
int maxLines = 1, int maxLines = 1,
int minLines,
bool expands = false,
StrutStyle strutStyle, StrutStyle strutStyle,
Color selectionColor, Color selectionColor,
double textScaleFactor = 1.0, double textScaleFactor = 1.0,
...@@ -165,6 +167,16 @@ class RenderEditable extends RenderBox { ...@@ -165,6 +167,16 @@ class RenderEditable extends RenderBox {
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(textDirection != null, 'RenderEditable created without a textDirection.'), assert(textDirection != null, 'RenderEditable created without a textDirection.'),
assert(maxLines == null || maxLines > 0), 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(textScaleFactor != null),
assert(offset != null), assert(offset != null),
assert(ignorePointer != null), assert(ignorePointer != null),
...@@ -186,6 +198,8 @@ class RenderEditable extends RenderBox { ...@@ -186,6 +198,8 @@ class RenderEditable extends RenderBox {
_showCursor = showCursor ?? ValueNotifier<bool>(false), _showCursor = showCursor ?? ValueNotifier<bool>(false),
_hasFocus = hasFocus ?? false, _hasFocus = hasFocus ?? false,
_maxLines = maxLines, _maxLines = maxLines,
_minLines = minLines,
_expands = expands,
_selectionColor = selectionColor, _selectionColor = selectionColor,
_selection = selection, _selection = selection,
_offset = offset, _offset = offset,
...@@ -735,6 +749,29 @@ class RenderEditable extends RenderBox { ...@@ -735,6 +749,29 @@ class RenderEditable extends RenderBox {
markNeedsTextLayout(); 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. /// The color to use when painting the selection.
Color get selectionColor => _selectionColor; Color get selectionColor => _selectionColor;
Color _selectionColor; Color _selectionColor;
...@@ -1194,8 +1231,28 @@ class RenderEditable extends RenderBox { ...@@ -1194,8 +1231,28 @@ class RenderEditable extends RenderBox {
double get preferredLineHeight => _textPainter.preferredLineHeight; double get preferredLineHeight => _textPainter.preferredLineHeight;
double _preferredHeight(double width) { 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; 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) { if (width == double.infinity) {
final String text = _textPainter.text.toPlainText(); final String text = _textPainter.text.toPlainText();
int lines = 1; int lines = 1;
...@@ -1658,6 +1715,8 @@ class RenderEditable extends RenderBox { ...@@ -1658,6 +1715,8 @@ class RenderEditable extends RenderBox {
properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor)); properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor));
properties.add(DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor)); properties.add(DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor));
properties.add(IntProperty('maxLines', maxLines)); 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(DiagnosticsProperty<Color>('selectionColor', selectionColor));
properties.add(DoubleProperty('textScaleFactor', textScaleFactor)); properties.add(DoubleProperty('textScaleFactor', textScaleFactor));
properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null)); properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
......
...@@ -278,6 +278,8 @@ class EditableText extends StatefulWidget { ...@@ -278,6 +278,8 @@ class EditableText extends StatefulWidget {
this.locale, this.locale,
this.textScaleFactor, this.textScaleFactor,
this.maxLines = 1, this.maxLines = 1,
this.minLines,
this.expands = false,
this.autofocus = false, this.autofocus = false,
this.selectionColor, this.selectionColor,
this.selectionControls, this.selectionControls,
...@@ -310,6 +312,16 @@ class EditableText extends StatefulWidget { ...@@ -310,6 +312,16 @@ class EditableText extends StatefulWidget {
assert(backgroundCursorColor != null), assert(backgroundCursorColor != null),
assert(textAlign != null), assert(textAlign != null),
assert(maxLines == null || maxLines > 0), 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(autofocus != null),
assert(rendererIgnoresPointer != null), assert(rendererIgnoresPointer != null),
assert(scrollPadding != null), assert(scrollPadding != null),
...@@ -465,12 +477,78 @@ class EditableText extends StatefulWidget { ...@@ -465,12 +477,78 @@ class EditableText extends StatefulWidget {
/// container will start with enough vertical space for one line and /// container will start with enough vertical space for one line and
/// automatically grow to accommodate additional lines as they are entered. /// 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 /// If this is not null, the value must be greater than zero, and it will lock
/// than 1, it will take up enough horizontal space to accommodate that number /// the input to the given number of lines and take up enough horizontal space
/// of lines. /// 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} /// {@endtemplate}
final int maxLines; 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} /// {@template flutter.widgets.editableText.autofocus}
/// Whether this text field should focus itself if nothing else is already /// Whether this text field should focus itself if nothing else is already
/// focused. /// focused.
...@@ -676,6 +754,8 @@ class EditableText extends StatefulWidget { ...@@ -676,6 +754,8 @@ class EditableText extends StatefulWidget {
properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null)); properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); 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<bool>('autofocus', autofocus, defaultValue: false));
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null)); properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null));
} }
...@@ -795,7 +875,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -795,7 +875,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// If this is a multiline EditableText, do nothing for a "newline" // If this is a multiline EditableText, do nothing for a "newline"
// action; The newline is already inserted. Otherwise, finalize // action; The newline is already inserted. Otherwise, finalize
// editing. // editing.
if (widget.maxLines == 1) if (!_isMultiline)
_finalizeEditing(true); _finalizeEditing(true);
break; break;
case TextInputAction.done: case TextInputAction.done:
...@@ -1333,6 +1413,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1333,6 +1413,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
: _cursorVisibilityNotifier, : _cursorVisibilityNotifier,
hasFocus: _hasFocus, hasFocus: _hasFocus,
maxLines: widget.maxLines, maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
strutStyle: widget.strutStyle, strutStyle: widget.strutStyle,
selectionColor: widget.selectionColor, selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
...@@ -1403,6 +1485,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -1403,6 +1485,8 @@ class _Editable extends LeafRenderObjectWidget {
this.showCursor, this.showCursor,
this.hasFocus, this.hasFocus,
this.maxLines, this.maxLines,
this.minLines,
this.expands,
this.strutStyle, this.strutStyle,
this.selectionColor, this.selectionColor,
this.textScaleFactor, this.textScaleFactor,
...@@ -1433,6 +1517,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -1433,6 +1517,8 @@ class _Editable extends LeafRenderObjectWidget {
final ValueNotifier<bool> showCursor; final ValueNotifier<bool> showCursor;
final bool hasFocus; final bool hasFocus;
final int maxLines; final int maxLines;
final int minLines;
final bool expands;
final StrutStyle strutStyle; final StrutStyle strutStyle;
final Color selectionColor; final Color selectionColor;
final double textScaleFactor; final double textScaleFactor;
...@@ -1462,6 +1548,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -1462,6 +1548,8 @@ class _Editable extends LeafRenderObjectWidget {
showCursor: showCursor, showCursor: showCursor,
hasFocus: hasFocus, hasFocus: hasFocus,
maxLines: maxLines, maxLines: maxLines,
minLines: minLines,
expands: expands,
strutStyle: strutStyle, strutStyle: strutStyle,
selectionColor: selectionColor, selectionColor: selectionColor,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
...@@ -1492,6 +1580,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -1492,6 +1580,8 @@ class _Editable extends LeafRenderObjectWidget {
..showCursor = showCursor ..showCursor = showCursor
..hasFocus = hasFocus ..hasFocus = hasFocus
..maxLines = maxLines ..maxLines = maxLines
..minLines = minLines
..expands = expands
..strutStyle = strutStyle ..strutStyle = strutStyle
..selectionColor = selectionColor ..selectionColor = selectionColor
..textScaleFactor = textScaleFactor ..textScaleFactor = textScaleFactor
......
...@@ -993,6 +993,100 @@ void main() { ...@@ -993,6 +993,100 @@ void main() {
expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopRight(find.byKey(sKey)).dx)); 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 { testWidgets('InputDecorator prefixIcon/suffixIcon', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
...@@ -1075,7 +1169,6 @@ void main() { ...@@ -1075,7 +1169,6 @@ void main() {
expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0); expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0);
}); });
testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async { testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
...@@ -1463,12 +1556,12 @@ void main() { ...@@ -1463,12 +1556,12 @@ void main() {
testWidgets('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async { testWidgets('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
buildInputDecorator( buildInputDecorator(
// isFocused: false (default) // isFocused: false (default)
isEmpty: true, isEmpty: true,
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide.none), border: OutlineInputBorder(borderSide: BorderSide.none),
hasFloatingPlaceholder: false, hasFloatingPlaceholder: false,
labelText: 'label', labelText: 'label',
), ),
), ),
); );
......
...@@ -6,24 +6,40 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,24 +6,40 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() { void main() {
testWidgets('Request focus shows keyboard', (WidgetTester tester) async { testWidgets('Dialog interaction', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); expect(tester.testTextInput.isVisible, isFalse);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( const MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(
focusNode: focusNode, 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); expect(tester.testTextInput.isVisible, isFalse);
FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode); 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(); await tester.idle();
expect(tester.testTextInput.isVisible, isTrue); expect(tester.testTextInput.isVisible, isTrue);
...@@ -33,21 +49,26 @@ void main() { ...@@ -33,21 +49,26 @@ void main() {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
}); });
testWidgets('Autofocus shows keyboard', (WidgetTester tester) async { testWidgets('Request focus shows keyboard', (WidgetTester tester) async {
expect(tester.testTextInput.isVisible, isFalse); final FocusNode focusNode = FocusNode();
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(
autofocus: true, focusNode: focusNode,
), ),
), ),
), ),
), ),
); );
expect(tester.testTextInput.isVisible, isFalse);
FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode);
await tester.idle();
expect(tester.testTextInput.isVisible, isTrue); expect(tester.testTextInput.isVisible, isTrue);
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
...@@ -55,33 +76,21 @@ void main() { ...@@ -55,33 +76,21 @@ void main() {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
}); });
testWidgets('Tap shows keyboard', (WidgetTester tester) async { testWidgets('Autofocus shows keyboard', (WidgetTester tester) async {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField(), child: TextField(
autofocus: true,
),
), ),
), ),
), ),
); );
expect(tester.testTextInput.isVisible, isFalse);
await tester.tap(find.byType(TextField));
await tester.idle();
expect(tester.testTextInput.isVisible, isTrue);
tester.testTextInput.hide();
expect(tester.testTextInput.isVisible, isFalse);
await tester.tap(find.byType(TextField));
await tester.idle();
expect(tester.testTextInput.isVisible, isTrue); expect(tester.testTextInput.isVisible, isTrue);
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
...@@ -89,36 +98,27 @@ void main() { ...@@ -89,36 +98,27 @@ void main() {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
}); });
testWidgets('Dialog interaction', (WidgetTester tester) async { testWidgets('Tap shows keyboard', (WidgetTester tester) async {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(),
autofocus: true,
),
), ),
), ),
), ),
); );
expect(tester.testTextInput.isVisible, isTrue); expect(tester.testTextInput.isVisible, isFalse);
final BuildContext context = tester.element(find.byType(TextField));
showDialog<void>(
context: context,
builder: (BuildContext context) => const SimpleDialog(title: Text('Dialog')),
);
await tester.pump(); await tester.tap(find.byType(TextField));
await tester.idle();
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isTrue);
Navigator.of(tester.element(find.text('Dialog'))).pop(); tester.testTextInput.hide();
await tester.pump();
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
......
...@@ -174,6 +174,24 @@ void main() { ...@@ -174,6 +174,24 @@ void main() {
debugResetSemanticsIdCounter(); 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 { testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
final VoidCallback onEditingComplete = () { }; final VoidCallback onEditingComplete = () { };
...@@ -883,23 +901,8 @@ void main() { ...@@ -883,23 +901,8 @@ void main() {
expect(controller.selection.isCollapsed, false); expect(controller.selection.isCollapsed, false);
}); });
testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async { testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey(); await tester.pumpWidget(textFieldBuilder());
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));
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
...@@ -907,46 +910,361 @@ void main() { ...@@ -907,46 +910,361 @@ void main() {
final Size emptyInputSize = inputBox.size; final Size emptyInputSize = inputBox.size;
await tester.enterText(find.byType(TextField), 'No wrapping here.'); await tester.enterText(find.byType(TextField), 'No wrapping here.');
await tester.pumpWidget(builder(null)); await tester.pumpWidget(textFieldBuilder());
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize)); 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(findInputBox(), equals(inputBox));
expect(inputBox.size, greaterThan(emptyInputSize)); expect(inputBox.size, equals(emptyInputSize));
final Size threeLineInputSize = inputBox.size;
await tester.enterText(find.byType(TextField), kThreeLines); // maxLines: 3 makes the TextField 3 lines tall
await tester.pumpWidget(builder(null)); await tester.enterText(find.byType(TextField), '');
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, greaterThan(emptyInputSize)); 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.enterText(find.byType(TextField), kThreeLines);
await tester.pumpWidget(builder(null)); await tester.pumpWidget(textFieldBuilder(maxLines: 3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, threeLineInputSize); expect(inputBox.size, threeLineInputSize);
// An extra line won't increase the size because we max at 3. // An extra line won't increase the size because we max at 3.
await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pumpWidget(builder(3)); await tester.pumpWidget(textFieldBuilder(maxLines: 3));
expect(findInputBox(), equals(inputBox)); expect(findInputBox(), equals(inputBox));
expect(inputBox.size, threeLineInputSize); expect(inputBox.size, threeLineInputSize);
// But now it will... but it will max at four // But now it will... but it will max at four
await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.enterText(find.byType(TextField), kMoreThanFourLines);
await tester.pumpWidget(builder(4)); await tester.pumpWidget(textFieldBuilder(maxLines: 4));
expect(findInputBox(), equals(inputBox)); 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; final Size fourLineInputSize = inputBox.size;
// Now it won't max out until the end // 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(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 { testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey(); final Key textFieldKey = UniqueKey();
......
...@@ -56,6 +56,7 @@ void main() { ...@@ -56,6 +56,7 @@ void main() {
' │ cursorColor: null\n' ' │ cursorColor: null\n'
' │ showCursor: ValueNotifier<bool>#00000(false)\n' ' │ showCursor: ValueNotifier<bool>#00000(false)\n'
' │ maxLines: 1\n' ' │ maxLines: 1\n'
' │ minLines: null\n'
' │ selectionColor: null\n' ' │ selectionColor: null\n'
' │ textScaleFactor: 1.0\n' ' │ textScaleFactor: 1.0\n'
' │ locale: ja_JP\n' ' │ locale: ja_JP\n'
......
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