Flutter problem getting text-height with LineMetrics

552

Solution 1

I cannot speak to the weird behaviour of textPainter.computeLineMetrics you observe, but I think there is a different solution to your problem, which does not involve measuring the height of the text widget.

Your content-cards could use a Stack with a Positioned and a BackdropFilter, which you can clip to fit the overlayed text. In doing so, you do not need to bother with measuring the Text widgets.

Example:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: <Widget>[
        FittedBox(
          fit: BoxFit.fill,
          child: Image.network('https://picsum.photos/300'),
        ),
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: ClipRect(
            child: BackdropFilter(
              filter: ImageFilter.blur(
                sigmaX: 3.0,
                sigmaY: 3.0,
              ),
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: const Text(
                    'Aut velit illum eos aut aut eaque totam. Autem aut quis omnis et minus. Itaque at molestias enim sunt autem voluptas voluptatem delectus. Minima deleniti fugiat sit sunt fugiat. Cumque iusto quo eum. Ipsa laborum est qui.'),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

NOTE: This example renders an expanded Stack. For your content-cards, you would probably use e.g. a SizedBox to constrain its dimensions.

Best regards

Solution 2

EDIT: It turns out in some cases this solution works only with a static fix (maxWidth - 10) too 😕

Although I couldn't find the reason/solution for the problem of wrong width-calculations of flutter line-metrics, I did find a solution for the problem of getting the height of a flutter text-widget before it's build:

extension StringExtension on String {
  double getHeight(BuildContext context, RichText richText, double maxWidth) {
    double maxWidthFix = maxWidth - 10; //NOTE: Fix!!!
    BoxConstraints constraints = BoxConstraints(
      maxWidth: maxWidthFix, // maxwidth calculated
    );

    RenderParagraph renderObject = richText.createRenderObject(context);
    renderObject.layout(constraints);
    return renderObject.getMaxIntrinsicHeight(maxWidthFix);
  }
}

With this String extension it's possible to calculate the correct height of texts to draw the background blur in a corresponding size:

// Get text heights
final double _titleHeight = title.getHeight(context, title, maxTextWidth);
final double _textHeight = text.getHeight(context, text, maxTextWidth);
final double _blurHeight = _titleHeight + _textHeight + margin;

These Screenshots proof that the initial problem is solved when using the string extension:

enter image description here

I created this solution based on two answers of the related topic:
How can I get the size of the Text Widget in flutter

For flutter line-metrics I only found a workaround that solves the problem in 99%? of cases:

// NOTE: width of lineMetrics sometimes not matching real text width
final double lineFix = 10;

textPainter.layout(
  minWidth: 0,
  maxWidth: width - lineFix,
);

textPainter.computeLineMetrics();
Share:
552
Rafa2602
Author by

Rafa2602

Updated on December 20, 2022

Comments

  • Rafa2602
    Rafa2602 over 1 year

    I have an app displaying content-cards with text in front of a background-image. For better readability a blur-layer is added between text and background-image. As it is not easy with flutter to get the height of a widget (or text) before it is build, the only way I could find to solve this problem (without using callbacks or redraws) are LineMetrics. With LineMetrics I can calculate the space the text will take to draw the blur-layer in the correct size.

    Now comes the problem: the calculated width attribute of LineMetrics sometimes doesn't fit the rendered text width. This is problematic in cases where it causes the miss of a line break, as the blurred background then doesn't cover the whole text-area anymore.

    Screenshot Text Fill

    What am I doing: Following this medium article I first create a TextSpan with text and style, then add it to a TextPainter and later call the layout() function with minWidth: 0, and maxWidth: maxTextWidth. Finally I create the LineMetrics with textPainter.computeLineMetrics():

      // Widget
      @override
      Widget build(BuildContext context) {
    
        // Get text styles
        final TextStyle titleStyle = TextStyle(
          fontFamily: 'SF Pro Display',
          fontStyle: FontStyle.normal,
          fontSize: 14,
          height: (17 / 14),
          fontWeight: FontWeight.bold,
          color: Colors.white);
        final TextStyle textStyle = TextStyle(
          fontFamily: 'SF Pro Display',
          fontStyle: FontStyle.normal,
          fontSize: 14,
          height: (17 / 14),
          fontWeight: FontWeight.normal,
          color: Colors.white);
    
        // Build text spans
        final titleSpan = TextSpan(text: title, style: titleStyle);
        final textSpan = TextSpan(text: text, style: textStyle);
    
        // Calculate text metrics
        final double _maxWidth = style.width - style.margin.horizontal;
        List<LineMetrics> _titleLines = _getLineMetrics(_maxWidth, titleSpan);
        List<LineMetrics> _textLines = _getLineMetrics(_maxWidth, textSpan);
    
        // Calculate text heights
        final double _titleHeight = _getLinesHeight(_titleLines) + style.margin.top;
        final double _textHeight = _getLinesHeight(_textLines) + style.margin.bottom;
    
        // Generate coloured debug container
        Column _titleContainer = _getTextSpanContainer(_titleLines);
        Column _textContainer = _getTextSpanContainer(_textLines);
    
        // Widget content
        List<Widget> _textOverlay = [
          Expanded(child: Text('')),
          Stack(children: <Widget>[
            Align(alignment: Alignment.topLeft, child: _titleContainer),
            Align(alignment: Alignment.topLeft, child: RichText(text: titleSpan))
          ]),
          Stack(children: <Widget>[
            Align(alignment: Alignment.topLeft, child: _textContainer),
            Align(alignment: Alignment.topLeft, child: RichText(text: textSpan))
          ])
        ];
    
    
      List<LineMetrics> _getLineMetrics(double width, TextSpan textSpan) {
        final textPainter = TextPainter(
          text: textSpan,
          textDirection: TextDirection.ltr
        );
    
        textPainter.layout(
          minWidth: 0,
          maxWidth: width,
        );
    
        // TODO: width of lineMetrics sometimes not matching real text width
        return textPainter.computeLineMetrics();
      }
    
      double _getLinesHeight(List<LineMetrics> lines) {
        double _textHeight = 0;
    
        for (LineMetrics line in lines) {
          _textHeight += line.height;
        }
    
        return _textHeight;
      }
    
      Column _getTextSpanContainer(List<LineMetrics> lines) {
        List<Widget> container = [];
    
        for (LineMetrics line in lines) {
          container.add(Container(
            width: line.width,
            height: line.height,
            color: Colors.red,
          ));
        }
    
        return Column(
          children: container,
          crossAxisAlignment: CrossAxisAlignment.start,
        );
      }
    

    I added red outlines behind each text-line with the calculated width of LineMetrics to visualise the problem. Most of the time it works but sometimes the calculated width's doesn't match:

    Screenshot Text Outline

    I tried out almost every possible attribute in TextStyles (WordSpacing, LetterSpacing, textWidthBasis...), build it with and without custom font, drawing with RichText and normal text Elements, but nothing changes on the described problem.

    Can anyone help to fix this strange behaviour, or provide an alternative method to get the text-height before a widget is build?

    Some related issues:

    • Suragch
      Suragch almost 3 years
      I don't understand the blur part. Can you show an image?
    • Rafa2602
      Rafa2602 almost 3 years
      @Suragch I updated the problem with a screenshot of the text overflowing the background blur. This is caused by a mismatch between text width and calculated line metrics.
  • Rafa2602
    Rafa2602 almost 3 years
    wow the solution is really nice. I just needed to move the text from some outer scope down to the child of the backdrop filter. Additionally it reduces a lot of complexity - thanks a lot for sharing!