Text field vertical align (#34355)
Adds the `textAlignVertical` param to TextField and InputDecorator, allowing arbitrary vertical positioning of text in its input.
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index f43cf04..e1b601d 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -574,7 +574,6 @@
const _RenderDecorationLayout({
this.boxToBaseline,
this.inputBaseline, // for InputBorderType.underline
- this.outlineBaseline, // for InputBorderType.outline
this.subtextBaseline,
this.containerHeight,
this.subtextHeight,
@@ -582,7 +581,6 @@
final Map<RenderBox, double> boxToBaseline;
final double inputBaseline;
- final double outlineBaseline;
final double subtextBaseline; // helper/error counter
final double containerHeight;
final double subtextHeight;
@@ -596,6 +594,7 @@
@required TextBaseline textBaseline,
@required bool isFocused,
@required bool expands,
+ TextAlignVertical textAlignVertical,
}) : assert(decoration != null),
assert(textDirection != null),
assert(textBaseline != null),
@@ -603,6 +602,7 @@
_decoration = decoration,
_textDirection = textDirection,
_textBaseline = textBaseline,
+ _textAlignVertical = textAlignVertical,
_isFocused = isFocused,
_expands = expands;
@@ -746,6 +746,27 @@
markNeedsLayout();
}
+ TextAlignVertical get textAlignVertical {
+ if (_textAlignVertical == null) {
+ return _isOutlineAligned ? TextAlignVertical.center : TextAlignVertical.top;
+ }
+ return _textAlignVertical;
+ }
+ TextAlignVertical _textAlignVertical;
+ set textAlignVertical(TextAlignVertical value) {
+ assert(value != null);
+ if (_textAlignVertical == value) {
+ return;
+ }
+ // No need to relayout if the effective value is still the same.
+ if (textAlignVertical.y == value.y) {
+ _textAlignVertical = value;
+ return;
+ }
+ _textAlignVertical = value;
+ markNeedsLayout();
+ }
+
bool get isFocused => _isFocused;
bool _isFocused;
set isFocused(bool value) {
@@ -766,6 +787,12 @@
markNeedsLayout();
}
+ // Indicates that the decoration should be aligned to accommodate an outline
+ // border.
+ bool get _isOutlineAligned {
+ return !decoration.isCollapsed && decoration.border.isOutline;
+ }
+
@override
void attach(PipelineOwner owner) {
super.attach(owner);
@@ -862,7 +889,7 @@
EdgeInsets get contentPadding => decoration.contentPadding;
- // Lay out the given box if needed, and return its baseline
+ // Lay out the given box if needed, and return its baseline.
double _layoutLineBox(RenderBox box, BoxConstraints constraints) {
if (box == null) {
return 0.0;
@@ -1006,21 +1033,34 @@
? maxContainerHeight
: math.min(contentHeight, maxContainerHeight);
- // Always position the prefix/suffix in the same place (baseline).
+ // Try to consider the prefix/suffix as part of the text when aligning it.
+ // If the prefix/suffix overflows however, allow it to extend outside of the
+ // input and align the remaining part of the text and prefix/suffix.
final double overflow = math.max(0, contentHeight - maxContainerHeight);
- final double baselineAdjustment = fixAboveInput - overflow;
+ // Map textAlignVertical from -1:1 to 0:1 so that it can be used to scale
+ // the baseline from its minimum to maximum values.
+ final double textAlignVerticalFactor = (textAlignVertical.y + 1.0) / 2.0;
+ // Adjust to try to fit top overflow inside the input on an inverse scale of
+ // textAlignVertical, so that top aligned text adjusts the most and bottom
+ // aligned text doesn't adjust at all.
+ final double baselineAdjustment = fixAboveInput - overflow * (1 - textAlignVerticalFactor);
// The baselines that will be used to draw the actual input text content.
- final double inputBaseline = contentPadding.top
+ final double topInputBaseline = 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;
+ final double maxContentHeight = containerHeight
+ - contentPadding.top
+ - topHeight
+ - contentPadding.bottom;
+ final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
+ // When outline aligned, the baseline is vertically centered by default, and
+ // outlinePadding is used to account for the presence of the border and
+ // floating label.
+ final double outlinePadding = _isOutlineAligned ? 10.0 : 0;
+ final double textAlignVerticalOffset = (maxContentHeight - alignableHeight - outlinePadding) * textAlignVerticalFactor;
+ final double inputBaseline = topInputBaseline + textAlignVerticalOffset;
// Find the positions of the text below the input when it exists.
double subtextCounterBaseline = 0;
@@ -1050,7 +1090,6 @@
boxToBaseline: boxToBaseline,
containerHeight: containerHeight,
inputBaseline: inputBaseline,
- outlineBaseline: outlineBaseline,
subtextBaseline: subtextBaseline,
subtextHeight: subtextHeight,
);
@@ -1160,9 +1199,7 @@
final double right = overallWidth - contentPadding.right;
height = layout.containerHeight;
- baseline = decoration.isCollapsed || !decoration.border.isOutline
- ? layout.inputBaseline
- : layout.outlineBaseline;
+ baseline = layout.inputBaseline;
if (icon != null) {
double x;
@@ -1213,12 +1250,13 @@
start -= contentPadding.left;
start += centerLayout(prefixIcon, start);
}
- if (label != null)
+ if (label != null) {
if (decoration.alignLabelWithHint) {
baselineLayout(label, start);
} else {
centerLayout(label, start);
}
+ }
if (prefix != null)
start += baselineLayout(prefix, start);
if (input != null)
@@ -1512,6 +1550,7 @@
class _Decorator extends RenderObjectWidget {
const _Decorator({
Key key,
+ @required this.textAlignVertical,
@required this.decoration,
@required this.textDirection,
@required this.textBaseline,
@@ -1526,6 +1565,7 @@
final _Decoration decoration;
final TextDirection textDirection;
final TextBaseline textBaseline;
+ final TextAlignVertical textAlignVertical;
final bool isFocused;
final bool expands;
@@ -1538,6 +1578,7 @@
decoration: decoration,
textDirection: textDirection,
textBaseline: textBaseline,
+ textAlignVertical: textAlignVertical,
isFocused: isFocused,
expands: expands,
);
@@ -1612,6 +1653,7 @@
this.decoration,
this.baseStyle,
this.textAlign,
+ this.textAlignVertical,
this.isFocused = false,
this.isHovering = false,
this.expands = false,
@@ -1643,6 +1685,20 @@
/// How the text in the decoration should be aligned horizontally.
final TextAlign textAlign;
+ /// {@template flutter.widgets.inputDecorator.textAlignVertical}
+ /// How the text should be aligned vertically.
+ ///
+ /// Determines the alignment of the baseline within the available space of
+ /// the input (typically a TextField). For example, TextAlignVertical.top will
+ /// place the baseline such that the text, and any attached decoration like
+ /// prefix and suffix, is as close to the top of the input as possible without
+ /// overflowing. The heights of the prefix and suffix are similarly included
+ /// for other alignment values. If the height is greater than the height
+ /// available, then the prefix and suffix will be allowed to overflow first
+ /// before the text scrolls.
+ /// {@endtemplate}
+ final TextAlignVertical textAlignVertical;
+
/// Whether the input field has focus.
///
/// Determines the position of the label text and the color and weight of the
@@ -2148,6 +2204,7 @@
),
textDirection: textDirection,
textBaseline: textBaseline,
+ textAlignVertical: widget.textAlignVertical,
isFocused: isFocused,
expands: widget.expands,
);
@@ -3468,3 +3525,42 @@
properties.add(DiagnosticsProperty<bool>('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint));
}
}
+
+/// The vertical alignment of text within an input.
+///
+/// A single [y] value that can range from -1.0 to 1.0. -1.0 aligns to the top
+/// of the input so that the top of the first line of text fits within the input
+/// and its padding. 0.0 aligns to the center of the input. 1.0 aligns so that
+/// the bottom of the last line of text aligns with the bottom interior edge of
+/// the input.
+///
+/// See also:
+///
+/// * [TextField.textAlignVertical], which is passed on to the [InputDecorator].
+/// * [InputDecorator.textAlignVertical], which defines the alignment of
+/// prefix, input, and suffix, within the [InputDecorator].
+class TextAlignVertical {
+ /// Construct TextAlignVertical from any given y value.
+ const TextAlignVertical({
+ @required this.y,
+ }) : assert(y != null),
+ assert(y >= -1.0 && y <= 1.0);
+
+ /// A value ranging from -1.0 to 1.0 that defines the topmost and bottommost
+ /// locations of the top and bottom of the input text box.
+ final double y;
+
+ /// Aligns a TextField's input Text with the topmost location within the
+ /// TextField.
+ static const TextAlignVertical top = TextAlignVertical(y: -1.0);
+ /// Aligns a TextField's input Text to the center of the TextField.
+ static const TextAlignVertical center = TextAlignVertical(y: 0.0);
+ /// Aligns a TextField's input Text with the bottommost location within the
+ /// TextField.
+ static const TextAlignVertical bottom = TextAlignVertical(y: 1.0);
+
+ @override
+ String toString() {
+ return '$runtimeType(y: $y)';
+ }
+}
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index 1c7cc2c..e8061e2 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -149,6 +149,7 @@
this.style,
this.strutStyle,
this.textAlign = TextAlign.start,
+ this.textAlignVertical,
this.textDirection,
this.readOnly = false,
this.showCursor,
@@ -278,6 +279,9 @@
/// {@macro flutter.widgets.editableText.textAlign}
final TextAlign textAlign;
+ /// {@macro flutter.material.inputDecorator.textAlignVertical}
+ final TextAlignVertical textAlignVertical;
+
/// {@macro flutter.widgets.editableText.textDirection}
final TextDirection textDirection;
@@ -506,6 +510,7 @@
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
+ properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
@@ -1009,6 +1014,7 @@
decoration: _getEffectiveDecoration(),
baseStyle: widget.style,
textAlign: widget.textAlign,
+ textAlignVertical: widget.textAlignVertical,
isHovering: _isHovering,
isFocused: focusNode.hasFocus,
isEmpty: controller.value.text.isEmpty,
diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart
index a8b674b..db37d04 100644
--- a/packages/flutter/lib/src/rendering/object.dart
+++ b/packages/flutter/lib/src/rendering/object.dart
@@ -2153,8 +2153,9 @@
renderers.add(renderer);
}
final Matrix4 transform = Matrix4.identity();
- for (int index = renderers.length - 1; index > 0; index -= 1)
+ for (int index = renderers.length - 1; index > 0; index -= 1) {
renderers[index].applyPaintTransform(renderers[index - 1], transform);
+ }
return transform;
}
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart
index 88678da..a34039d 100644
--- a/packages/flutter/test/material/input_decorator_test.dart
+++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -15,10 +15,12 @@
InputDecoration decoration = const InputDecoration(),
InputDecorationTheme inputDecorationTheme,
TextDirection textDirection = TextDirection.ltr,
+ bool expands = false,
bool isEmpty = false,
bool isFocused = false,
bool isHovering = false,
TextStyle baseStyle,
+ TextAlignVertical textAlignVertical,
Widget child = const Text(
'text',
style: TextStyle(fontFamily: 'Ahem', fontSize: 16.0),
@@ -37,11 +39,13 @@
child: Directionality(
textDirection: textDirection,
child: InputDecorator(
+ expands: expands,
decoration: decoration,
isEmpty: isEmpty,
isFocused: isFocused,
isHovering: isHovering,
baseStyle: baseStyle,
+ textAlignVertical: textAlignVertical,
child: child,
),
),
@@ -277,69 +281,216 @@
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
});
- testWidgets('InputDecorator alignLabelWithHint for multiline TextField no-strut', (WidgetTester tester) async {
- Widget buildFrame(bool alignLabelWithHint) {
- return MaterialApp(
- home: Material(
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: TextField(
- maxLines: 8,
- decoration: InputDecoration(
- labelText: 'label',
- alignLabelWithHint: alignLabelWithHint,
- hintText: 'hint',
- ),
- strutStyle: StrutStyle.disabled,
- ),
- ),
- ),
- );
- }
-
- // alignLabelWithHint: false centers the label in the TextField
- await tester.pumpWidget(buildFrame(false));
- await tester.pumpAndSettle();
- expect(tester.getTopLeft(find.text('label')).dy, 76.0);
- expect(tester.getBottomLeft(find.text('label')).dy, 92.0);
-
- // alignLabelWithHint: true aligns the label with the hint.
- await tester.pumpWidget(buildFrame(true));
- await tester.pumpAndSettle();
- expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
- expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
- });
-
- testWidgets('InputDecorator alignLabelWithHint for multiline TextField', (WidgetTester tester) async {
- Widget buildFrame(bool alignLabelWithHint) {
- return MaterialApp(
- home: Material(
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: TextField(
- maxLines: 8,
- decoration: InputDecoration(
- labelText: 'label',
- alignLabelWithHint: alignLabelWithHint,
- hintText: 'hint',
+ group('alignLabelWithHint', () {
+ group('expands false', () {
+ testWidgets('multiline TextField no-strut', (WidgetTester tester) async {
+ const String text = 'text';
+ final FocusNode focusNode = FocusNode();
+ final TextEditingController controller = TextEditingController();
+ Widget buildFrame(bool alignLabelWithHint) {
+ return MaterialApp(
+ home: Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: TextField(
+ controller: controller,
+ focusNode: focusNode,
+ maxLines: 8,
+ decoration: InputDecoration(
+ labelText: 'label',
+ alignLabelWithHint: alignLabelWithHint,
+ hintText: 'hint',
+ ),
+ strutStyle: StrutStyle.disabled,
+ ),
),
),
- ),
- ),
- );
- }
+ );
+ }
- // alignLabelWithHint: false centers the label in the TextField
- await tester.pumpWidget(buildFrame(false));
- await tester.pumpAndSettle();
- expect(tester.getTopLeft(find.text('label')).dy, 76.0);
- expect(tester.getBottomLeft(find.text('label')).dy, 92.0);
+ // alignLabelWithHint: false centers the label in the TextField.
+ await tester.pumpWidget(buildFrame(false));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, 76.0);
+ expect(tester.getBottomLeft(find.text('label')).dy, 92.0);
- // alignLabelWithHint: true aligns the label with the hint.
- await tester.pumpWidget(buildFrame(true));
- await tester.pumpAndSettle();
- expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
- expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
+ // Entering text still happens at the top.
+ await tester.enterText(find.byType(TextField), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 28.0);
+ controller.clear();
+ focusNode.unfocus();
+
+ // alignLabelWithHint: true aligns the label with the hint.
+ await tester.pumpWidget(buildFrame(true));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
+ expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
+
+ // Entering text still happens at the top.
+ await tester.enterText(find.byType(TextField), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 28.0);
+ controller.clear();
+ focusNode.unfocus();
+ });
+
+ testWidgets('multiline TextField', (WidgetTester tester) async {
+ const String text = 'text';
+ final FocusNode focusNode = FocusNode();
+ final TextEditingController controller = TextEditingController();
+ Widget buildFrame(bool alignLabelWithHint) {
+ return MaterialApp(
+ home: Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: TextField(
+ controller: controller,
+ focusNode: focusNode,
+ maxLines: 8,
+ decoration: InputDecoration(
+ labelText: 'label',
+ alignLabelWithHint: alignLabelWithHint,
+ hintText: 'hint',
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ // alignLabelWithHint: false centers the label in the TextField.
+ await tester.pumpWidget(buildFrame(false));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, 76.0);
+ expect(tester.getBottomLeft(find.text('label')).dy, 92.0);
+
+ // Entering text still happens at the top.
+ await tester.enterText(find.byType(InputDecorator), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 28.0);
+ controller.clear();
+ focusNode.unfocus();
+
+ // alignLabelWithHint: true aligns the label with the hint.
+ await tester.pumpWidget(buildFrame(true));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
+ expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
+
+ // Entering text still happens at the top.
+ await tester.enterText(find.byType(InputDecorator), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 28.0);
+ controller.clear();
+ focusNode.unfocus();
+ });
+ });
+
+ group('expands true', () {
+ testWidgets('multiline TextField', (WidgetTester tester) async {
+ const String text = 'text';
+ final FocusNode focusNode = FocusNode();
+ final TextEditingController controller = TextEditingController();
+ Widget buildFrame(bool alignLabelWithHint) {
+ return MaterialApp(
+ home: Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: TextField(
+ controller: controller,
+ focusNode: focusNode,
+ maxLines: null,
+ expands: true,
+ decoration: InputDecoration(
+ labelText: 'label',
+ alignLabelWithHint: alignLabelWithHint,
+ hintText: 'hint',
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ // alignLabelWithHint: false centers the label in the TextField.
+ await tester.pumpWidget(buildFrame(false));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, 292.0);
+ expect(tester.getBottomLeft(find.text('label')).dy, 308.0);
+
+ // Entering text still happens at the top.
+ await tester.enterText(find.byType(InputDecorator), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 28.0);
+ controller.clear();
+ focusNode.unfocus();
+
+ // alignLabelWithHint: true aligns the label with the hint at the top.
+ await tester.pumpWidget(buildFrame(true));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, 28.0);
+ expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
+ expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
+
+ // Entering text still happens at the top.
+ await tester.enterText(find.byType(InputDecorator), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 28.0);
+ controller.clear();
+ focusNode.unfocus();
+ });
+
+ testWidgets('multiline TextField with outline border', (WidgetTester tester) async {
+ const String text = 'text';
+ final FocusNode focusNode = FocusNode();
+ final TextEditingController controller = TextEditingController();
+ Widget buildFrame(bool alignLabelWithHint) {
+ return MaterialApp(
+ home: Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: TextField(
+ controller: controller,
+ focusNode: focusNode,
+ maxLines: null,
+ expands: true,
+ decoration: InputDecoration(
+ labelText: 'label',
+ alignLabelWithHint: alignLabelWithHint,
+ hintText: 'hint',
+ border: OutlineInputBorder(
+ borderSide: const BorderSide(width: 1, color: Colors.black, style: BorderStyle.solid),
+ borderRadius: BorderRadius.circular(0),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ // alignLabelWithHint: false centers the label in the TextField.
+ await tester.pumpWidget(buildFrame(false));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, 292.0);
+ expect(tester.getBottomLeft(find.text('label')).dy, 308.0);
+
+ // Entering text happens in the center as well.
+ await tester.enterText(find.byType(InputDecorator), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 291.0);
+ controller.clear();
+ focusNode.unfocus();
+
+ // alignLabelWithHint: true aligns keeps the label in the center because
+ // that's where the hint is.
+ await tester.pumpWidget(buildFrame(true));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('label')).dy, 291.0);
+ expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
+ expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
+
+ // Entering text still happens in the center.
+ await tester.enterText(find.byType(InputDecorator), text);
+ expect(tester.getTopLeft(find.text(text)).dy, 291.0);
+ controller.clear();
+ focusNode.unfocus();
+ });
+ });
});
// Overall height for this InputDecorator is 40.0dps
@@ -1178,6 +1329,471 @@
expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0);
});
+ group('textAlignVertical position', () {
+ group('simple case', () {
+ testWidgets('align top (default)', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true, // so we have a tall input where align can vary
+ decoration: const InputDecoration(
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.top, // default when no border
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Same as the default case above.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(12.0, .0001));
+ });
+
+ testWidgets('align center', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: const InputDecoration(
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.center,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Below the top aligned case.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(290.0, .0001));
+ });
+
+ testWidgets('align bottom', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: const InputDecoration(
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.bottom,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Below the center aligned case.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(568.0, .0001));
+ });
+
+ testWidgets('align as a double', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: const InputDecoration(
+ filled: true,
+ ),
+ textAlignVertical: const TextAlignVertical(y: 0.75),
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // In between the center and bottom aligned cases.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(498.5, .0001));
+ });
+ });
+
+ group('outline border', () {
+ testWidgets('align top', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true, // so we have a tall input where align can vary
+ decoration: const InputDecoration(
+ filled: true,
+ border: OutlineInputBorder(),
+ ),
+ textAlignVertical: TextAlignVertical.top,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Similar to the case without a border, but with a little extra room at
+ // the top to make room for the border.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(24.0, .0001));
+ });
+
+ testWidgets('align center (default)', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: const InputDecoration(
+ filled: true,
+ border: OutlineInputBorder(),
+ ),
+ textAlignVertical: TextAlignVertical.center, // default when border
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Below the top aligned case.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(289.0, .0001));
+ });
+
+ testWidgets('align bottom', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: const InputDecoration(
+ filled: true,
+ border: OutlineInputBorder(),
+ ),
+ textAlignVertical: TextAlignVertical.bottom,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Below the center aligned case.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
+ });
+ });
+
+ group('prefix', () {
+ testWidgets('InputDecorator tall prefix align top', (WidgetTester tester) async {
+ const Key pKey = Key('p');
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ decoration: InputDecoration(
+ prefix: Container(
+ key: pKey,
+ height: 100,
+ width: 10,
+ ),
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.top, // default when no border
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Same as the default case above.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(96, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
+ });
+
+ testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async {
+ const Key pKey = Key('p');
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ decoration: InputDecoration(
+ prefix: Container(
+ key: pKey,
+ height: 100,
+ width: 10,
+ ),
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.center,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Same as the default case above.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(96.0, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
+ });
+
+ testWidgets('InputDecorator tall prefix align bottom', (WidgetTester tester) async {
+ const Key pKey = Key('p');
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ decoration: InputDecoration(
+ prefix: Container(
+ key: pKey,
+ height: 100,
+ width: 10,
+ ),
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.bottom,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Top of the input + 100 prefix height - overlap
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(96.0, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
+ });
+ });
+
+ group('outline border and prefix', () {
+ testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async {
+ const Key pKey = Key('p');
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: InputDecoration(
+ border: const OutlineInputBorder(),
+ prefix: Container(
+ key: pKey,
+ height: 100,
+ width: 10,
+ ),
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.center, // default when border
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // In the middle of the expanded InputDecorator.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(331.0, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(247.0, .0001));
+ });
+
+ testWidgets('InputDecorator tall prefix with border align top', (WidgetTester tester) async {
+ const Key pKey = Key('p');
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: InputDecoration(
+ border: const OutlineInputBorder(),
+ prefix: Container(
+ key: pKey,
+ height: 100,
+ width: 10,
+ ),
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.top,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Above the center example.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(108.0, .0001));
+ // The prefix is positioned at the top of the input, so this value is
+ // the same as the top aligned test without a prefix.
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, 24.0);
+ });
+
+ testWidgets('InputDecorator tall prefix with border align bottom', (WidgetTester tester) async {
+ const Key pKey = Key('p');
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: InputDecoration(
+ border: const OutlineInputBorder(),
+ prefix: Container(
+ key: pKey,
+ height: 100,
+ width: 10,
+ ),
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.bottom,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Below the center example.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(470.0, .0001));
+ });
+
+ testWidgets('InputDecorator tall prefix with border align double', (WidgetTester tester) async {
+ const Key pKey = Key('p');
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true,
+ decoration: InputDecoration(
+ border: const OutlineInputBorder(),
+ prefix: Container(
+ key: pKey,
+ height: 100,
+ width: 10,
+ ),
+ filled: true,
+ ),
+ textAlignVertical: const TextAlignVertical(y: 0.1),
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // Between the top and center examples.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(353.3, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(269.3, .0001));
+ });
+ });
+
+ group('label', () {
+ testWidgets('align top (default)', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true, // so we have a tall input where align can vary
+ decoration: const InputDecoration(
+ labelText: 'label',
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.top, // default
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // The label causes the text to start slightly lower than it would
+ // otherwise.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(28.0, .0001));
+ });
+
+ testWidgets('align center', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true, // so we have a tall input where align can vary
+ decoration: const InputDecoration(
+ labelText: 'label',
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.center,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // The label reduces the amount of space available for text, so the
+ // center is slightly lower.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(298.0, .0001));
+ });
+
+ testWidgets('align bottom', (WidgetTester tester) async {
+ const String text = 'text';
+ await tester.pumpWidget(
+ buildInputDecorator(
+ // isEmpty: false (default)
+ // isFocused: false (default)
+ expands: true, // so we have a tall input where align can vary
+ decoration: const InputDecoration(
+ labelText: 'label',
+ filled: true,
+ ),
+ textAlignVertical: TextAlignVertical.bottom,
+ // Set the fontSize so that everything works out to whole numbers.
+ child: const Text(
+ text,
+ style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
+ ),
+ ),
+ );
+
+ // The label reduces the amount of space available for text, but the
+ // bottom line is still in the same place.
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(568.0, .0001));
+ });
+ });
+ });
+
testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(