31

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.

raaaay
  • 496
  • 7
  • 14
Lian
  • 2,197
  • 2
  • 15
  • 16

4 Answers4

42

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,
          ),
        ],
      ),
    );
  }
}
Lian
  • 2,197
  • 2
  • 15
  • 16
  • 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. – Abhishek Doshi Mar 03 '22 at 13:16
  • 1
    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. – Daan Dec 16 '22 at 15:58
  • Awesome, but how can I get Text Direction from localization? – hasanm08 Jan 25 '23 at 16:20
27

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 minWidth = 0, 
       double maxWidth = double.infinity, 
       int maxLines = 2
  }) {
  final TextPainter textPainter = TextPainter(
    text: TextSpan(text: text, style: style),
    maxLines: maxLines,
    textDirection: TextDirection.ltr,
  )..layout(minWidth: minWidth, maxWidth: maxWidth);
  return textPainter.didExceedMaxLines;
}
raaaay
  • 496
  • 7
  • 14
Valentina Konyukhova
  • 4,464
  • 2
  • 24
  • 33
  • 1
    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. – vladli Nov 28 '22 at 21:26
  • 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. – Dhiren Basra Dec 06 '22 at 07:21
13

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),
      )
    ],
  ),
);
Panduka Nandara
  • 504
  • 6
  • 14
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;
      },
    );
  }
}
Mohammad Ersan
  • 12,304
  • 8
  • 54
  • 77