Expanding text in Flutter

Vasily Styagov
4 min readMar 14, 2020

--

I’m pretty sure that your app contains a lot of text: titles, descriptions, hints, etc. And not all of those texts are necessary for the user to see. So, sometimes, you want to hide part of them. A short description of a movie provides enough information for a user to decide if he wants to read more. And the common pattern is to truncate this text, maybe ellipsize it, to allow the user to expand it in some way (e.g. tapping on the whole text or on the ellipsis).

If you want to crop the text and allow the user to expand it with a tap — there is a very simple solution. We just need to createStatefulWidget with a boolean property indicating that the text is expanded (e.g. isExpanded). Then we need to wrap the Text widget with GestureDetector and invert isExpanded property in its onTap callback. The overflow property of Text defines the way the text is cropped. Also, we define maxLines property depending on the current state.

Ok, it was easy. Default implementation of Text offers only 4 options on overflow:

  1. TextOverflow.clip crops overflowing text;
  2. TextOverflow.fade fades the end of the last line to transparent;
  3. TextOverflow.ellipsis uses ellipsis to indicate that the text has overflowed;
  4. TextOverflow.visible renders overflowing text outside of its container.

But what if we need to customize the ellipsis?

Here comes TextPainter

TextPainter allows you to draw your text right on Canvas , and it provides you with this Canvas and Size in paint() method. Also, TextPainter has ellipsis property, that allows you to override the string which will be substituted at the end of the cropped text. TextPainter allows us to set ellipsis and define the maximum number of lines. But it needs CustomPaint to render. And CustomPaint works in pair with CustomPainter , so we need to wrap use the TextPainter inside of CustomPainter . Note that you also must set textDirection to let TextPainter know where the line’s end is.

And voilà, there’s the custom ellipsis!

But this solution is not very flexible: “more” is just the part of the text. Also, you could notice that CustomPaint requires the size of the area for painting. How to calculate it? We’ll find it out a bit later.

Some maths with lines

But my goal was to implement an expandable text widget that ellipsizes the text based on the “maximum lines” attribute. It also must be expanded by a tap on the word “more” appended after the ellipsis. Also, I want this “more” word to be styled differently from the main text.

Different styles, tappable parts… Sounds like we need RichText . But it can ellipsize text just like the regular Text do (actually, Text renders RichText under the hood). So, if it had many spans in it, all of them would be cropped.

Ok, let’s get to it.

First of all, we need to know how the text is gonna be rendered. Unfortunately, I couldn’t figure out how text would render line by line, but we can get rectangles describing bounds of these lines. RenderObjectserves this purpose. It defines the base layout model. Hence we can get bounds of the text after layout. But we need Constraints to perform layout, so let’s wrap it in LayoutBuilder . Since text can either be expanded or stay the same, the widget must hold the state.

Now we can get the RenderParapgraph ( RenderObject that displays paragraph of text), layout it and get bounds of each line with getBoxesForSelection method.

If the number of lines is less than the maximum number or if the widget is in the expanded state we can just render our text as-is.

Otherwise, let’s do some simple maths. We cannot get the content of each line. But we know the length of each line in pixels. So we can:

  1. Calculate the total length of the maximum number of lines;
  2. Calculate the total length of all the lines;
  3. Count the ratio of these values;
  4. Take the approximate length of cropped string, by multiplying the length of the full string on this ratio;
  5. Take substring of the length that we’ve got.

Here’s the relevant code:

All we need to do now is to create TextSpan with this new string and desired style and append ellipsis and “more” word with the other style. But the resulting string may be longer than needed, and in that case just layout it again and check the number of lines. If it’s greater than the desired maximum number of lines… Ok, let’s just crop the substring of length ellipsis + "more" from the end of the truncated string. That’s all. No more layouts. Now we can prepare the final RichText .

Not too accurate (I mean, the “More” word is not always at the very end of the last line), but you can be sure that a user will see the exact number of lines.

--

--

Vasily Styagov

Walking into crypto with Sweat 🚶 Making mobile world Flutter with http://devartel.io 🐦 Riffs and grooves 🎸