42

I have a Text widget which can be truncated if it exceeds a certain size:

ConstrainedBox( constraints: BoxConstraints(maxHeight: 50.0), child: Text( widget.review, overflow: TextOverflow.ellipsis, ) ); 

Or max number of lines:

RichText( maxLines: 2, overflow: TextOverflow.ellipsis, text: TextSpan( style: TextStyle(color: Colors.black), text: widget.review, )); 

My goal is to have the text expandable only if an overflow occurred. Is there a proper way of checking if the text overflowed?

What I've tried

I have found that in RichText, there is a RenderParagraph renderObject , which has a private property TextPainter _textPainter which has a bool didExceedMaxLines.

In short, I just need to access richText.renderObject._textPainter.didExceedMaxLines but as you can see, it is made private with the underscore.

6 Answers 6

50

I found a way to do it. Full code below, but in short:

  1. Use a LayoutBuilder to determine how much space we have.
  2. Use a TextPainter to simulate the render of the text within the space.

Here's the full demo app:

import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Text Overflow Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar(title: Text("DEMO")), body: TextOverflowDemo(), ), ); } } class TextOverflowDemo extends StatefulWidget { @override _EditorState createState() => _EditorState(); } class _EditorState extends State<TextOverflowDemo> { var controller = TextEditingController(); @override void initState() { controller.addListener(() { setState(() { mytext = controller.text; }); }); controller.text = "This is a long overflowing text!!!"; super.initState(); } @override void dispose() { controller.dispose(); super.dispose(); } String mytext = ""; @override Widget build(BuildContext context) { int maxLines = 1; double fontSize = 30.0; return Padding( padding: const EdgeInsets.all(12.0), child: Column( children: <Widget>[ LayoutBuilder(builder: (context, size) { // Build the textspan var span = TextSpan( text: mytext, style: TextStyle(fontSize: fontSize), ); // Use a textpainter to determine if it will exceed max lines var tp = TextPainter( maxLines: maxLines, textAlign: TextAlign.left, textDirection: TextDirection.ltr, text: span, ); // trigger it to layout tp.layout(maxWidth: size.maxWidth); // whether the text overflowed or not var exceeded = tp.didExceedMaxLines; return Column(children: <Widget>[ Text.rich( span, overflow: TextOverflow.ellipsis, maxLines: maxLines, ), Text(exceeded ? "Overflowed!" : "Not overflowed yet.") ]); }), TextField( controller: controller, ), ], ), ); } } 
Sign up to request clarification or add additional context in comments.

4 Comments

This works like a charm. I tried the other solution which just states using TextPainter, but the issue is, we can't get the renderObjectSize when we are building the widget. Hence, didExceedMaxLines always returned true.
Important note; didExceedMaxLines calculation won't be accurate when the device scales font sizes up (or down); think: vision impairment for example. To accurately support this, add: textScaleFactor: MediaQuery.of(context).textScaleFactor to the TextPainter initialisation.
Awesome, but how can I get Text Direction from localization?
I am using widget span. TextPainter cannot create widget span. Is there a solution for this?
39

There is a shorter way to get an answer if text is overflowed or not. You just need to define textStyle and get the answer from this method

bool hasTextOverflow( String text, TextStyle style, double textScaleFactor, {double minWidth = 0, double maxWidth = double.infinity, int maxLines = 2 }) { final TextPainter textPainter = TextPainter( text: TextSpan(text: text, style: style), maxLines: maxLines, textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, )..layout(minWidth: minWidth, maxWidth: maxWidth); return textPainter.didExceedMaxLines; } 

2 Comments

A great function, thanks very much for sharing! I might add that it is (probably) more commonly used with maxLines = 1, which I'd set as a default for an example.
Good function, maxWidth will be device width. so you may get exact bool of didExceedMaxLines as true. otherwise if maxWidth == double.infinity always gives false.
17

You can use a flutter plug-in auto_size_text at pub.dev. When the text is get overflowed, you can set some widget to be appeared.

int maxLines = 3; String caption = 'Your caption is here'; return AutoSizeText( caption, maxLines: maxLines, style: TextStyle(fontSize: 20), minFontSize: 15, overflowReplacement: Column( // This widget will be replaced. crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( caption, maxLines: maxLines, overflow: TextOverflow.ellipsis, ), Text( "Show more", style: TextStyle(color: PrimaryColor.kGrey), ) ], ), ); 

Comments

3

Building on the other solutions provided, here's a generalized Widget that may be useful.

/// Checks the text provided and style, and then calls /// the provided builder function with whether the text /// will overflow the maxLines value class DetectTextOverflowBuilder extends StatelessWidget { final Text textWidget; final Widget Function(BuildContext, bool) builder; final int maxLines; const DetectTextOverflowBuilder({ super.key, required this.textWidget, required this.builder, this.maxLines = 1, }); @override Widget build(BuildContext context) { final defaultTextStyle = DefaultTextStyle.of(context).style; return LayoutBuilder( builder: (context, constraints) { final span = TextSpan( text: textWidget.data, style: textWidget.style ?? defaultTextStyle, ); // Use a textpainter to determine if it will exceed max lines final tp = TextPainter( maxLines: maxLines, textAlign: TextAlign.left, textDirection: TextDirection.ltr, text: span, ); // trigger it to layout tp.layout(maxWidth: constraints.maxWidth); // whether the text overflowed or not final exceeded = tp.didExceedMaxLines; return builder(context, exceeded); }, ); } } 

Follows the builder pattern, with the builder function being passed the context and a bool willOverflow indicating whether the text widget will overflow the layout builders maxWidth.

Advantage of this approach in my opinion are

  • Reusability
  • Increased flexibility in terms of being able to control what gets displayed based on whether or not the text will have overflowed the available space
  • Doesn't display the text content by default

Example usage:

Example widget view collapsed | Example widget view expanded

class ShowMoreDemo extends StatefulWidget { const ShowMoreDemo({super.key}); @override ShowMoreDemoState createState() => ShowMoreDemoState(); } class ShowMoreDemoState extends State<ShowMoreDemo> { bool isShowingMore = false; @override Widget build(BuildContext context) { final messageContent = Text( AppConstant.loremText, maxLines: isShowingMore ? 40 : 1, overflow: TextOverflow.ellipsis, ); return DetectTextOverflowBuilder( textWidget: messageContent, builder: (context, willOverflow) { return Column( children: [ messageContent, SizedBox(height: 20.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('4 days ago'), if (willOverflow) TextButton( onPressed: () => setState(() { isShowingMore = !isShowingMore; }), child: Text( isShowingMore ? 'Show Less' : 'Show More', ), ), ], ), ], ); }, ); } } 

Comments

3

Most of the solution need maxLines to be passed, but in my case I do not have specific number of maxLines as it is dependent on device screen height.

This solution will also update UI at runtime if user increase device font size from control centre in iOS or from settings in Android.

The way I solved is here:

  1. Calculate total height needed by your text (with style) using Text-Painter.
  2. Figure out available space you have got, and render UI based on difference.
class OverflowText extends StatelessWidget { const OverflowText({super.key, required this.text, this.style}); final TextStyle? style; final String text; double _heightRequired( String text, TextStyle? style, double maxWidth, TextScaler textScaler, ) { final tp = TextPainter( text: TextSpan(text: text, style: style), textDirection: TextDirection.ltr, textScaler: textScaler, )..layout(maxWidth: maxWidth); return tp.size.height; } @override Widget build(BuildContext context) { final media = MediaQuery.of(context); return LayoutBuilder( builder: (context, constraints) { final heightRequired = _heightRequired( text, style, constraints.maxWidth, media.textScaler, ); final willTruncate = heightRequired > constraints.maxHeight; if (willTruncate) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( text, style: style, overflow: TextOverflow.fade, ), ), const SizedBox(height: 8), TextButton( onPressed: () {}, style: TextButton.styleFrom( foregroundColor: Colors.white, padding: EdgeInsets.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, minimumSize: const Size(0, 0), ), child: const Text( 'Read More', style: TextStyle( decoration: TextDecoration.underline, fontWeight: FontWeight.bold, ), ), ), ], ); } return Text(text, style: style); }, ); } } 

Demo

Comments

0

Made my own Widget i use it cross the project, it take Text widget in the constructor and reads the properties of it.

import 'package:flutter/material.dart'; class OverflowDetectorText extends StatelessWidget { final Text child; OverflowDetectorText({ Key? key, required this.child, }) : super(key: key); @override Widget build(BuildContext context) { var tp = TextPainter( maxLines: child.maxLines, textAlign: child.textAlign ?? TextAlign.start, textDirection: child.textDirection ?? TextDirection.ltr, text: child.textSpan ?? TextSpan( text: child.data, style: child.style, ), ); return LayoutBuilder( builder: (context, constrains) { tp.layout(maxWidth: constrains.maxWidth); final overflowed = tp.didExceedMaxLines; if (overflowed) { //You can wrap your Text `child` with anything } return child; }, ); } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.