Tight Paragraph Width (#30988)
Add `textWidthBasis` param to Text to allow calculating width according to longest line.
This commit is contained in:
parent
0c20a2ed01
commit
323108ff47
@ -15,6 +15,19 @@ import 'text_span.dart';
|
|||||||
|
|
||||||
export 'package:flutter/services.dart' show TextRange, TextSelection;
|
export 'package:flutter/services.dart' show TextRange, TextSelection;
|
||||||
|
|
||||||
|
/// The different ways of considering the width of a Text widget.
|
||||||
|
enum TextWidthBasis {
|
||||||
|
/// Multiline text will take up the full width given by the parent. For single
|
||||||
|
/// line text, only the minimum amount of width needed to contain the text
|
||||||
|
/// will be used. A common use case for this is a standard series of
|
||||||
|
/// paragraphs.
|
||||||
|
parent,
|
||||||
|
|
||||||
|
/// The width will be exactly enough to contain the longest line and no
|
||||||
|
/// longer. A common use case for this is chat bubbles.
|
||||||
|
longestLine,
|
||||||
|
}
|
||||||
|
|
||||||
class _CaretMetrics {
|
class _CaretMetrics {
|
||||||
const _CaretMetrics({this.offset, this.fullHeight});
|
const _CaretMetrics({this.offset, this.fullHeight});
|
||||||
/// The offset of the top left corner of the caret from the top left
|
/// The offset of the top left corner of the caret from the top left
|
||||||
@ -60,10 +73,12 @@ class TextPainter {
|
|||||||
String ellipsis,
|
String ellipsis,
|
||||||
Locale locale,
|
Locale locale,
|
||||||
StrutStyle strutStyle,
|
StrutStyle strutStyle,
|
||||||
|
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
|
||||||
}) : assert(text == null || text.debugAssertIsValid()),
|
}) : assert(text == null || text.debugAssertIsValid()),
|
||||||
assert(textAlign != null),
|
assert(textAlign != null),
|
||||||
assert(textScaleFactor != null),
|
assert(textScaleFactor != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(textWidthBasis != null),
|
||||||
_text = text,
|
_text = text,
|
||||||
_textAlign = textAlign,
|
_textAlign = textAlign,
|
||||||
_textDirection = textDirection,
|
_textDirection = textDirection,
|
||||||
@ -71,7 +86,8 @@ class TextPainter {
|
|||||||
_maxLines = maxLines,
|
_maxLines = maxLines,
|
||||||
_ellipsis = ellipsis,
|
_ellipsis = ellipsis,
|
||||||
_locale = locale,
|
_locale = locale,
|
||||||
_strutStyle = strutStyle;
|
_strutStyle = strutStyle,
|
||||||
|
_textWidthBasis = textWidthBasis;
|
||||||
|
|
||||||
ui.Paragraph _paragraph;
|
ui.Paragraph _paragraph;
|
||||||
bool _needsLayout = true;
|
bool _needsLayout = true;
|
||||||
@ -233,6 +249,18 @@ class TextPainter {
|
|||||||
_needsLayout = true;
|
_needsLayout = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// {@macro flutter.dart:ui.text.TextWidthBasis}
|
||||||
|
TextWidthBasis get textWidthBasis => _textWidthBasis;
|
||||||
|
TextWidthBasis _textWidthBasis;
|
||||||
|
set textWidthBasis(TextWidthBasis value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_textWidthBasis == value)
|
||||||
|
return;
|
||||||
|
_textWidthBasis = value;
|
||||||
|
_paragraph = null;
|
||||||
|
_needsLayout = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ui.Paragraph _layoutTemplate;
|
ui.Paragraph _layoutTemplate;
|
||||||
|
|
||||||
@ -317,7 +345,9 @@ class TextPainter {
|
|||||||
/// Valid only after [layout] has been called.
|
/// Valid only after [layout] has been called.
|
||||||
double get width {
|
double get width {
|
||||||
assert(!_needsLayout);
|
assert(!_needsLayout);
|
||||||
return _applyFloatingPointHack(_paragraph.width);
|
return _applyFloatingPointHack(
|
||||||
|
textWidthBasis == TextWidthBasis.longestLine ? _paragraph.longestLine : _paragraph.width,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The vertical space required to paint this text.
|
/// The vertical space required to paint this text.
|
||||||
|
@ -49,6 +49,7 @@ class RenderParagraph extends RenderBox {
|
|||||||
TextOverflow overflow = TextOverflow.clip,
|
TextOverflow overflow = TextOverflow.clip,
|
||||||
double textScaleFactor = 1.0,
|
double textScaleFactor = 1.0,
|
||||||
int maxLines,
|
int maxLines,
|
||||||
|
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
|
||||||
Locale locale,
|
Locale locale,
|
||||||
StrutStyle strutStyle,
|
StrutStyle strutStyle,
|
||||||
}) : assert(text != null),
|
}) : assert(text != null),
|
||||||
@ -59,6 +60,7 @@ class RenderParagraph extends RenderBox {
|
|||||||
assert(overflow != null),
|
assert(overflow != null),
|
||||||
assert(textScaleFactor != null),
|
assert(textScaleFactor != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(textWidthBasis != null),
|
||||||
_softWrap = softWrap,
|
_softWrap = softWrap,
|
||||||
_overflow = overflow,
|
_overflow = overflow,
|
||||||
_textPainter = TextPainter(
|
_textPainter = TextPainter(
|
||||||
@ -70,6 +72,7 @@ class RenderParagraph extends RenderBox {
|
|||||||
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
|
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
strutStyle: strutStyle,
|
strutStyle: strutStyle,
|
||||||
|
textWidthBasis: textWidthBasis,
|
||||||
);
|
);
|
||||||
|
|
||||||
final TextPainter _textPainter;
|
final TextPainter _textPainter;
|
||||||
@ -212,6 +215,17 @@ class RenderParagraph extends RenderBox {
|
|||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.basic.TextWidthBasis}
|
||||||
|
TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
|
||||||
|
set textWidthBasis(TextWidthBasis value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_textPainter.textWidthBasis == value)
|
||||||
|
return;
|
||||||
|
_textPainter.textWidthBasis = value;
|
||||||
|
_overflowShader = null;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
|
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
|
||||||
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
|
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
|
||||||
_textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
|
_textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
|
||||||
|
@ -4811,12 +4811,14 @@ class RichText extends LeafRenderObjectWidget {
|
|||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.locale,
|
this.locale,
|
||||||
this.strutStyle,
|
this.strutStyle,
|
||||||
|
this.textWidthBasis = TextWidthBasis.parent,
|
||||||
}) : assert(text != null),
|
}) : assert(text != null),
|
||||||
assert(textAlign != null),
|
assert(textAlign != null),
|
||||||
assert(softWrap != null),
|
assert(softWrap != null),
|
||||||
assert(overflow != null),
|
assert(overflow != null),
|
||||||
assert(textScaleFactor != null),
|
assert(textScaleFactor != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(textWidthBasis != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// The text to display in this widget.
|
/// The text to display in this widget.
|
||||||
@ -4875,6 +4877,9 @@ class RichText extends LeafRenderObjectWidget {
|
|||||||
/// {@macro flutter.painting.textPainter.strutStyle}
|
/// {@macro flutter.painting.textPainter.strutStyle}
|
||||||
final StrutStyle strutStyle;
|
final StrutStyle strutStyle;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
|
||||||
|
final TextWidthBasis textWidthBasis;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderParagraph createRenderObject(BuildContext context) {
|
RenderParagraph createRenderObject(BuildContext context) {
|
||||||
assert(textDirection != null || debugCheckHasDirectionality(context));
|
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||||
@ -4886,6 +4891,7 @@ class RichText extends LeafRenderObjectWidget {
|
|||||||
textScaleFactor: textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
strutStyle: strutStyle,
|
strutStyle: strutStyle,
|
||||||
|
textWidthBasis: textWidthBasis,
|
||||||
locale: locale ?? Localizations.localeOf(context, nullOk: true),
|
locale: locale ?? Localizations.localeOf(context, nullOk: true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -4902,6 +4908,7 @@ class RichText extends LeafRenderObjectWidget {
|
|||||||
..textScaleFactor = textScaleFactor
|
..textScaleFactor = textScaleFactor
|
||||||
..maxLines = maxLines
|
..maxLines = maxLines
|
||||||
..strutStyle = strutStyle
|
..strutStyle = strutStyle
|
||||||
|
..textWidthBasis = textWidthBasis
|
||||||
..locale = locale ?? Localizations.localeOf(context, nullOk: true);
|
..locale = locale ?? Localizations.localeOf(context, nullOk: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4914,6 +4921,7 @@ class RichText extends LeafRenderObjectWidget {
|
|||||||
properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: TextOverflow.clip));
|
properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: TextOverflow.clip));
|
||||||
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
|
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
|
||||||
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||||
|
properties.add(EnumProperty<TextWidthBasis>('textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent));
|
||||||
properties.add(StringProperty('text', text.toPlainText()));
|
properties.add(StringProperty('text', text.toPlainText()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,12 +33,14 @@ class DefaultTextStyle extends InheritedWidget {
|
|||||||
this.softWrap = true,
|
this.softWrap = true,
|
||||||
this.overflow = TextOverflow.clip,
|
this.overflow = TextOverflow.clip,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
|
this.textWidthBasis = TextWidthBasis.parent,
|
||||||
@required Widget child,
|
@required Widget child,
|
||||||
}) : assert(style != null),
|
}) : assert(style != null),
|
||||||
assert(softWrap != null),
|
assert(softWrap != null),
|
||||||
assert(overflow != null),
|
assert(overflow != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
assert(child != null),
|
assert(child != null),
|
||||||
|
assert(textWidthBasis != null),
|
||||||
super(key: key, child: child);
|
super(key: key, child: child);
|
||||||
|
|
||||||
/// A const-constructible default text style that provides fallback values.
|
/// A const-constructible default text style that provides fallback values.
|
||||||
@ -52,7 +54,8 @@ class DefaultTextStyle extends InheritedWidget {
|
|||||||
textAlign = null,
|
textAlign = null,
|
||||||
softWrap = true,
|
softWrap = true,
|
||||||
maxLines = null,
|
maxLines = null,
|
||||||
overflow = TextOverflow.clip;
|
overflow = TextOverflow.clip,
|
||||||
|
textWidthBasis = TextWidthBasis.parent;
|
||||||
|
|
||||||
/// Creates a default text style that overrides the text styles in scope at
|
/// Creates a default text style that overrides the text styles in scope at
|
||||||
/// this point in the widget tree.
|
/// this point in the widget tree.
|
||||||
@ -77,6 +80,7 @@ class DefaultTextStyle extends InheritedWidget {
|
|||||||
bool softWrap,
|
bool softWrap,
|
||||||
TextOverflow overflow,
|
TextOverflow overflow,
|
||||||
int maxLines,
|
int maxLines,
|
||||||
|
TextWidthBasis textWidthBasis,
|
||||||
@required Widget child,
|
@required Widget child,
|
||||||
}) {
|
}) {
|
||||||
assert(child != null);
|
assert(child != null);
|
||||||
@ -90,6 +94,7 @@ class DefaultTextStyle extends InheritedWidget {
|
|||||||
softWrap: softWrap ?? parent.softWrap,
|
softWrap: softWrap ?? parent.softWrap,
|
||||||
overflow: overflow ?? parent.overflow,
|
overflow: overflow ?? parent.overflow,
|
||||||
maxLines: maxLines ?? parent.maxLines,
|
maxLines: maxLines ?? parent.maxLines,
|
||||||
|
textWidthBasis: textWidthBasis ?? parent.textWidthBasis,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -121,6 +126,10 @@ class DefaultTextStyle extends InheritedWidget {
|
|||||||
/// [Text.maxLines].
|
/// [Text.maxLines].
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
|
||||||
|
/// The strategy to use when calculating the width of the Text. See
|
||||||
|
/// [TextWidthBasis] for possible values and their implications.
|
||||||
|
final TextWidthBasis textWidthBasis;
|
||||||
|
|
||||||
/// The closest instance of this class that encloses the given context.
|
/// The closest instance of this class that encloses the given context.
|
||||||
///
|
///
|
||||||
/// If no such instance exists, returns an instance created by
|
/// If no such instance exists, returns an instance created by
|
||||||
@ -141,7 +150,8 @@ class DefaultTextStyle extends InheritedWidget {
|
|||||||
textAlign != oldWidget.textAlign ||
|
textAlign != oldWidget.textAlign ||
|
||||||
softWrap != oldWidget.softWrap ||
|
softWrap != oldWidget.softWrap ||
|
||||||
overflow != oldWidget.overflow ||
|
overflow != oldWidget.overflow ||
|
||||||
maxLines != oldWidget.maxLines;
|
maxLines != oldWidget.maxLines ||
|
||||||
|
textWidthBasis != oldWidget.textWidthBasis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -152,6 +162,7 @@ class DefaultTextStyle extends InheritedWidget {
|
|||||||
properties.add(FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
|
properties.add(FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
|
||||||
properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null));
|
properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null));
|
||||||
properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
|
properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
|
||||||
|
properties.add(EnumProperty<TextWidthBasis>('textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +248,7 @@ class Text extends StatelessWidget {
|
|||||||
this.textScaleFactor,
|
this.textScaleFactor,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.semanticsLabel,
|
this.semanticsLabel,
|
||||||
|
this.textWidthBasis,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
data != null,
|
data != null,
|
||||||
'A non-null String must be provided to a Text widget.',
|
'A non-null String must be provided to a Text widget.',
|
||||||
@ -260,6 +272,7 @@ class Text extends StatelessWidget {
|
|||||||
this.textScaleFactor,
|
this.textScaleFactor,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.semanticsLabel,
|
this.semanticsLabel,
|
||||||
|
this.textWidthBasis,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
textSpan != null,
|
textSpan != null,
|
||||||
'A non-null TextSpan must be provided to a Text.rich widget.',
|
'A non-null TextSpan must be provided to a Text.rich widget.',
|
||||||
@ -359,6 +372,9 @@ class Text extends StatelessWidget {
|
|||||||
/// ```
|
/// ```
|
||||||
final String semanticsLabel;
|
final String semanticsLabel;
|
||||||
|
|
||||||
|
/// {@macro flutter.dart:ui.text.TextWidthBasis}
|
||||||
|
final TextWidthBasis textWidthBasis;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
||||||
@ -376,6 +392,7 @@ class Text extends StatelessWidget {
|
|||||||
textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
|
textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
|
||||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||||
strutStyle: strutStyle,
|
strutStyle: strutStyle,
|
||||||
|
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: effectiveTextStyle,
|
style: effectiveTextStyle,
|
||||||
text: data,
|
text: data,
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../rendering/mock_canvas.dart';
|
import '../rendering/mock_canvas.dart';
|
||||||
import 'semantics_tester.dart';
|
import 'semantics_tester.dart';
|
||||||
@ -363,6 +364,43 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byType(Text), isNot(paints..clipRect()));
|
expect(find.byType(Text), isNot(paints..clipRect()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('textWidthBasis affects the width of a Text widget', (WidgetTester tester) async {
|
||||||
|
Future<void> createText(TextWidthBasis textWidthBasis) {
|
||||||
|
return tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Container(
|
||||||
|
// Each word takes up more than a half of a line. Together they
|
||||||
|
// wrap onto two lines, but leave a lot of extra space.
|
||||||
|
child: Text('twowordsthateachtakeupmorethanhalfof alineoftextsothattheywrapwithlotsofextraspace',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
textWidthBasis: textWidthBasis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double fontHeight = 14.0;
|
||||||
|
const double screenWidth = 800.0;
|
||||||
|
|
||||||
|
// When textWidthBasis is parent, takes up full screen width.
|
||||||
|
await createText(TextWidthBasis.parent);
|
||||||
|
final Size textSizeParent = tester.getSize(find.byType(Text));
|
||||||
|
expect(textSizeParent.width, equals(screenWidth));
|
||||||
|
expect(textSizeParent.height, equals(fontHeight * 2));
|
||||||
|
|
||||||
|
// When textWidthBasis is longestLine, sets the width to as small as
|
||||||
|
// possible for the two lines.
|
||||||
|
await createText(TextWidthBasis.longestLine);
|
||||||
|
final Size textSizeLongestLine = tester.getSize(find.byType(Text));
|
||||||
|
expect(textSizeLongestLine.width, equals(630.0));
|
||||||
|
expect(textSizeLongestLine.height, equals(fontHeight * 2));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pumpTextWidget({ WidgetTester tester, String text, TextOverflow overflow }) {
|
Future<void> _pumpTextWidget({ WidgetTester tester, String text, TextOverflow overflow }) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user