mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Add support for specifying maxLines for Text. (#7493)
Overflow handling works with clipping, adding an ellipsis on the last line, or fading the last line. Fixes https://github.com/flutter/flutter/issues/7271
This commit is contained in:
parent
60847a1ded
commit
75f39789c7
|
@ -1 +1 @@
|
|||
b3ed79122edd7172327ce415688ef674d6a7fa5d
|
||||
2efc78cc24eac9439a5315ed9333fa8599aab3a1
|
||||
|
|
|
@ -11,6 +11,8 @@ import 'basic_types.dart';
|
|||
import 'text_editing.dart';
|
||||
import 'text_span.dart';
|
||||
|
||||
final String _kZeroWidthSpace = new String.fromCharCode(0x200B);
|
||||
|
||||
/// An object that paints a [TextSpan] tree into a [Canvas].
|
||||
///
|
||||
/// To use a [TextPainter], follow these steps:
|
||||
|
@ -34,8 +36,9 @@ class TextPainter {
|
|||
TextSpan text,
|
||||
TextAlign textAlign,
|
||||
double textScaleFactor: 1.0,
|
||||
int maxLines,
|
||||
String ellipsis,
|
||||
}) : _text = text, _textAlign = textAlign, _textScaleFactor = textScaleFactor, _ellipsis = ellipsis {
|
||||
}) : _text = text, _textAlign = textAlign, _textScaleFactor = textScaleFactor, _maxLines = maxLines, _ellipsis = ellipsis {
|
||||
assert(text == null || text.debugAssertIsValid());
|
||||
assert(textScaleFactor != null);
|
||||
}
|
||||
|
@ -50,6 +53,8 @@ class TextPainter {
|
|||
assert(value == null || value.debugAssertIsValid());
|
||||
if (_text == value)
|
||||
return;
|
||||
if (_text?.style != value?.style)
|
||||
_layoutTemplate = null;
|
||||
_text = value;
|
||||
_paragraph = null;
|
||||
_needsLayout = true;
|
||||
|
@ -95,6 +100,32 @@ class TextPainter {
|
|||
_needsLayout = true;
|
||||
}
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
int get maxLines => _maxLines;
|
||||
int _maxLines;
|
||||
set maxLines(int value) {
|
||||
if (_maxLines == value)
|
||||
return;
|
||||
_maxLines = value;
|
||||
_paragraph = null;
|
||||
_needsLayout = true;
|
||||
}
|
||||
|
||||
ui.Paragraph _layoutTemplate;
|
||||
double get preferredLineHeight {
|
||||
assert(text != null);
|
||||
if (_layoutTemplate == null) {
|
||||
ui.ParagraphBuilder builder = new ui.ParagraphBuilder(new ui.ParagraphStyle());
|
||||
if (text.style != null)
|
||||
builder.pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor));
|
||||
builder.addText(_kZeroWidthSpace);
|
||||
_layoutTemplate = builder.build()
|
||||
..layout(new ui.ParagraphConstraints(width: double.INFINITY));
|
||||
}
|
||||
return _layoutTemplate.height;
|
||||
}
|
||||
|
||||
// Unfortunately, using full precision floating point here causes bad layouts
|
||||
// because floating point math isn't associative. If we add and subtract
|
||||
|
@ -162,6 +193,11 @@ class TextPainter {
|
|||
return null;
|
||||
}
|
||||
|
||||
bool get didExceedMaxLines {
|
||||
assert(!_needsLayout);
|
||||
return _paragraph.didExceedMaxLines;
|
||||
}
|
||||
|
||||
double _lastMinWidth;
|
||||
double _lastMaxWidth;
|
||||
|
||||
|
@ -179,9 +215,14 @@ class TextPainter {
|
|||
ui.ParagraphStyle paragraphStyle = _text.style?.getParagraphStyle(
|
||||
textAlign: textAlign,
|
||||
textScaleFactor: textScaleFactor,
|
||||
maxLines: _maxLines,
|
||||
ellipsis: _ellipsis,
|
||||
);
|
||||
paragraphStyle ??= new ui.ParagraphStyle();
|
||||
paragraphStyle ??= new ui.ParagraphStyle(
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
ellipsis: ellipsis,
|
||||
);
|
||||
ui.ParagraphBuilder builder = new ui.ParagraphBuilder(paragraphStyle);
|
||||
_text.build(builder, textScaleFactor: textScaleFactor);
|
||||
_paragraph = builder.build();
|
||||
|
|
|
@ -233,6 +233,7 @@ class TextStyle {
|
|||
TextAlign textAlign,
|
||||
double textScaleFactor: 1.0,
|
||||
String ellipsis,
|
||||
int maxLines,
|
||||
}) {
|
||||
return new ui.ParagraphStyle(
|
||||
textAlign: textAlign,
|
||||
|
@ -241,6 +242,7 @@ class TextStyle {
|
|||
fontFamily: fontFamily,
|
||||
fontSize: fontSize == null ? null : fontSize * textScaleFactor,
|
||||
lineHeight: height,
|
||||
maxLines: maxLines,
|
||||
ellipsis: ellipsis,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, TextBox;
|
||||
import 'dart:ui' as ui show TextBox;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
|
@ -88,9 +88,6 @@ class RenderEditable extends RenderBox {
|
|||
set text(TextSpan value) {
|
||||
if (_textPainter.text == value)
|
||||
return;
|
||||
TextSpan oldStyledText = _textPainter.text;
|
||||
if (oldStyledText.style != value.style)
|
||||
_layoutTemplate = null;
|
||||
_textPainter.text = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
@ -115,7 +112,9 @@ class RenderEditable extends RenderBox {
|
|||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// Whether to paint the cursor.
|
||||
/// 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 extend
|
||||
/// indefinitely instead.
|
||||
int get maxLines => _maxLines;
|
||||
int _maxLines;
|
||||
set maxLines(int value) {
|
||||
|
@ -222,19 +221,7 @@ class RenderEditable extends RenderBox {
|
|||
|
||||
Size _contentSize;
|
||||
|
||||
ui.Paragraph _layoutTemplate;
|
||||
double get _preferredLineHeight {
|
||||
if (_layoutTemplate == null) {
|
||||
ui.ParagraphBuilder builder = new ui.ParagraphBuilder(new ui.ParagraphStyle())
|
||||
..pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor))
|
||||
..addText(_kZeroWidthSpace);
|
||||
// TODO(abarth): ParagraphBuilder#build's argument should be optional.
|
||||
// TODO(abarth): These min/max values should be the default for ui.Paragraph.
|
||||
_layoutTemplate = builder.build()
|
||||
..layout(new ui.ParagraphConstraints(width: double.INFINITY));
|
||||
}
|
||||
return _layoutTemplate.height;
|
||||
}
|
||||
double get _preferredLineHeight => _textPainter.preferredLineHeight;
|
||||
|
||||
double get _maxContentWidth {
|
||||
return _maxLines > 1 ?
|
||||
|
|
|
@ -33,14 +33,16 @@ class RenderParagraph extends RenderBox {
|
|||
TextAlign textAlign,
|
||||
bool softWrap: true,
|
||||
TextOverflow overflow: TextOverflow.clip,
|
||||
double textScaleFactor: 1.0
|
||||
double textScaleFactor: 1.0,
|
||||
int maxLines,
|
||||
}) : _softWrap = softWrap,
|
||||
_overflow = overflow,
|
||||
_textPainter = new TextPainter(
|
||||
text: text,
|
||||
textAlign: textAlign,
|
||||
textScaleFactor: textScaleFactor,
|
||||
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
|
||||
text: text,
|
||||
textAlign: textAlign,
|
||||
textScaleFactor: textScaleFactor,
|
||||
maxLines: maxLines,
|
||||
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
|
||||
) {
|
||||
assert(text != null);
|
||||
assert(text.debugAssertIsValid());
|
||||
|
@ -110,8 +112,20 @@ class RenderParagraph extends RenderBox {
|
|||
markNeedsLayout();
|
||||
}
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
int get maxLines => _textPainter.maxLines;
|
||||
set maxLines(int value) {
|
||||
if (_textPainter.maxLines == value)
|
||||
return;
|
||||
_textPainter.maxLines = value;
|
||||
_overflowShader = null;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
void _layoutText({ double minWidth: 0.0, double maxWidth: double.INFINITY }) {
|
||||
bool wrap = _softWrap || _overflow == TextOverflow.ellipsis;
|
||||
bool wrap = _softWrap || (_overflow == TextOverflow.ellipsis && maxLines == null);
|
||||
_textPainter.layout(minWidth: minWidth, maxWidth: wrap ? maxWidth : double.INFINITY);
|
||||
}
|
||||
|
||||
|
@ -183,30 +197,40 @@ class RenderParagraph extends RenderBox {
|
|||
size = constraints.constrain(textSize);
|
||||
|
||||
final bool didOverflowWidth = size.width < textSize.width;
|
||||
final bool didOverflowHeight = _textPainter.didExceedMaxLines;
|
||||
// TODO(abarth): We're only measuring the sizes of the line boxes here. If
|
||||
// the glyphs draw outside the line boxes, we might think that there isn't
|
||||
// visual overflow when there actually is visual overflow. This can become
|
||||
// a problem if we start having horizontal overflow and introduce a clip
|
||||
// that affects the actual (but undetected) vertical overflow.
|
||||
_hasVisualOverflow = didOverflowWidth || size.height < textSize.height;
|
||||
if (didOverflowWidth) {
|
||||
_hasVisualOverflow = didOverflowWidth || didOverflowHeight;
|
||||
if (_hasVisualOverflow) {
|
||||
switch (_overflow) {
|
||||
case TextOverflow.clip:
|
||||
case TextOverflow.ellipsis:
|
||||
_overflowShader = null;
|
||||
break;
|
||||
case TextOverflow.fade:
|
||||
TextPainter fadeWidthPainter = new TextPainter(
|
||||
TextPainter fadeSizePainter = new TextPainter(
|
||||
text: new TextSpan(style: _textPainter.text.style, text: '\u2026'),
|
||||
textScaleFactor: textScaleFactor
|
||||
)..layout();
|
||||
final double fadeEnd = size.width;
|
||||
final double fadeStart = fadeEnd - fadeWidthPainter.width;
|
||||
// TODO(abarth): This shader has an LTR bias.
|
||||
_overflowShader = new ui.Gradient.linear(
|
||||
<Point>[new Point(fadeStart, 0.0), new Point(fadeEnd, 0.0)],
|
||||
<Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)]
|
||||
);
|
||||
if (didOverflowWidth) {
|
||||
final double fadeEnd = size.width;
|
||||
final double fadeStart = fadeEnd - fadeSizePainter.width;
|
||||
// TODO(abarth): This shader has an LTR bias.
|
||||
_overflowShader = new ui.Gradient.linear(
|
||||
<Point>[new Point(fadeStart, 0.0), new Point(fadeEnd, 0.0)],
|
||||
<Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)]
|
||||
);
|
||||
} else {
|
||||
final double fadeEnd = size.height;
|
||||
final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
|
||||
_overflowShader = new ui.Gradient.linear(
|
||||
<Point>[new Point(0.0, fadeStart), new Point(0.0, fadeEnd)],
|
||||
<Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)]
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -2442,7 +2442,8 @@ class RichText extends LeafRenderObjectWidget {
|
|||
this.textAlign,
|
||||
this.softWrap: true,
|
||||
this.overflow: TextOverflow.clip,
|
||||
this.textScaleFactor: 1.0
|
||||
this.textScaleFactor: 1.0,
|
||||
this.maxLines,
|
||||
}) : super(key: key) {
|
||||
assert(text != null);
|
||||
assert(softWrap != null);
|
||||
|
@ -2470,13 +2471,19 @@ class RichText extends LeafRenderObjectWidget {
|
|||
/// the specified font size.
|
||||
final double textScaleFactor;
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
final int maxLines;
|
||||
|
||||
@override
|
||||
RenderParagraph createRenderObject(BuildContext context) {
|
||||
return new RenderParagraph(text,
|
||||
textAlign: textAlign,
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
textScaleFactor: textScaleFactor
|
||||
textScaleFactor: textScaleFactor,
|
||||
maxLines: maxLines,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2487,7 +2494,8 @@ class RichText extends LeafRenderObjectWidget {
|
|||
..textAlign = textAlign
|
||||
..softWrap = softWrap
|
||||
..overflow = overflow
|
||||
..textScaleFactor = textScaleFactor;
|
||||
..textScaleFactor = textScaleFactor
|
||||
..maxLines = maxLines;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ class DefaultTextStyle extends InheritedWidget {
|
|||
this.textAlign,
|
||||
this.softWrap: true,
|
||||
this.overflow: TextOverflow.clip,
|
||||
this.maxLines,
|
||||
Widget child
|
||||
}) : super(key: key, child: child) {
|
||||
assert(style != null);
|
||||
|
@ -35,6 +36,7 @@ class DefaultTextStyle extends InheritedWidget {
|
|||
: style = const TextStyle(),
|
||||
textAlign = null,
|
||||
softWrap = true,
|
||||
maxLines = null,
|
||||
overflow = TextOverflow.clip;
|
||||
|
||||
/// Creates a default text style that inherits from the given [BuildContext].
|
||||
|
@ -50,6 +52,7 @@ class DefaultTextStyle extends InheritedWidget {
|
|||
TextAlign textAlign,
|
||||
bool softWrap,
|
||||
TextOverflow overflow,
|
||||
int maxLines,
|
||||
Widget child
|
||||
}) {
|
||||
assert(context != null);
|
||||
|
@ -61,6 +64,7 @@ class DefaultTextStyle extends InheritedWidget {
|
|||
textAlign: textAlign ?? parent.textAlign,
|
||||
softWrap: softWrap ?? parent.softWrap,
|
||||
overflow: overflow ?? parent.overflow,
|
||||
maxLines: maxLines ?? parent.maxLines,
|
||||
child: child
|
||||
);
|
||||
}
|
||||
|
@ -79,6 +83,11 @@ class DefaultTextStyle extends InheritedWidget {
|
|||
/// How visual overflow should be handled.
|
||||
final TextOverflow overflow;
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
final int maxLines;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// If no such instance exists, returns an instance created by
|
||||
|
@ -134,7 +143,8 @@ class Text extends StatelessWidget {
|
|||
this.textAlign,
|
||||
this.softWrap,
|
||||
this.overflow,
|
||||
this.textScaleFactor
|
||||
this.textScaleFactor,
|
||||
this.maxLines,
|
||||
}) : super(key: key) {
|
||||
assert(data != null);
|
||||
}
|
||||
|
@ -168,6 +178,11 @@ class Text extends StatelessWidget {
|
|||
/// Defaults to [MediaQuery.textScaleFactor].
|
||||
final double textScaleFactor;
|
||||
|
||||
/// An optional maximum number of lines the text is allowed to take up.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
final int maxLines;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
||||
|
@ -179,6 +194,7 @@ class Text extends StatelessWidget {
|
|||
softWrap: softWrap ?? defaultTextStyle.softWrap,
|
||||
overflow: overflow ?? defaultTextStyle.overflow,
|
||||
textScaleFactor: textScaleFactor ?? MediaQuery.of(context).textScaleFactor,
|
||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||
text: new TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data
|
||||
|
|
|
@ -98,9 +98,9 @@ void main() {
|
|||
|
||||
ui.ParagraphStyle ps2 = s2.getParagraphStyle(textAlign: TextAlign.center);
|
||||
expect(ps2, equals(new ui.ParagraphStyle(textAlign: TextAlign.center, fontWeight: FontWeight.w800, fontSize: 10.0, lineHeight: 100.0)));
|
||||
expect(ps2.toString(), 'ParagraphStyle(textAlign: TextAlign.center, fontWeight: FontWeight.w800, fontStyle: unspecified, lineCount: unspecified, fontFamily: unspecified, fontSize: 10.0, lineHeight: 100.0x, ellipsis: unspecified)');
|
||||
expect(ps2.toString(), 'ParagraphStyle(textAlign: TextAlign.center, fontWeight: FontWeight.w800, fontStyle: unspecified, lineCount: unspecified, fontFamily: unspecified, fontSize: 10.0, lineHeight: 100.0x, maxLines: unspecified, ellipsis: unspecified)');
|
||||
ui.ParagraphStyle ps5 = s5.getParagraphStyle();
|
||||
expect(ps5, equals(new ui.ParagraphStyle(fontWeight: FontWeight.w700, fontSize: 12.0, lineHeight: 123.0)));
|
||||
expect(ps5.toString(), 'ParagraphStyle(textAlign: unspecified, fontWeight: FontWeight.w700, fontStyle: unspecified, lineCount: unspecified, fontFamily: unspecified, fontSize: 12.0, lineHeight: 123.0x, ellipsis: unspecified)');
|
||||
expect(ps5.toString(), 'ParagraphStyle(textAlign: unspecified, fontWeight: FontWeight.w700, fontStyle: unspecified, lineCount: unspecified, fontFamily: unspecified, fontSize: 12.0, lineHeight: 123.0x, maxLines: unspecified, ellipsis: unspecified)');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -72,4 +72,64 @@ void main() {
|
|||
TextRange range85 = paragraph.getWordBoundary(new TextPosition(offset: 75));
|
||||
expect(range85.textInside(_kText), equals('Queen\'s'));
|
||||
});
|
||||
|
||||
test('overflow test', () {
|
||||
RenderParagraph paragraph = new RenderParagraph(
|
||||
new TextSpan(text: 'This is\na wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.'),
|
||||
maxLines: 1,
|
||||
softWrap: true,
|
||||
);
|
||||
|
||||
void relayoutWith({int maxLines, bool softWrap, TextOverflow overflow}) {
|
||||
paragraph
|
||||
..maxLines = maxLines
|
||||
..softWrap = softWrap
|
||||
..overflow = overflow;
|
||||
pumpFrame();
|
||||
}
|
||||
|
||||
// Lay out in a narrow box to force wrapping.
|
||||
layout(paragraph, constraints: new BoxConstraints(maxWidth: 50.0));
|
||||
double lineHeight = paragraph.size.height;
|
||||
|
||||
relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.clip);
|
||||
expect(paragraph.size.height, equals(3 * lineHeight));
|
||||
|
||||
relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.clip);
|
||||
expect(paragraph.size.height, greaterThan(5 * lineHeight));
|
||||
|
||||
// Try again with ellipsis overflow. We can't test that the ellipsis are
|
||||
// drawn, but we can test the sizing.
|
||||
relayoutWith(maxLines: 1, softWrap: true, overflow: TextOverflow.ellipsis);
|
||||
expect(paragraph.size.height, equals(lineHeight));
|
||||
|
||||
relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
|
||||
expect(paragraph.size.height, equals(3 * lineHeight));
|
||||
|
||||
// This is the one weird case. If maxLines is null, we would expect to allow
|
||||
// infinite wrapping. However, if we did, we'd never know when to append an
|
||||
// ellipsis, so this really means "append ellipsis as soon as we exceed the
|
||||
// width".
|
||||
relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.ellipsis);
|
||||
expect(paragraph.size.height, equals(2 * lineHeight));
|
||||
|
||||
// Now with no soft wrapping.
|
||||
relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.clip);
|
||||
expect(paragraph.size.height, equals(lineHeight));
|
||||
|
||||
relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.clip);
|
||||
expect(paragraph.size.height, equals(2 * lineHeight));
|
||||
|
||||
relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.clip);
|
||||
expect(paragraph.size.height, equals(2 * lineHeight));
|
||||
|
||||
relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis);
|
||||
expect(paragraph.size.height, equals(lineHeight));
|
||||
|
||||
relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.ellipsis);
|
||||
expect(paragraph.size.height, equals(2 * lineHeight));
|
||||
|
||||
relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.ellipsis);
|
||||
expect(paragraph.size.height, equals(2 * lineHeight));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue