Skip to content

Commit 63438b9

Browse files
authored
Fix Vertical Alignment Regression (flutter#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.
1 parent 2dd1418 commit 63438b9

File tree

2 files changed

+255
-11
lines changed

2 files changed

+255
-11
lines changed

packages/flutter/lib/src/material/input_decorator.dart

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -574,13 +574,15 @@ class _RenderDecorationLayout {
574574
const _RenderDecorationLayout({
575575
this.boxToBaseline,
576576
this.inputBaseline, // for InputBorderType.underline
577+
this.outlineBaseline, // for InputBorderType.outline
577578
this.subtextBaseline,
578579
this.containerHeight,
579580
this.subtextHeight,
580581
});
581582

582583
final Map<RenderBox, double> boxToBaseline;
583584
final double inputBaseline;
585+
final double outlineBaseline;
584586
final double subtextBaseline; // helper/error counter
585587
final double containerHeight;
586588
final double subtextHeight;
@@ -1055,13 +1057,32 @@ class _RenderDecoration extends RenderBox {
10551057
- topHeight
10561058
- contentPadding.bottom;
10571059
final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
1058-
// When outline aligned, the baseline is vertically centered by default, and
1059-
// outlinePadding is used to account for the presence of the border and
1060-
// floating label.
1061-
final double outlinePadding = _isOutlineAligned ? 10.0 : 0;
1062-
final double textAlignVerticalOffset = (maxContentHeight - alignableHeight - outlinePadding) * textAlignVerticalFactor;
1060+
final double maxVerticalOffset = maxContentHeight - alignableHeight;
1061+
final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor;
10631062
final double inputBaseline = topInputBaseline + textAlignVerticalOffset;
10641063

1064+
// The three main alignments for the baseline when an outline is present are
1065+
//
1066+
// * top (-1.0): topmost point considering padding.
1067+
// * center (0.0): the absolute center of the input ignoring padding but
1068+
// accommodating the border and floating label.
1069+
// * bottom (1.0): bottommost point considering padding.
1070+
//
1071+
// That means that if the padding is uneven, center is not the exact
1072+
// midpoint of top and bottom. To account for this, the above center and
1073+
// below center alignments are interpolated independently.
1074+
final double outlineCenterBaseline = inputInternalBaseline
1075+
+ baselineAdjustment / 2.0
1076+
+ (containerHeight - (2.0 + inputHeight)) / 2.0;
1077+
final double outlineTopBaseline = topInputBaseline;
1078+
final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset;
1079+
final double outlineBaseline = _interpolateThree(
1080+
outlineTopBaseline,
1081+
outlineCenterBaseline,
1082+
outlineBottomBaseline,
1083+
textAlignVertical,
1084+
);
1085+
10651086
// Find the positions of the text below the input when it exists.
10661087
double subtextCounterBaseline = 0;
10671088
double subtextHelperBaseline = 0;
@@ -1090,11 +1111,41 @@ class _RenderDecoration extends RenderBox {
10901111
boxToBaseline: boxToBaseline,
10911112
containerHeight: containerHeight,
10921113
inputBaseline: inputBaseline,
1114+
outlineBaseline: outlineBaseline,
10931115
subtextBaseline: subtextBaseline,
10941116
subtextHeight: subtextHeight,
10951117
);
10961118
}
10971119

1120+
// Interpolate between three stops using textAlignVertical. This is used to
1121+
// calculate the outline baseline, which ignores padding when the alignment is
1122+
// middle. When the alignment is less than zero, it interpolates between the
1123+
// centered text box's top and the top of the content padding. When the
1124+
// alignment is greater than zero, it interpolates between the centered box's
1125+
// top and the position that would align the bottom of the box with the bottom
1126+
// padding.
1127+
double _interpolateThree(double begin, double middle, double end, TextAlignVertical textAlignVertical) {
1128+
if (textAlignVertical.y <= 0) {
1129+
// It's possible for begin, middle, and end to not be in order because of
1130+
// excessive padding. Those cases are handled by using middle.
1131+
if (begin >= middle) {
1132+
return middle;
1133+
}
1134+
// Do a standard linear interpolation on the first half, between begin and
1135+
// middle.
1136+
final double t = textAlignVertical.y + 1;
1137+
return begin + (middle - begin) * t;
1138+
}
1139+
1140+
if (middle >= end) {
1141+
return middle;
1142+
}
1143+
// Do a standard linear interpolation on the second half, between middle and
1144+
// end.
1145+
final double t = textAlignVertical.y;
1146+
return middle + (end - middle) * t;
1147+
}
1148+
10981149
@override
10991150
double computeMinIntrinsicWidth(double height) {
11001151
return _minWidth(icon, height)
@@ -1199,7 +1250,7 @@ class _RenderDecoration extends RenderBox {
11991250
final double right = overallWidth - contentPadding.right;
12001251

12011252
height = layout.containerHeight;
1202-
baseline = layout.inputBaseline;
1253+
baseline = _isOutlineAligned ? layout.outlineBaseline : layout.inputBaseline;
12031254

12041255
if (icon != null) {
12051256
double x;

packages/flutter/test/material/input_decorator_test.dart

Lines changed: 198 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,7 +1495,7 @@ void main() {
14951495
);
14961496

14971497
// Below the center aligned case.
1498-
expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
1498+
expect(tester.getTopLeft(find.text(text)).dy, closeTo(564.0, .0001));
14991499
});
15001500
});
15011501

@@ -1680,8 +1680,8 @@ void main() {
16801680
);
16811681

16821682
// Below the center example.
1683-
expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
1684-
expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(470.0, .0001));
1683+
expect(tester.getTopLeft(find.text(text)).dy, closeTo(564.0, .0001));
1684+
expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(480.0, .0001));
16851685
});
16861686

16871687
testWidgets('InputDecorator tall prefix with border align double', (WidgetTester tester) async {
@@ -1711,8 +1711,8 @@ void main() {
17111711
);
17121712

17131713
// Between the top and center examples.
1714-
expect(tester.getTopLeft(find.text(text)).dy, closeTo(353.3, .0001));
1715-
expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(269.3, .0001));
1714+
expect(tester.getTopLeft(find.text(text)).dy, closeTo(354.3, .0001));
1715+
expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(270.3, .0001));
17161716
});
17171717
});
17181718

@@ -1794,6 +1794,199 @@ void main() {
17941794
});
17951795
});
17961796

1797+
group('OutlineInputBorder', () {
1798+
group('default alignment', () {
1799+
testWidgets('Centers when border', (WidgetTester tester) async {
1800+
await tester.pumpWidget(
1801+
buildInputDecorator(
1802+
decoration: const InputDecoration(
1803+
border: OutlineInputBorder(),
1804+
),
1805+
),
1806+
);
1807+
1808+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
1809+
expect(tester.getTopLeft(find.text('text')).dy, 19.0);
1810+
expect(tester.getBottomLeft(find.text('text')).dy, 35.0);
1811+
expect(getBorderBottom(tester), 56.0);
1812+
expect(getBorderWeight(tester), 1.0);
1813+
});
1814+
1815+
testWidgets('Centers when border and label', (WidgetTester tester) async {
1816+
await tester.pumpWidget(
1817+
buildInputDecorator(
1818+
decoration: const InputDecoration(
1819+
labelText: 'label',
1820+
border: OutlineInputBorder(),
1821+
),
1822+
),
1823+
);
1824+
1825+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 56.0));
1826+
expect(tester.getTopLeft(find.text('text')).dy, 19.0);
1827+
expect(tester.getBottomLeft(find.text('text')).dy, 35.0);
1828+
expect(getBorderBottom(tester), 56.0);
1829+
expect(getBorderWeight(tester), 1.0);
1830+
});
1831+
1832+
testWidgets('Centers when border and contentPadding', (WidgetTester tester) async {
1833+
await tester.pumpWidget(
1834+
buildInputDecorator(
1835+
decoration: const InputDecoration(
1836+
border: OutlineInputBorder(),
1837+
contentPadding: EdgeInsets.fromLTRB(
1838+
12.0, 14.0,
1839+
8.0, 14.0,
1840+
),
1841+
),
1842+
),
1843+
);
1844+
1845+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 44.0));
1846+
expect(tester.getTopLeft(find.text('text')).dy, 13.0);
1847+
expect(tester.getBottomLeft(find.text('text')).dy, 29.0);
1848+
expect(getBorderBottom(tester), 44.0);
1849+
expect(getBorderWeight(tester), 1.0);
1850+
});
1851+
1852+
testWidgets('Centers when border and contentPadding and label', (WidgetTester tester) async {
1853+
await tester.pumpWidget(
1854+
buildInputDecorator(
1855+
decoration: const InputDecoration(
1856+
labelText: 'label',
1857+
border: OutlineInputBorder(),
1858+
contentPadding: EdgeInsets.fromLTRB(
1859+
12.0, 14.0,
1860+
8.0, 14.0,
1861+
),
1862+
),
1863+
),
1864+
);
1865+
1866+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 44.0));
1867+
expect(tester.getTopLeft(find.text('text')).dy, 13.0);
1868+
expect(tester.getBottomLeft(find.text('text')).dy, 29.0);
1869+
expect(getBorderBottom(tester), 44.0);
1870+
expect(getBorderWeight(tester), 1.0);
1871+
});
1872+
1873+
testWidgets('Centers when border and lopsided contentPadding and label', (WidgetTester tester) async {
1874+
await tester.pumpWidget(
1875+
buildInputDecorator(
1876+
decoration: const InputDecoration(
1877+
labelText: 'label',
1878+
border: OutlineInputBorder(),
1879+
contentPadding: EdgeInsets.fromLTRB(
1880+
12.0, 104.0,
1881+
8.0, 0.0,
1882+
),
1883+
),
1884+
),
1885+
);
1886+
1887+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 120.0));
1888+
expect(tester.getTopLeft(find.text('text')).dy, 51.0);
1889+
expect(tester.getBottomLeft(find.text('text')).dy, 67.0);
1890+
expect(getBorderBottom(tester), 120.0);
1891+
expect(getBorderWeight(tester), 1.0);
1892+
});
1893+
});
1894+
1895+
group('3 point interpolation alignment', () {
1896+
testWidgets('top align includes padding', (WidgetTester tester) async {
1897+
await tester.pumpWidget(
1898+
buildInputDecorator(
1899+
expands: true,
1900+
textAlignVertical: TextAlignVertical.top,
1901+
decoration: const InputDecoration(
1902+
border: OutlineInputBorder(),
1903+
contentPadding: EdgeInsets.fromLTRB(
1904+
12.0, 24.0,
1905+
8.0, 2.0,
1906+
),
1907+
),
1908+
),
1909+
);
1910+
1911+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
1912+
// Aligned to the top including the 24px padding.
1913+
expect(tester.getTopLeft(find.text('text')).dy, 24.0);
1914+
expect(tester.getBottomLeft(find.text('text')).dy, 40.0);
1915+
expect(getBorderBottom(tester), 600.0);
1916+
expect(getBorderWeight(tester), 1.0);
1917+
});
1918+
1919+
testWidgets('center align ignores padding', (WidgetTester tester) async {
1920+
await tester.pumpWidget(
1921+
buildInputDecorator(
1922+
expands: true,
1923+
textAlignVertical: TextAlignVertical.center,
1924+
decoration: const InputDecoration(
1925+
border: OutlineInputBorder(),
1926+
contentPadding: EdgeInsets.fromLTRB(
1927+
12.0, 24.0,
1928+
8.0, 2.0,
1929+
),
1930+
),
1931+
),
1932+
);
1933+
1934+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
1935+
// Baseline is on the center of the 600px high input.
1936+
expect(tester.getTopLeft(find.text('text')).dy, 291.0);
1937+
expect(tester.getBottomLeft(find.text('text')).dy, 307.0);
1938+
expect(getBorderBottom(tester), 600.0);
1939+
expect(getBorderWeight(tester), 1.0);
1940+
});
1941+
1942+
testWidgets('bottom align includes padding', (WidgetTester tester) async {
1943+
await tester.pumpWidget(
1944+
buildInputDecorator(
1945+
expands: true,
1946+
textAlignVertical: TextAlignVertical.bottom,
1947+
decoration: const InputDecoration(
1948+
border: OutlineInputBorder(),
1949+
contentPadding: EdgeInsets.fromLTRB(
1950+
12.0, 24.0,
1951+
8.0, 2.0,
1952+
),
1953+
),
1954+
),
1955+
);
1956+
1957+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
1958+
// Includes bottom padding of 2px.
1959+
expect(tester.getTopLeft(find.text('text')).dy, 582.0);
1960+
expect(tester.getBottomLeft(find.text('text')).dy, 598.0);
1961+
expect(getBorderBottom(tester), 600.0);
1962+
expect(getBorderWeight(tester), 1.0);
1963+
});
1964+
1965+
testWidgets('padding exceeds middle keeps top at middle', (WidgetTester tester) async {
1966+
await tester.pumpWidget(
1967+
buildInputDecorator(
1968+
expands: true,
1969+
textAlignVertical: TextAlignVertical.top,
1970+
decoration: const InputDecoration(
1971+
border: OutlineInputBorder(),
1972+
contentPadding: EdgeInsets.fromLTRB(
1973+
12.0, 504.0,
1974+
8.0, 0.0,
1975+
),
1976+
),
1977+
),
1978+
);
1979+
1980+
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 600.0));
1981+
// Same position as the center example above.
1982+
expect(tester.getTopLeft(find.text('text')).dy, 291.0);
1983+
expect(tester.getBottomLeft(find.text('text')).dy, 307.0);
1984+
expect(getBorderBottom(tester), 600.0);
1985+
expect(getBorderWeight(tester), 1.0);
1986+
});
1987+
});
1988+
});
1989+
17971990
testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
17981991
await tester.pumpWidget(
17991992
buildInputDecorator(

0 commit comments

Comments
 (0)