mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Fix TextField height issues (#27205)
* Create new TextField attribute to control maxLength behaviour * Create test case for maxLinesIncrementalHeight * fix maxLinesIncrementalHeight set method * fix editable_test.dart * Just introducing some proposed API additions, renaming to expands * Pass minLines and expands through to child widgets and validate * minLines can't be null, and expands can't be true when maxLines is 1 * Implement minLines and maxLines height sizing logic * Simplify minLines validation logic * expands parameter proof of concept * min/max mixup bug fix, and tests work with expands false * Test expands=true cases, and fix textPainter.height being out of date * Test all behavior matrix cases * min/max assertion more strict, can't be equal * Tests work that were missing expands serialization * Action sheet tests no longer fail due to rounding error * TextFieldFocus test no longer mysteriously fails * TODOs for making expands nullable. Will depend on how Expanded wrapping works * Expanded growth happens when expanded is true and maxLines is null * Test Expanded wrapper * No more overflow when wrapped in Expanded * Docs improvements * expands can be null * Simplify error cases to support existing behavior * Docs examples and other docs cleanup * Expansion up to perfectly meet the parent size * Fix analyze null error * Fix test after move to nullable expands * minLines defaults to null * expands is now exclusively for expanding to parent height and not growth between min and max * _layout rewritten to handle max height. Need to fix prefix tests and reenable expands * Tests for textfield overflowing parent * layoutLineBox is documented and private * expands works in new _layout * _layout return numbers seem to perfectly match original _layout * inputWidth comment after trying it out and failing tests * Fix analyze errors * WIP prefix/suffix do affect height * Prefix/suffix and icons affect height, tests pass, but I'm still visually verifying identical to original * Tall prefix test that verifies pixel perfect layout * Fix overflowing edge case and test it * Clean up comments, old code, and todos * Changing _expands causes relayout. Wasnt able to figure out how to test though... * Clean up code review comments * Fix misalignment when tall prefix and border, and clean up related test * Simple code review cleanup * Bring back inputWidth to _layout method * Fix rounding errors showing up in mac tests * Fix flake by reordering tests. Without this, the dreaded intrinsicwidth flake is reproducible 50% of the time on my machine. * Fix more rounding error mac tests
This commit is contained in:
parent
80082ac4d3
commit
9e9f48dabb
@ -552,13 +552,16 @@ class _RenderDecoration extends RenderBox {
|
||||
@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;
|
||||
|
||||
final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{};
|
||||
final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{};
|
||||
@ -709,6 +712,16 @@ class _RenderDecoration extends RenderBox {
|
||||
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 +817,31 @@ class _RenderDecoration extends RenderBox {
|
||||
|
||||
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);
|
||||
// Lay out the given box if needed, and return its baseline
|
||||
double _layoutLineBox(RenderBox box, BoxConstraints constraints) {
|
||||
if (box == null) {
|
||||
return 0.0;
|
||||
}
|
||||
box.layout(constraints, 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);
|
||||
return baseline;
|
||||
}
|
||||
layoutLineBox(prefix);
|
||||
layoutLineBox(suffix);
|
||||
|
||||
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 +851,144 @@ class _RenderDecoration extends RenderBox {
|
||||
+ _boxSize(suffix).width
|
||||
+ _boxSize(suffixIcon).width
|
||||
+ contentPadding.right));
|
||||
|
||||
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);
|
||||
boxToBaseline[label] = _layoutLineBox(
|
||||
label,
|
||||
boxConstraints.copyWith(maxWidth: inputWidth),
|
||||
);
|
||||
boxToBaseline[hint] = _layoutLineBox(
|
||||
hint,
|
||||
boxConstraints.copyWith(minWidth: inputWidth, maxWidth: inputWidth),
|
||||
);
|
||||
boxToBaseline[counter] = _layoutLineBox(counter, boxConstraints);
|
||||
|
||||
// The helper or error text can occupy the full width less the space
|
||||
// occupied by the icon and counter.
|
||||
boxConstraints = boxConstraints.copyWith(
|
||||
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) {
|
||||
// The height of the input needs to accommodate label above and counter and
|
||||
// helperError below, when they exist.
|
||||
const double subtextGap = 8.0;
|
||||
subtextBaseline = containerHeight + subtextGap + aboveBaseline;
|
||||
subtextHeight = subtextGap + aboveBaseline + belowBaseline;
|
||||
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 * 2;
|
||||
final _HelperError helperErrorWidget = decoration.helperError;
|
||||
final double helperErrorHeight = helperErrorWidget.helperText == null
|
||||
? 0
|
||||
: helperError.size.height + subtextGap * 2;
|
||||
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 (helperErrorWidget.helperText != null) {
|
||||
subtextHelperBaseline =
|
||||
containerHeight + subtextGap + boxToBaseline[helperError];
|
||||
subtextHelperHeight = helperError.size.height + subtextGap;
|
||||
}
|
||||
final double subtextBaseline = math.max(
|
||||
subtextCounterBaseline,
|
||||
subtextHelperBaseline,
|
||||
);
|
||||
final double subtextHeight = math.max(
|
||||
subtextCounterHeight,
|
||||
subtextHelperHeight,
|
||||
);
|
||||
|
||||
return _RenderDecorationLayout(
|
||||
boxToBaseline: boxToBaseline,
|
||||
@ -1370,15 +1452,18 @@ class _Decorator extends RenderObjectWidget {
|
||||
@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 +1475,7 @@ class _Decorator extends RenderObjectWidget {
|
||||
textDirection: textDirection,
|
||||
textBaseline: textBaseline,
|
||||
isFocused: isFocused,
|
||||
expands: expands,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1399,6 +1485,7 @@ class _Decorator extends RenderObjectWidget {
|
||||
..decoration = decoration
|
||||
..textDirection = textDirection
|
||||
..textBaseline = textBaseline
|
||||
..expands = expands
|
||||
..isFocused = isFocused;
|
||||
}
|
||||
}
|
||||
@ -1461,6 +1548,7 @@ class InputDecorator extends StatefulWidget {
|
||||
this.baseStyle,
|
||||
this.textAlign,
|
||||
this.isFocused = false,
|
||||
this.expands = false,
|
||||
this.isEmpty = false,
|
||||
this.child,
|
||||
}) : assert(isFocused != null),
|
||||
@ -1495,6 +1583,19 @@ class InputDecorator extends StatefulWidget {
|
||||
/// 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 +1634,7 @@ class InputDecorator extends StatefulWidget {
|
||||
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 +2030,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
textDirection: textDirection,
|
||||
textBaseline: textBaseline,
|
||||
isFocused: isFocused,
|
||||
expands: widget.expands,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,8 @@ class TextField extends StatefulWidget {
|
||||
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 @@ class TextField extends StatefulWidget {
|
||||
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 @@ class TextField extends StatefulWidget {
|
||||
/// {@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 @@ class TextField extends StatefulWidget {
|
||||
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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
textAlign: widget.textAlign,
|
||||
isFocused: focusNode.hasFocus,
|
||||
isEmpty: controller.value.text.isEmpty,
|
||||
expands: widget.expands,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
@ -1801,9 +1801,9 @@ abstract class RenderBox extends RenderObject {
|
||||
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.
|
||||
|
||||
|
@ -144,6 +144,8 @@ class RenderEditable extends RenderBox {
|
||||
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 @@ class RenderEditable extends RenderBox {
|
||||
}) : 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 @@ class RenderEditable extends RenderBox {
|
||||
_showCursor = showCursor ?? ValueNotifier<bool>(false),
|
||||
_hasFocus = hasFocus ?? false,
|
||||
_maxLines = maxLines,
|
||||
_minLines = minLines,
|
||||
_expands = expands,
|
||||
_selectionColor = selectionColor,
|
||||
_selection = selection,
|
||||
_offset = offset,
|
||||
@ -691,6 +705,29 @@ class RenderEditable extends RenderBox {
|
||||
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;
|
||||
@ -1150,8 +1187,28 @@ class RenderEditable extends RenderBox {
|
||||
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;
|
||||
@ -1613,6 +1670,8 @@ class RenderEditable extends RenderBox {
|
||||
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));
|
||||
|
@ -278,6 +278,8 @@ class EditableText extends StatefulWidget {
|
||||
this.locale,
|
||||
this.textScaleFactor,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.expands = false,
|
||||
this.autofocus = false,
|
||||
this.selectionColor,
|
||||
this.selectionControls,
|
||||
@ -310,6 +312,16 @@ class EditableText extends StatefulWidget {
|
||||
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 @@ class EditableText extends StatefulWidget {
|
||||
/// 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 @@ class EditableText extends StatefulWidget {
|
||||
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 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
// 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 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
: _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 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
this.showCursor,
|
||||
this.hasFocus,
|
||||
this.maxLines,
|
||||
this.minLines,
|
||||
this.expands,
|
||||
this.strutStyle,
|
||||
this.selectionColor,
|
||||
this.textScaleFactor,
|
||||
@ -1433,6 +1517,8 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
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 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
showCursor: showCursor,
|
||||
hasFocus: hasFocus,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
expands: expands,
|
||||
strutStyle: strutStyle,
|
||||
selectionColor: selectionColor,
|
||||
textScaleFactor: textScaleFactor,
|
||||
@ -1492,6 +1580,8 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
..showCursor = showCursor
|
||||
..hasFocus = hasFocus
|
||||
..maxLines = maxLines
|
||||
..minLines = minLines
|
||||
..expands = expands
|
||||
..strutStyle = strutStyle
|
||||
..selectionColor = selectionColor
|
||||
..textScaleFactor = textScaleFactor
|
||||
|
@ -993,6 +993,100 @@ void main() {
|
||||
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 @@ void main() {
|
||||
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(
|
||||
|
@ -6,6 +6,49 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
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();
|
||||
|
||||
@ -89,49 +132,6 @@ void main() {
|
||||
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);
|
||||
});
|
||||
|
||||
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
|
@ -174,6 +174,24 @@ void main() {
|
||||
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 @@ void main() {
|
||||
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,278 @@ void main() {
|
||||
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);
|
||||
});
|
||||
|
||||
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 * 2;
|
||||
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 = 28.0;
|
||||
expect(inputBox.size.height, height - padding - helperTextSpace);
|
||||
|
||||
// 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();
|
||||
|
@ -56,6 +56,7 @@ void main() {
|
||||
' │ 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'
|
||||
|
Loading…
Reference in New Issue
Block a user