mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Text field vertical align (#34355)
Adds the `textAlignVertical` param to TextField and InputDecorator, allowing arbitrary vertical positioning of text in its input.
This commit is contained in:
parent
2b0c351880
commit
b798b27bbd
@ -574,7 +574,6 @@ class _RenderDecorationLayout {
|
||||
const _RenderDecorationLayout({
|
||||
this.boxToBaseline,
|
||||
this.inputBaseline, // for InputBorderType.underline
|
||||
this.outlineBaseline, // for InputBorderType.outline
|
||||
this.subtextBaseline,
|
||||
this.containerHeight,
|
||||
this.subtextHeight,
|
||||
@ -582,7 +581,6 @@ class _RenderDecorationLayout {
|
||||
|
||||
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 @@ class _RenderDecoration extends RenderBox {
|
||||
@required TextBaseline textBaseline,
|
||||
@required bool isFocused,
|
||||
@required bool expands,
|
||||
TextAlignVertical textAlignVertical,
|
||||
}) : assert(decoration != null),
|
||||
assert(textDirection != null),
|
||||
assert(textBaseline != null),
|
||||
@ -603,6 +602,7 @@ class _RenderDecoration extends RenderBox {
|
||||
_decoration = decoration,
|
||||
_textDirection = textDirection,
|
||||
_textBaseline = textBaseline,
|
||||
_textAlignVertical = textAlignVertical,
|
||||
_isFocused = isFocused,
|
||||
_expands = expands;
|
||||
|
||||
@ -746,6 +746,27 @@ class _RenderDecoration extends RenderBox {
|
||||
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 @@ class _RenderDecoration extends RenderBox {
|
||||
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 @@ class _RenderDecoration extends RenderBox {
|
||||
|
||||
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 @@ class _RenderDecoration extends RenderBox {
|
||||
? 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 @@ class _RenderDecoration extends RenderBox {
|
||||
boxToBaseline: boxToBaseline,
|
||||
containerHeight: containerHeight,
|
||||
inputBaseline: inputBaseline,
|
||||
outlineBaseline: outlineBaseline,
|
||||
subtextBaseline: subtextBaseline,
|
||||
subtextHeight: subtextHeight,
|
||||
);
|
||||
@ -1160,9 +1199,7 @@ class _RenderDecoration extends RenderBox {
|
||||
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 @@ class _RenderDecoration extends RenderBox {
|
||||
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 _RenderDecorationElement extends RenderObjectElement {
|
||||
class _Decorator extends RenderObjectWidget {
|
||||
const _Decorator({
|
||||
Key key,
|
||||
@required this.textAlignVertical,
|
||||
@required this.decoration,
|
||||
@required this.textDirection,
|
||||
@required this.textBaseline,
|
||||
@ -1526,6 +1565,7 @@ class _Decorator extends RenderObjectWidget {
|
||||
final _Decoration decoration;
|
||||
final TextDirection textDirection;
|
||||
final TextBaseline textBaseline;
|
||||
final TextAlignVertical textAlignVertical;
|
||||
final bool isFocused;
|
||||
final bool expands;
|
||||
|
||||
@ -1538,6 +1578,7 @@ class _Decorator extends RenderObjectWidget {
|
||||
decoration: decoration,
|
||||
textDirection: textDirection,
|
||||
textBaseline: textBaseline,
|
||||
textAlignVertical: textAlignVertical,
|
||||
isFocused: isFocused,
|
||||
expands: expands,
|
||||
);
|
||||
@ -1612,6 +1653,7 @@ class InputDecorator extends StatefulWidget {
|
||||
this.decoration,
|
||||
this.baseStyle,
|
||||
this.textAlign,
|
||||
this.textAlignVertical,
|
||||
this.isFocused = false,
|
||||
this.isHovering = false,
|
||||
this.expands = false,
|
||||
@ -1643,6 +1685,20 @@ class InputDecorator extends StatefulWidget {
|
||||
/// 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 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
),
|
||||
textDirection: textDirection,
|
||||
textBaseline: textBaseline,
|
||||
textAlignVertical: widget.textAlignVertical,
|
||||
isFocused: isFocused,
|
||||
expands: widget.expands,
|
||||
);
|
||||
@ -3468,3 +3525,42 @@ class InputDecorationTheme extends Diagnosticable {
|
||||
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)';
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +149,7 @@ class TextField extends StatefulWidget {
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.textAlignVertical,
|
||||
this.textDirection,
|
||||
this.readOnly = false,
|
||||
this.showCursor,
|
||||
@ -278,6 +279,9 @@ class TextField extends StatefulWidget {
|
||||
/// {@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 @@ class TextField extends StatefulWidget {
|
||||
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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
decoration: _getEffectiveDecoration(),
|
||||
baseStyle: widget.style,
|
||||
textAlign: widget.textAlign,
|
||||
textAlignVertical: widget.textAlignVertical,
|
||||
isHovering: _isHovering,
|
||||
isFocused: focusNode.hasFocus,
|
||||
isEmpty: controller.value.text.isEmpty,
|
||||
|
@ -2153,8 +2153,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -15,10 +15,12 @@ Widget buildInputDecorator({
|
||||
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 @@ Widget buildInputDecorator({
|
||||
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 @@ void main() {
|
||||
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 @@ void main() {
|
||||
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(
|
||||
|
Loading…
Reference in New Issue
Block a user