From 0a3df1b5761f498eb2c492555685bf7e25dd86ac Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 3 May 2019 11:41:07 -0700 Subject: [PATCH] Text wrap width (#31987) Add `textWidthBasis` param to Text to allow calculating width according to longest line. --- .../lib/src/painting/text_painter.dart | 36 +++++++++++++++++- .../flutter/lib/src/rendering/paragraph.dart | 14 +++++++ packages/flutter/lib/src/widgets/basic.dart | 8 ++++ packages/flutter/lib/src/widgets/text.dart | 21 +++++++++- packages/flutter/test/widgets/text_test.dart | 38 +++++++++++++++++++ 5 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index c558b588df..bab9d10057 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -15,6 +15,21 @@ import 'text_span.dart'; export 'package:flutter/services.dart' show TextRange, TextSelection; +/// The different ways of considering the width of one or more lines of text. +/// +/// See [Text.widthType]. +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 { const _CaretMetrics({this.offset, this.fullHeight}); /// The offset of the top left corner of the caret from the top left @@ -60,10 +75,12 @@ class TextPainter { String ellipsis, Locale locale, StrutStyle strutStyle, + TextWidthBasis textWidthBasis = TextWidthBasis.parent, }) : assert(text == null || text.debugAssertIsValid()), assert(textAlign != null), assert(textScaleFactor != null), assert(maxLines == null || maxLines > 0), + assert(textWidthBasis != null), _text = text, _textAlign = textAlign, _textDirection = textDirection, @@ -71,7 +88,8 @@ class TextPainter { _maxLines = maxLines, _ellipsis = ellipsis, _locale = locale, - _strutStyle = strutStyle; + _strutStyle = strutStyle, + _textWidthBasis = textWidthBasis; ui.Paragraph _paragraph; bool _needsLayout = true; @@ -233,6 +251,18 @@ class TextPainter { _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; @@ -317,7 +347,9 @@ class TextPainter { /// Valid only after [layout] has been called. double get width { assert(!_needsLayout); - return _applyFloatingPointHack(_paragraph.width); + return _applyFloatingPointHack( + textWidthBasis == TextWidthBasis.longestLine ? _paragraph.longestLine : _paragraph.width, + ); } /// The vertical space required to paint this text. diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 2145f2c809..d8d485bd3a 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -49,6 +49,7 @@ class RenderParagraph extends RenderBox { TextOverflow overflow = TextOverflow.clip, double textScaleFactor = 1.0, int maxLines, + TextWidthBasis textWidthBasis = TextWidthBasis.parent, Locale locale, StrutStyle strutStyle, }) : assert(text != null), @@ -59,6 +60,7 @@ class RenderParagraph extends RenderBox { assert(overflow != null), assert(textScaleFactor != null), assert(maxLines == null || maxLines > 0), + assert(textWidthBasis != null), _softWrap = softWrap, _overflow = overflow, _textPainter = TextPainter( @@ -70,6 +72,7 @@ class RenderParagraph extends RenderBox { ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, locale: locale, strutStyle: strutStyle, + textWidthBasis: textWidthBasis, ); final TextPainter _textPainter; @@ -212,6 +215,17 @@ class RenderParagraph extends RenderBox { 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 }) { final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis; _textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity); diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 98190a5e5f..dca7fa55be 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4811,12 +4811,14 @@ class RichText extends LeafRenderObjectWidget { this.maxLines, this.locale, this.strutStyle, + this.textWidthBasis = TextWidthBasis.parent, }) : assert(text != null), assert(textAlign != null), assert(softWrap != null), assert(overflow != null), assert(textScaleFactor != null), assert(maxLines == null || maxLines > 0), + assert(textWidthBasis != null), super(key: key); /// The text to display in this widget. @@ -4875,6 +4877,9 @@ class RichText extends LeafRenderObjectWidget { /// {@macro flutter.painting.textPainter.strutStyle} final StrutStyle strutStyle; + /// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis} + final TextWidthBasis textWidthBasis; + @override RenderParagraph createRenderObject(BuildContext context) { assert(textDirection != null || debugCheckHasDirectionality(context)); @@ -4886,6 +4891,7 @@ class RichText extends LeafRenderObjectWidget { textScaleFactor: textScaleFactor, maxLines: maxLines, strutStyle: strutStyle, + textWidthBasis: textWidthBasis, locale: locale ?? Localizations.localeOf(context, nullOk: true), ); } @@ -4902,6 +4908,7 @@ class RichText extends LeafRenderObjectWidget { ..textScaleFactor = textScaleFactor ..maxLines = maxLines ..strutStyle = strutStyle + ..textWidthBasis = textWidthBasis ..locale = locale ?? Localizations.localeOf(context, nullOk: true); } @@ -4914,6 +4921,7 @@ class RichText extends LeafRenderObjectWidget { properties.add(EnumProperty('overflow', overflow, defaultValue: TextOverflow.clip)); properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0)); properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')); + properties.add(EnumProperty('textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent)); properties.add(StringProperty('text', text.toPlainText())); } } diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index e3c7042d07..ee3233f8da 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -33,12 +33,14 @@ class DefaultTextStyle extends InheritedWidget { this.softWrap = true, this.overflow = TextOverflow.clip, this.maxLines, + this.textWidthBasis = TextWidthBasis.parent, @required Widget child, }) : assert(style != null), assert(softWrap != null), assert(overflow != null), assert(maxLines == null || maxLines > 0), assert(child != null), + assert(textWidthBasis != null), super(key: key, child: child); /// A const-constructible default text style that provides fallback values. @@ -52,7 +54,8 @@ class DefaultTextStyle extends InheritedWidget { textAlign = null, softWrap = true, maxLines = null, - overflow = TextOverflow.clip; + overflow = TextOverflow.clip, + textWidthBasis = TextWidthBasis.parent; /// Creates a default text style that overrides the text styles in scope at /// this point in the widget tree. @@ -77,6 +80,7 @@ class DefaultTextStyle extends InheritedWidget { bool softWrap, TextOverflow overflow, int maxLines, + TextWidthBasis textWidthBasis, @required Widget child, }) { assert(child != null); @@ -90,6 +94,7 @@ class DefaultTextStyle extends InheritedWidget { softWrap: softWrap ?? parent.softWrap, overflow: overflow ?? parent.overflow, maxLines: maxLines ?? parent.maxLines, + textWidthBasis: textWidthBasis ?? parent.textWidthBasis, child: child, ); }, @@ -121,6 +126,10 @@ class DefaultTextStyle extends InheritedWidget { /// [Text.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. /// /// If no such instance exists, returns an instance created by @@ -141,7 +150,8 @@ class DefaultTextStyle extends InheritedWidget { textAlign != oldWidget.textAlign || softWrap != oldWidget.softWrap || overflow != oldWidget.overflow || - maxLines != oldWidget.maxLines; + maxLines != oldWidget.maxLines || + textWidthBasis != oldWidget.textWidthBasis; } @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(EnumProperty('overflow', overflow, defaultValue: null)); properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); + properties.add(EnumProperty('textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent)); } } @@ -237,6 +248,7 @@ class Text extends StatelessWidget { this.textScaleFactor, this.maxLines, this.semanticsLabel, + this.textWidthBasis, }) : assert( data != null, 'A non-null String must be provided to a Text widget.', @@ -260,6 +272,7 @@ class Text extends StatelessWidget { this.textScaleFactor, this.maxLines, this.semanticsLabel, + this.textWidthBasis, }) : assert( textSpan != null, 'A non-null TextSpan must be provided to a Text.rich widget.', @@ -359,6 +372,9 @@ class Text extends StatelessWidget { /// ``` final String semanticsLabel; + /// {@macro flutter.dart:ui.text.TextWidthBasis} + final TextWidthBasis textWidthBasis; + @override Widget build(BuildContext context) { final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); @@ -376,6 +392,7 @@ class Text extends StatelessWidget { textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context), maxLines: maxLines ?? defaultTextStyle.maxLines, strutStyle: strutStyle, + textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, text: TextSpan( style: effectiveTextStyle, text: data, diff --git a/packages/flutter/test/widgets/text_test.dart b/packages/flutter/test/widgets/text_test.dart index 02878118cb..48a89ba1fd 100644 --- a/packages/flutter/test/widgets/text_test.dart +++ b/packages/flutter/test/widgets/text_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; @@ -363,6 +364,43 @@ void main() { expect(find.byType(Text), isNot(paints..clipRect())); }); + + testWidgets('textWidthBasis affects the width of a Text widget', (WidgetTester tester) async { + Future 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 _pumpTextWidget({ WidgetTester tester, String text, TextOverflow overflow }) {