Commit 0b31c699 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Make it possible to center the text in a TextField (#9140)

Also, fix an issue where hint text wasn't visible when the
InputDecoration was collapsed.

Fixes #8541
parent 9946355e
......@@ -238,6 +238,7 @@ class InputDecorator extends StatelessWidget {
Key key,
@required this.decoration,
this.baseStyle,
this.textAlign,
this.isFocused: false,
this.isEmpty: false,
this.child,
......@@ -252,6 +253,9 @@ class InputDecorator extends StatelessWidget {
/// If null, defaults to a text style from the current [Theme].
final TextStyle baseStyle;
/// How the text in the decoration should be aligned horizontally.
final TextAlign textAlign;
/// Whether the input field has focus.
///
/// Determines the position of the label text and the color of the divider.
......@@ -342,7 +346,7 @@ class InputDecorator extends StatelessWidget {
final Color activeColor = _getActiveColor(themeData);
double topPadding = isDense ? 12.0 : 16.0;
double topPadding = isCollapsed ? 0.0 : (isDense ? 12.0 : 16.0);
final List<Widget> stackChildren = <Widget>[];
......@@ -383,12 +387,18 @@ class InputDecorator extends StatelessWidget {
stackChildren.add(
new Positioned(
left: 0.0,
right: 0.0,
top: topPadding + baseStyle.fontSize - hintStyle.fontSize,
child: new AnimatedOpacity(
opacity: (isEmpty && !hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new Text(hintText, style: hintStyle),
child: new Text(
hintText,
style: hintStyle,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
),
),
),
);
......@@ -406,8 +416,14 @@ class InputDecorator extends StatelessWidget {
final TextStyle errorStyle = decoration.errorStyle ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor);
stackChildren.add(new Positioned(
left: 0.0,
right: 0.0,
bottom: 0.0,
child: new Text(errorText, style: errorStyle)
child: new Text(
errorText,
style: errorStyle,
textAlign: textAlign,
overflow: TextOverflow.ellipsis,
),
));
}
......
......@@ -69,6 +69,7 @@ class TextField extends StatefulWidget {
this.decoration: const InputDecoration(),
this.keyboardType: TextInputType.text,
this.style,
this.textAlign,
this.autofocus: false,
this.obscureText: false,
this.maxLines: 1,
......@@ -88,7 +89,7 @@ class TextField extends StatefulWidget {
/// The decoration to show around the text field.
///
/// By default, draws a horizontal line under the input field but can be
/// By default, draws a horizontal line under the text field but can be
/// configured to show an icon, label, hint text, and error text.
///
/// Set this field to null to remove the decoration entirely (including the
......@@ -105,10 +106,13 @@ class TextField extends StatefulWidget {
/// If null, defaults to a text style from the current [Theme].
final TextStyle style;
/// Whether this input field should focus itself if nothing else is already
/// How the text being edited should be aligned horizontally.
final TextAlign textAlign;
/// Whether this text field should focus itself if nothing else is already
/// focused.
///
/// If true, the keyboard will open as soon as this input obtains focus.
/// If true, the keyboard will open as soon as this text field obtains focus.
/// Otherwise, the keyboard is only shown after the user taps the text field.
///
/// Defaults to false.
......@@ -118,8 +122,8 @@ class TextField extends StatefulWidget {
/// Whether to hide the text being edited (e.g., for passwords).
///
/// When this is set to true, all the characters in the input are replaced by
/// U+2022 BULLET characters (•).
/// When this is set to true, all the characters in the text field are
/// replaced by U+2022 BULLET characters (•).
///
/// Defaults to false.
final bool obscureText;
......@@ -209,6 +213,7 @@ class _TextFieldState extends State<TextField> {
focusNode: focusNode,
keyboardType: config.keyboardType,
style: style,
textAlign: config.textAlign,
autofocus: config.autofocus,
obscureText: config.obscureText,
maxLines: config.maxLines,
......@@ -227,6 +232,7 @@ class _TextFieldState extends State<TextField> {
return new InputDecorator(
decoration: config.decoration,
baseStyle: config.style,
textAlign: config.textAlign,
isFocused: focusNode.hasFocus,
isEmpty: controller.value.text.isEmpty,
child: child,
......
......@@ -318,23 +318,35 @@ class TextPainter {
return new Offset(dx, box.top);
}
Offset get _emptyOffset {
// TODO(abarth): Handle the directionality of the text painter itself.
switch (textAlign ?? TextAlign.left) {
case TextAlign.left:
case TextAlign.justify:
return Offset.zero;
case TextAlign.right:
return new Offset(width, 0.0);
case TextAlign.center:
return new Offset(width / 2.0, 0.0);
}
return null;
}
/// Returns the offset at which to paint the caret.
///
/// Valid only after [layout] has been called.
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
assert(!_needsLayout);
final int offset = position.offset;
// TODO(abarth): Handle the directionality of the text painter itself.
const Offset emptyOffset = Offset.zero;
switch (position.affinity) {
case TextAffinity.upstream:
return _getOffsetFromUpstream(offset, caretPrototype)
?? _getOffsetFromDownstream(offset, caretPrototype)
?? emptyOffset;
?? _emptyOffset;
case TextAffinity.downstream:
return _getOffsetFromDownstream(offset, caretPrototype)
?? _getOffsetFromUpstream(offset, caretPrototype)
?? emptyOffset;
?? _emptyOffset;
}
assert(position.affinity != null);
return null;
......
......@@ -47,6 +47,7 @@ class RenderEditable extends RenderBox {
/// Creates a render object for a single line of editable text.
RenderEditable({
TextSpan text,
TextAlign textAlign,
Color cursorColor,
bool showCursor: false,
int maxLines: 1,
......@@ -55,7 +56,7 @@ class RenderEditable extends RenderBox {
TextSelection selection,
@required ViewportOffset offset,
this.onSelectionChanged,
}) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor),
}) : _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor),
_cursorColor = cursorColor,
_showCursor = showCursor,
_maxLines = maxLines,
......@@ -87,6 +88,15 @@ class RenderEditable extends RenderBox {
markNeedsLayout();
}
/// How the text should be aligned horizontally.
TextAlign get textAlign => _textPainter.textAlign;
set textAlign(TextAlign value) {
if (_textPainter.textAlign == value)
return;
_textPainter.textAlign = value;
markNeedsPaint();
}
/// The color to use when painting the cursor.
Color get cursorColor => _cursorColor;
Color _cursorColor;
......@@ -225,7 +235,7 @@ class RenderEditable extends RenderBox {
// TODO(mpcomplete): We should be more disciplined about when we dirty the
// layout state of the text painter so that we can know that the layout is
// clean at this point.
_textPainter.layout(maxWidth: _maxContentWidth);
_layoutText();
final Offset paintOffset = _paintOffset;
......@@ -261,12 +271,6 @@ class RenderEditable extends RenderBox {
double get _preferredLineHeight => _textPainter.preferredLineHeight;
double get _maxContentWidth {
if (_maxLines > 1)
return constraints.maxWidth - (_kCaretGap + _kCaretWidth);
return double.INFINITY;
}
@override
double computeMinIntrinsicHeight(double width) {
return _preferredLineHeight;
......@@ -333,11 +337,19 @@ class RenderEditable extends RenderBox {
Rect _caretPrototype;
void _layoutText() {
final double caretMargin = _kCaretGap + _kCaretWidth;
final double maxWidth = _maxLines > 1 ?
math.max(0.0, constraints.maxWidth - caretMargin) : double.INFINITY;
final double minWidth = math.max(0.0, constraints.minWidth - caretMargin);
_textPainter.layout(minWidth: minWidth, maxWidth: maxWidth);
}
@override
void performLayout() {
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null;
_textPainter.layout(maxWidth: _maxContentWidth);
_layoutText();
size = new Size(constraints.maxWidth, constraints.constrainHeight(
_textPainter.height.clamp(_preferredLineHeight, _preferredLineHeight * _maxLines)
));
......
......@@ -93,6 +93,7 @@ class EditableText extends StatefulWidget {
this.obscureText: false,
@required this.style,
@required this.cursorColor,
this.textAlign,
this.textScaleFactor,
this.maxLines: 1,
this.autofocus: false,
......@@ -125,6 +126,9 @@ class EditableText extends StatefulWidget {
/// The text style to use for the editable text.
final TextStyle style;
/// How the text should be aligned horizontally.
final TextAlign textAlign;
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
......@@ -407,6 +411,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
maxLines: config.maxLines,
selectionColor: config.selectionColor,
textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor,
textAlign: config.textAlign,
obscureText: config.obscureText,
offset: offset,
onSelectionChanged: _handleSelectionChanged,
......@@ -426,6 +431,7 @@ class _Editable extends LeafRenderObjectWidget {
this.maxLines,
this.selectionColor,
this.textScaleFactor,
this.textAlign,
this.obscureText,
this.offset,
this.onSelectionChanged,
......@@ -438,6 +444,7 @@ class _Editable extends LeafRenderObjectWidget {
final int maxLines;
final Color selectionColor;
final double textScaleFactor;
final TextAlign textAlign;
final bool obscureText;
final ViewportOffset offset;
final SelectionChangedHandler onSelectionChanged;
......@@ -451,6 +458,7 @@ class _Editable extends LeafRenderObjectWidget {
maxLines: maxLines,
selectionColor: selectionColor,
textScaleFactor: textScaleFactor,
textAlign: textAlign,
selection: value.selection,
offset: offset,
onSelectionChanged: onSelectionChanged,
......@@ -466,6 +474,7 @@ class _Editable extends LeafRenderObjectWidget {
..maxLines = maxLines
..selectionColor = selectionColor
..textScaleFactor = textScaleFactor
..textAlign = textAlign
..selection = value.selection
..offset = offset
..onSelectionChanged = onSelectionChanged;
......
......@@ -804,4 +804,52 @@ void main() {
expect(iconRight, equals(tester.getTopLeft(find.text('label')).x));
expect(iconRight, equals(tester.getTopLeft(find.byType(EditableText)).x));
});
testWidgets('Collapsed hint text placement', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(new Center(
child: new Material(
child: new TextField(
decoration: new InputDecoration.collapsed(
hintText: 'hint',
),
),
),
)),
);
expect(tester.getTopLeft(find.text('hint')), equals(tester.getTopLeft(find.byType(TextField))));
});
testWidgets('Can align to center', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(new Center(
child: new Material(
child: new Container(
width: 300.0,
child: new TextField(
textAlign: TextAlign.center,
decoration: null,
),
),
),
)),
);
final RenderEditable editable = findRenderEditable(tester);
Point topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(new TextPosition(offset: 0)).topLeft
);
expect(topLeft.x, equals(399.0));
await tester.enterText(find.byType(EditableText), 'abcd');
await tester.pump();
topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(new TextPosition(offset: 2)).topLeft
);
expect(topLeft.x, equals(399.0));
});
}
......@@ -29,7 +29,6 @@ void main() {
Future<Null> checkText(String testValue) async {
await tester.enterText(find.byType(EditableText), testValue);
await tester.idle();
formKey.currentState.save();
// pump'ing is unnecessary because callback happens regardless of frames
expect(fieldValue, equals(testValue));
......@@ -60,7 +59,6 @@ void main() {
Future<Null> checkText(String testValue) async {
await tester.enterText(find.byType(EditableText), testValue);
await tester.idle();
// pump'ing is unnecessary because callback happens regardless of frames
expect(fieldValue, equals(testValue));
}
......@@ -93,7 +91,6 @@ void main() {
Future<Null> checkErrorText(String testValue) async {
formKey.currentState.reset();
await tester.enterText(find.byType(EditableText), testValue);
await tester.idle();
await tester.pumpWidget(builder(false));
// We have to manually validate if we're not autovalidating.
......@@ -105,7 +102,6 @@ void main() {
// Try again with autovalidation. Should validate immediately.
formKey.currentState.reset();
await tester.enterText(find.byType(EditableText), testValue);
await tester.idle();
await tester.pumpWidget(builder(true));
expect(find.text(errorText(testValue)), findsOneWidget);
......@@ -146,7 +142,6 @@ void main() {
Future<Null> checkErrorText(String testValue) async {
await tester.enterText(find.byType(EditableText).first, testValue);
await tester.idle();
await tester.pump();
// Check for a new Text widget with our error text.
......@@ -190,7 +185,6 @@ void main() {
// sanity check, make sure we can still edit the text and everything updates
expect(inputKey.currentState.value, equals(initialValue));
await tester.enterText(find.byType(EditableText), 'world');
await tester.idle();
await tester.pump();
expect(inputKey.currentState.value, equals('world'));
expect(editableText.config.controller.text, equals('world'));
......@@ -221,7 +215,6 @@ void main() {
expect(formKey.currentState.validate(), isTrue);
await tester.enterText(find.byType(EditableText), 'Test');
await tester.idle();
await tester.pumpWidget(builder(false));
// Form wasn't saved yet.
......
......@@ -431,6 +431,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
Future<Null> enterText(Finder finder, String text) async {
await showKeyboard(finder);
testTextInput.enterText(text);
await idle();
return null;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment