flutterwidgetheightmetricstext-width

Flutter problem getting text-height with LineMetrics


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:


Solution

  • 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