Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a maxLines parameter for multiline Input. #6310

Merged
merged 7 commits into from
Oct 14, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/flutter_gallery/lib/demo/text_field_demo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
new Input(
hintText: 'Tell us about yourself (optional)',
labelText: 'Life story',
multiline: true,
maxLines: 3,
formField: new FormField<String>()
),
new Row(
Expand Down
11 changes: 6 additions & 5 deletions packages/flutter/lib/src/material/input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Input extends StatefulWidget {
this.hideText: false,
this.isDense: false,
this.autofocus: false,
this.multiline: false,
this.maxLines: 1,
this.formField,
this.onChanged,
this.onSubmitted
Expand Down Expand Up @@ -85,9 +85,10 @@ class Input extends StatefulWidget {
/// Whether this input field should focus itself is nothing else is already focused.
final bool autofocus;

/// True if the text should wrap and span multiple lines, false if it should
/// stay on a single line and scroll when overflowed.
final bool multiline;
/// The maximum number of lines for the text to span, wrapping if necessary.
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
final int maxLines;

/// Form-specific data, required if this Input is part of a Form.
final FormField<String> formField;
Expand Down Expand Up @@ -210,7 +211,7 @@ class _InputState extends State<Input> {
focusKey: focusKey,
style: textStyle,
hideText: config.hideText,
multiline: config.multiline,
maxLines: config.maxLines,
cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls,
Expand Down
44 changes: 31 additions & 13 deletions packages/flutter/lib/src/rendering/editable_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ class TextSelectionPoint {
final TextDirection direction;
}

typedef Offset RenderEditableLinePaintOffsetNeededCallback(ViewportDimensions dimensions, Rect caretRect);

/// A single line of editable text.
class RenderEditableLine extends RenderBox {
/// Creates a render object for a single line of editable text.
RenderEditableLine({
TextSpan text,
Color cursorColor,
bool showCursor: false,
bool multiline: false,
int maxLines: 1,
Color selectionColor,
double textScaleFactor: 1.0,
TextSelection selection,
Expand All @@ -54,7 +56,7 @@ class RenderEditableLine extends RenderBox {
}) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor),
_cursorColor = cursorColor,
_showCursor = showCursor,
_multiline = multiline,
_maxLines = maxLines,
_selection = selection,
_paintOffset = paintOffset {
assert(!showCursor || cursorColor != null);
Expand All @@ -70,7 +72,7 @@ class RenderEditableLine extends RenderBox {
SelectionChangedHandler onSelectionChanged;

/// Called when the inner or outer dimensions of this render object change.
ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
RenderEditableLinePaintOffsetNeededCallback onPaintOffsetUpdateNeeded;

/// The text to display
TextSpan get text => _textPainter.text;
Expand Down Expand Up @@ -173,7 +175,7 @@ class RenderEditableLine extends RenderBox {
if (selection.isCollapsed) {
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
Point start = new Point(0.0, constraints.constrainHeight(_preferredHeight)) + caretOffset + offset;
Point start = new Point(0.0, constraints.constrainHeight(_preferredLineHeight)) + caretOffset + offset;
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)];
} else {
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
Expand All @@ -192,10 +194,17 @@ class RenderEditableLine extends RenderBox {
return _textPainter.getPositionForOffset(globalToLocal(globalPosition).toOffset());
}

/// Returns the Rect in local coordinates for the caret at the given text
/// position.
Rect getLocalRectForCaret(TextPosition caretPosition) {
Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
return _caretPrototype.shift(caretOffset + _paintOffset);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed the caret rect I use here, dropping the vertical padding so that we scroll all the way to the edge when it moves.


Size _contentSize;

ui.Paragraph _layoutTemplate;
double get _preferredHeight {
double get _preferredLineHeight {
if (_layoutTemplate == null) {
ui.ParagraphBuilder builder = new ui.ParagraphBuilder()
..pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor))
Expand All @@ -208,21 +217,21 @@ class RenderEditableLine extends RenderBox {
return _layoutTemplate.height;
}

bool _multiline;
int _maxLines;
double get _maxContentWidth {
return _multiline ?
return _maxLines > 1 ?
constraints.maxWidth - (_kCaretGap + _kCaretWidth) :
double.INFINITY;
}

@override
double computeMinIntrinsicHeight(double width) {
return _preferredHeight;
return _preferredLineHeight;
}

@override
double computeMaxIntrinsicHeight(double width) {
return _preferredHeight;
return _preferredLineHeight;
}

@override
Expand Down Expand Up @@ -284,17 +293,26 @@ class RenderEditableLine extends RenderBox {
@override
void performLayout() {
Size oldSize = hasSize ? size : null;
double lineHeight = constraints.constrainHeight(_preferredHeight);
double lineHeight = constraints.constrainHeight(_preferredLineHeight);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, lineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null;
_textPainter.layout(maxWidth: _maxContentWidth);
size = new Size(constraints.maxWidth, constraints.constrainHeight(math.max(lineHeight, _textPainter.height)));
size = new Size(constraints.maxWidth, constraints.constrainHeight(
_textPainter.height.clamp(lineHeight, lineHeight * _maxLines)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline before final )

Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height);
if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize))
onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize));
assert(_selection != null); // valid assumption?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should determine if it is before checkin

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(one option is to just say that it is, and treat it as a bug if we find it isn't later)

Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
Rect caretRect = _caretPrototype.shift(caretOffset + _paintOffset);
if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize || !_withinBounds(caretRect)))
onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize), caretRect);
_contentSize = contentSize;
}

bool _withinBounds(Rect caretRect) {
Rect bounds = new Rect.fromLTWH(0.0, 0.0, size.width, size.height);
return (bounds.contains(caretRect.topLeft) && bounds.contains(caretRect.bottomRight));
}

void _paintCaret(Canvas canvas, Offset effectiveOffset) {
Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
Paint paint = new Paint()..color = _cursorColor;
Expand Down
96 changes: 61 additions & 35 deletions packages/flutter/lib/src/widgets/editable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import 'dart:async';

import 'package:flutter/rendering.dart' show RenderEditableLine, SelectionChangedHandler;
import 'package:flutter/rendering.dart' show RenderEditableLine, SelectionChangedHandler, RenderEditableLinePaintOffsetNeededCallback;
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:flutter_services/editing.dart' as mojom;
Expand Down Expand Up @@ -170,17 +170,17 @@ class RawInputLine extends Scrollable {
this.style,
this.cursorColor,
this.textScaleFactor,
this.multiline,
int maxLines: 1,
this.selectionColor,
this.selectionControls,
@required this.platform,
this.keyboardType,
this.onChanged,
this.onSubmitted
}) : super(
}) : maxLines = maxLines, super(
key: key,
initialScrollOffset: 0.0,
scrollDirection: Axis.horizontal
scrollDirection: maxLines > 1 ? Axis.vertical : Axis.horizontal
) {
assert(value != null);
}
Expand Down Expand Up @@ -208,9 +208,10 @@ class RawInputLine extends Scrollable {
/// The color to use when painting the cursor.
final Color cursorColor;

/// True if the text should wrap and span multiple lines, false if it should
/// stay on a single line and scroll when overflowed.
final bool multiline;
/// The maximum number of lines for the text to span, wrapping if necessary.
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
final int maxLines;

/// The color to use when painting the selection.
final Color selectionColor;
Expand Down Expand Up @@ -273,30 +274,45 @@ class RawInputLineState extends ScrollableState<RawInputLine> {

bool get _isAttachedToKeyboard => _keyboardHandle != null && _keyboardHandle.attached;

double _contentWidth = 0.0;
double _containerWidth = 0.0;
bool get _isMultiline => config.maxLines > 1;

Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
double _contentExtent = 0.0;
double _containerExtent = 0.0;

Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions, Rect caretRect) {
// We make various state changes here but don't have to do so in a
// setState() callback because we are called during layout and all
// we're updating is the new offset, which we are providing to the
// render object via our return value.
_containerWidth = dimensions.containerSize.width;
_contentWidth = dimensions.contentSize.width;
_contentExtent = _isMultiline ?
dimensions.contentSize.height :
dimensions.contentSize.width;
_containerExtent = _isMultiline ?
dimensions.containerSize.height :
dimensions.containerSize.width;
didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: _contentWidth,
containerExtent: _containerWidth,
// Set the scroll offset to match the content width so that the
// cursor (which is always at the end of the text) will be
// visible.
contentExtent: _contentExtent,
containerExtent: _containerExtent,
// TODO(ianh): We should really only do this when text is added,
// not generally any time the size changes.
scrollOffset: pixelOffsetToScrollOffset(-_contentWidth)
scrollOffset: _getScrollOffsetForCaret(caretRect, _containerExtent)
));
updateGestureDetector();
return scrollOffsetToPixelDelta(scrollOffset);
}

// Calculate the new scroll offset so the cursor remains visible.
double _getScrollOffsetForCaret(Rect caretRect, double containerExtent) {
double caretStart = _isMultiline ? caretRect.top : caretRect.left;
double caretEnd = _isMultiline ? caretRect.bottom : caretRect.right;
double newScrollOffset = scrollOffset;
if (caretStart < 0.0) // cursor before start of bounds
newScrollOffset += pixelOffsetToScrollOffset(-caretStart);
else if (caretEnd >= containerExtent) // cursor after end of bounds
newScrollOffset += pixelOffsetToScrollOffset(-(caretEnd - containerExtent));
return newScrollOffset;
}

void _attachOrDetachKeyboard(bool focused) {
if (focused && !_isAttachedToKeyboard) {
_keyboardHandle = keyboard.attach(_keyboardClient.createStub(),
Expand Down Expand Up @@ -373,10 +389,18 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
}
}

void _handleSelectionOverlayChanged(InputValue newInput) {
void _handleSelectionOverlayChanged(InputValue newInput, Rect caretRect) {
assert(!newInput.composing.isValid); // composing range must be empty while selecting
if (config.onChanged != null)
config.onChanged(newInput);

didUpdateScrollBehavior(scrollBehavior.updateExtents(
// TODO(mpcomplete): should just be able to pass
// scrollBehavior.containerExtent here (and remove the member var), but
// scrollBehavior gets re-created too often, and is sometimes
// uninitialized here. Investigate if this is a bug.
scrollOffset: _getScrollOffsetForCaret(caretRect, _containerExtent)
));
}

/// Whether the blinking cursor is actually visible at this precise moment
Expand Down Expand Up @@ -438,18 +462,20 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
}
}

return new _EditableLineWidget(
value: _keyboardClient.inputValue,
style: config.style,
cursorColor: config.cursorColor,
showCursor: _showCursor,
multiline: config.multiline,
selectionColor: config.selectionColor,
textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor,
hideText: config.hideText,
onSelectionChanged: _handleSelectionChanged,
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded
return new ClipRect(
child: new _EditableLineWidget(
value: _keyboardClient.inputValue,
style: config.style,
cursorColor: config.cursorColor,
showCursor: _showCursor,
maxLines: config.maxLines,
selectionColor: config.selectionColor,
textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor,
hideText: config.hideText,
onSelectionChanged: _handleSelectionChanged,
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded
)
);
}
}
Expand All @@ -461,7 +487,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
this.style,
this.cursorColor,
this.showCursor,
this.multiline,
this.maxLines,
this.selectionColor,
this.textScaleFactor,
this.hideText,
Expand All @@ -474,21 +500,21 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
final TextStyle style;
final Color cursorColor;
final bool showCursor;
final bool multiline;
final int maxLines;
final Color selectionColor;
final double textScaleFactor;
final bool hideText;
final SelectionChangedHandler onSelectionChanged;
final Offset paintOffset;
final ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
final RenderEditableLinePaintOffsetNeededCallback onPaintOffsetUpdateNeeded;

@override
RenderEditableLine createRenderObject(BuildContext context) {
return new RenderEditableLine(
text: _styledTextSpan,
cursorColor: cursorColor,
showCursor: showCursor,
multiline: multiline,
maxLines: maxLines,
selectionColor: selectionColor,
textScaleFactor: textScaleFactor,
selection: value.selection,
Expand Down