Fix Vertical Alignment Regression (#34859)
Change the way outlined inputs vertically align their text to be more similar to how it used to be before a refactor. Fixes an edge case uncovered by a SCUBA test.
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index e1b601d..4737888 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -574,6 +574,7 @@
const _RenderDecorationLayout({
this.boxToBaseline,
this.inputBaseline, // for InputBorderType.underline
+ this.outlineBaseline, // for InputBorderType.outline
this.subtextBaseline,
this.containerHeight,
this.subtextHeight,
@@ -581,6 +582,7 @@
final Map<RenderBox, double> boxToBaseline;
final double inputBaseline;
+ final double outlineBaseline;
final double subtextBaseline; // helper/error counter
final double containerHeight;
final double subtextHeight;
@@ -1055,13 +1057,32 @@
- 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 maxVerticalOffset = maxContentHeight - alignableHeight;
+ final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor;
final double inputBaseline = topInputBaseline + textAlignVerticalOffset;
+ // The three main alignments for the baseline when an outline is present are
+ //
+ // * top (-1.0): topmost point considering padding.
+ // * center (0.0): the absolute center of the input ignoring padding but
+ // accommodating the border and floating label.
+ // * bottom (1.0): bottommost point considering padding.
+ //
+ // That means that if the padding is uneven, center is not the exact
+ // midpoint of top and bottom. To account for this, the above center and
+ // below center alignments are interpolated independently.
+ final double outlineCenterBaseline = inputInternalBaseline
+ + baselineAdjustment / 2.0
+ + (containerHeight - (2.0 + inputHeight)) / 2.0;
+ final double outlineTopBaseline = topInputBaseline;
+ final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset;
+ final double outlineBaseline = _interpolateThree(
+ outlineTopBaseline,
+ outlineCenterBaseline,
+ outlineBottomBaseline,
+ textAlignVertical,
+ );
+
// Find the positions of the text below the input when it exists.
double subtextCounterBaseline = 0;
double subtextHelperBaseline = 0;
@@ -1090,11 +1111,41 @@
boxToBaseline: boxToBaseline,
containerHeight: containerHeight,
inputBaseline: inputBaseline,
+ outlineBaseline: outlineBaseline,
subtextBaseline: subtextBaseline,
subtextHeight: subtextHeight,
);
}
+ // Interpolate between three stops using textAlignVertical. This is used to
+ // calculate the outline baseline, which ignores padding when the alignment is
+ // middle. When the alignment is less than zero, it interpolates between the
+ // centered text box's top and the top of the content padding. When the
+ // alignment is greater than zero, it interpolates between the centered box's
+ // top and the position that would align the bottom of the box with the bottom
+ // padding.
+ double _interpolateThree(double begin, double middle, double end, TextAlignVertical textAlignVertical) {
+ if (textAlignVertical.y <= 0) {
+ // It's possible for begin, middle, and end to not be in order because of
+ // excessive padding. Those cases are handled by using middle.
+ if (begin >= middle) {
+ return middle;
+ }
+ // Do a standard linear interpolation on the first half, between begin and
+ // middle.
+ final double t = textAlignVertical.y + 1;
+ return begin + (middle - begin) * t;
+ }
+
+ if (middle >= end) {
+ return middle;
+ }
+ // Do a standard linear interpolation on the second half, between middle and
+ // end.
+ final double t = textAlignVertical.y;
+ return middle + (end - middle) * t;
+ }
+
@override
double computeMinIntrinsicWidth(double height) {
return _minWidth(icon, height)
@@ -1199,7 +1250,7 @@
final double right = overallWidth - contentPadding.right;
height = layout.containerHeight;
- baseline = layout.inputBaseline;
+ baseline = _isOutlineAligned ? layout.outlineBaseline : layout.inputBaseline;
if (icon != null) {
double x;
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart
index a34039d..86ca4cc 100644
--- a/packages/flutter/test/material/input_decorator_test.dart
+++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -1495,7 +1495,7 @@
);
// Below the center aligned case.
- expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(564.0, .0001));
});
});
@@ -1680,8 +1680,8 @@
);
// 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));
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(564.0, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(480.0, .0001));
});
testWidgets('InputDecorator tall prefix with border align double', (WidgetTester tester) async {
@@ -1711,8 +1711,8 @@
);
// 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));
+ expect(tester.getTopLeft(find.text(text)).dy, closeTo(354.3, .0001));
+ expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(270.3, .0001));
});
});
@@ -1794,6 +1794,199 @@
});
});
+ group('OutlineInputBorder', () {
+ group('default alignment', () {
+ testWidgets('Centers when border', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
+ expect(tester.getTopLeft(find.text('text')).dy, 19.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 35.0);
+ expect(getBorderBottom(tester), 56.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+
+ testWidgets('Centers when border and label', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: const InputDecoration(
+ labelText: 'label',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
+ expect(tester.getTopLeft(find.text('text')).dy, 19.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 35.0);
+ expect(getBorderBottom(tester), 56.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+
+ testWidgets('Centers when border and contentPadding', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(
+ 12.0, 14.0,
+ 8.0, 14.0,
+ ),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 44.0));
+ expect(tester.getTopLeft(find.text('text')).dy, 13.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 29.0);
+ expect(getBorderBottom(tester), 44.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+
+ testWidgets('Centers when border and contentPadding and label', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: const InputDecoration(
+ labelText: 'label',
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(
+ 12.0, 14.0,
+ 8.0, 14.0,
+ ),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 44.0));
+ expect(tester.getTopLeft(find.text('text')).dy, 13.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 29.0);
+ expect(getBorderBottom(tester), 44.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+
+ testWidgets('Centers when border and lopsided contentPadding and label', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: const InputDecoration(
+ labelText: 'label',
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(
+ 12.0, 104.0,
+ 8.0, 0.0,
+ ),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 120.0));
+ expect(tester.getTopLeft(find.text('text')).dy, 51.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 67.0);
+ expect(getBorderBottom(tester), 120.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+ });
+
+ group('3 point interpolation alignment', () {
+ testWidgets('top align includes padding', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ expands: true,
+ textAlignVertical: TextAlignVertical.top,
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(
+ 12.0, 24.0,
+ 8.0, 2.0,
+ ),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+ // Aligned to the top including the 24px padding.
+ expect(tester.getTopLeft(find.text('text')).dy, 24.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
+ expect(getBorderBottom(tester), 600.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+
+ testWidgets('center align ignores padding', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ expands: true,
+ textAlignVertical: TextAlignVertical.center,
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(
+ 12.0, 24.0,
+ 8.0, 2.0,
+ ),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+ // Baseline is on the center of the 600px high input.
+ expect(tester.getTopLeft(find.text('text')).dy, 291.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 307.0);
+ expect(getBorderBottom(tester), 600.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+
+ testWidgets('bottom align includes padding', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ expands: true,
+ textAlignVertical: TextAlignVertical.bottom,
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(
+ 12.0, 24.0,
+ 8.0, 2.0,
+ ),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+ // Includes bottom padding of 2px.
+ expect(tester.getTopLeft(find.text('text')).dy, 582.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 598.0);
+ expect(getBorderBottom(tester), 600.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+
+ testWidgets('padding exceeds middle keeps top at middle', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ buildInputDecorator(
+ expands: true,
+ textAlignVertical: TextAlignVertical.top,
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(
+ 12.0, 504.0,
+ 8.0, 0.0,
+ ),
+ ),
+ ),
+ );
+
+ expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
+ // Same position as the center example above.
+ expect(tester.getTopLeft(find.text('text')).dy, 291.0);
+ expect(tester.getBottomLeft(find.text('text')).dy, 307.0);
+ expect(getBorderBottom(tester), 600.0);
+ expect(getBorderWeight(tester), 1.0);
+ });
+ });
+ });
+
testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
await tester.pumpWidget(
buildInputDecorator(