How can I get the size of the Text widget in Flutter
Asked Answered
O

10

77

I've painted a shape for the background of my content of Text.

I want the background autoscale the Text, even the softWrap being true.

So, I need to get the width and height of my Text Widget before Widget build(BuildContext context).

Actually, I am simulating the chat bubble effect like iOS message using flutter. Here is the iOS version tutorial. Creating a Chat Bubble .

The core code below:

let label =  UILabel()
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 18)
label.textColor = .white
label.text = text

let constraintRect = CGSize(width: 0.66 * view.frame.width,
                            height: .greatestFiniteMagnitude)
let boundingBox = text.boundingRect(with: constraintRect,
                                    options: .usesLineFragmentOrigin,
                                    attributes: [.font: label.font],
                                    context: nil)
label.frame.size = CGSize(width: ceil(boundingBox.width),
                          height: ceil(boundingBox.height))

let bubbleSize = CGSize(width: label.frame.width + 28,
                             height: label.frame.height + 20)

let width = bubbleSize.width
let height = bubbleSize.height

=========================================
SOLUTION
Here is my solution.

bubble.dart:

// Define a CustomPainter to paint the bubble background.
class BubblePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()
      ..color = Color(0xff188aff)
      ..style = PaintingStyle.fill;
    final Path bubble = Path()
      ..moveTo(size.width - 22.0, size.height)
      ..lineTo(17.0, size.height)
      ..cubicTo(
          7.61, size.height, 0.0, size.height - 7.61, 0.0, size.height - 17.0)
      ..lineTo(0.0, 17.0)
      ..cubicTo(0.0, 7.61, 7.61, 0.0, 17.0, 0.0)
      ..lineTo(size.width - 21, 0.0)
      ..cubicTo(size.width - 11.61, 0.0, size.width - 4.0, 7.61,
          size.width - 4.0, 17.0)
      ..lineTo(size.width - 4.0, size.height - 11.0)
      ..cubicTo(size.width - 4.0, size.height - 1.0, size.width, size.height,
          size.width, size.height)
      ..lineTo(size.width + 0.05, size.height - 0.01)
      ..cubicTo(size.width - 4.07, size.height + 0.43, size.width - 8.16,
          size.height - 1.06, size.width - 11.04, size.height - 4.04)
      ..cubicTo(size.width - 16.0, size.height, size.width - 19.0, size.height,
          size.width - 22.0, size.height)
      ..close();
    canvas.drawPath(bubble, paint);
  }

  @override
  bool shouldRepaint(BubblePainter oldPainter) => true;
}

// This is my custom RenderObject.
class BubbleMessage extends SingleChildRenderObjectWidget {
  BubbleMessage({
    Key key,
    this.painter,
    Widget child,
  }) : super(key: key, child: child);

  final CustomPainter painter;

  @override
  RenderCustomPaint createRenderObject(BuildContext context) {
    return RenderCustomPaint(
      painter: painter,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, RenderCustomPaint renderObject) {
    renderObject..painter = painter;
  }
}

Use the BubbleMessage Widget like this:

import 'bubble.dart' 

...code ... 

BubbleMessage(
  painter: BubblePainter(),
  child: Container(
    constraints: BoxConstraints(
      maxWidth: 250.0,
      minWidth: 50.0,
    ),
    padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 6.0),
    child: Text(
      'your text variable',
      softWrap: true,
      style: TextStyle(
        fontSize: 16.0,
      ),
    ),
  ),
),

...code ...

The bubble effect:

enter image description here

Outcast answered 5/10, 2018 at 6:44 Comment(9)
You can't get size of widget before build. But you can get it after build and then call setState with updated parametersFiltrate
You should use a custom RenderObjectSollars
@AndreyTurkovsky I think it will be painted two times. Are there other solutions?Outcast
As I said, don't use widgets. Use a RenderObject.Sollars
@RémiRousselet Thanks. But I have no idea how to use RenderObject. Could you give me some tips?Outcast
The doc is pretty good, you can read docs.flutter.io/flutter/rendering/RenderBox-class.html + docs.flutter.io/flutter/rendering/RenderObject-class.html And if it's not enough you can dive into the code of Align or PaddingSollars
Check these links github.com/flutter/flutter/issues/18431#issuecomment-396990020 github.com/flutter/flutter/issues/19264 github.com/flutter/flutter/issues/21822Shepp
Thanks all. Finally, I used the TextPainter to measure the size before build method.Outcast
OK. Finally, I accepted @RémiRousselet 's suggestion. Having dived in RenderBox for a long time, I found the RenderCustomPaint widget can easily satisfy my need.Outcast
A
130

My apologies. This is not a direct answer on the topic's question! But If someone needs to get the size of a Text widget — this method can help. It helped me in creation of custom menu widget.

class TextSized extends StatelessWidget {
  const TextSized({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final String text = "Text in one line";
    final TextStyle textStyle = TextStyle(
      fontSize: 30,
      color: Colors.white,
    );
    final Size txtSize = _textSize(text, textStyle);

    // This kind of use - meaningless. It's just an example.
    return Container(
      color: Colors.blueGrey,
      width: txtSize.width,
      height: txtSize.height,
      child: Text(
        text,
        style: textStyle,
        softWrap: false,
        overflow: TextOverflow.clip,
        maxLines: 1,
      ),
    );
  }

  // Here it is!
  Size _textSize(String text, TextStyle style) {
    final TextPainter textPainter = TextPainter(
        text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr)
      ..layout(minWidth: 0, maxWidth: double.infinity);
    return textPainter.size;
  }
}
Album answered 4/2, 2020 at 21:23 Comment(1)
This code will always lay out text in a single line. So it's only good for getting the width of text in a single line unless you pass the width of text widget's parent to layout() as maxWidthSycophant
G
85

Problem with other answers is that if you use Text widget to display your text and constraint it with measurements result without considering default font family and scale factor, then you will get wrong results because Text widget is using device's textScaleFactor by default and passing it to RichText widget inside of it. This is the correct code to measure text size:

final Size size = (TextPainter(
        text: TextSpan(text: text, style: textStyle),
        maxLines: 1,
        textScaleFactor: MediaQuery.of(context).textScaleFactor,
        textDirection: TextDirection.ltr)
      ..layout())
    .size;
Gouge answered 23/6, 2020 at 13:42 Comment(9)
This should be the correct answer. Need to account for textScaleFactor or else result will be wrong on all devices except those that use the default textScaleFactor.Kirovabad
Thank you! That's the perfect solution for my probleme.Tother
This appears not to work with multi-line texts, even if changing maxLines to let's say 10. Why is that?Payoff
What do you mean by not working? what result you're getting? @PayoffGouge
With a text that spans over multiple lines on the screen, size as defined above provides the with and height of the text as when rendered in a single line (causing overflow). My workaround is height = (textSpanWidth / screenWidth).ceilToDouble(), but maybe there is a more reliable wayPayoff
I used this working solution for a while now. I just discovered that line breaks \n break this whole approach though. Any idea why and how to fix this? :)Loran
You can pass maxWidth to the layout method for multiline text (use in combination with maxLines > 1).Lipps
textScaleFactor is now deprecated. The deprecation message is: 'textScaleFactor' is deprecated and shouldn't be used. Use textScaler instead. Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. This feature was deprecated after v3.12.0-2.0.pre.Trisyllable
I posted a fixed version of this in a separate answer. But I'm not sure if this is 100% correct, please take a look.Trisyllable
S
31

A simple example:

For how it works see inline comments.

Inspiration from https://github.com/flutter/flutter/issues/23247

enter image description here

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Calc Text Size',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Calc Text Size'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const String loremIpsum =
      'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
      'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim '
      'veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea '
      'commodo consequat. Duis aute irure dolor in reprehenderit in voluptate '
      'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint '
      'occaecat cupidatat non proident, sunt in culpa qui officia deserunt '
      'mollit anim id est laborum.';

  @override
  Widget build(BuildContext context) {
    final mq = MediaQuery.of(context);
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: mq.size.height,
              width: 240.0,
              child: ListView(
                padding: EdgeInsets.all(4.0),
                children: <Widget>[
                  Container(
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.orange),
                    ),
                    child: Bubble(
                      text: TextSpan(
                        text: loremIpsum,
                        style: Theme.of(context).textTheme.body1,
                      ),
                    ),
                  ),
                  Container(
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.orange, width: 2.0),
                    ),
                    padding: EdgeInsets.symmetric(horizontal: 2.0),
                    child: Bubble(
                      text: TextSpan(
                        text: loremIpsum,
                        style: Theme.of(context).textTheme.body1,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class Bubble extends StatefulWidget {
  Bubble({@required this.text});

  final TextSpan text;

  @override
  _BubbleState createState() => new _BubbleState();
}

class _BubbleState extends State<Bubble> {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      // The text to render
      final textWidget = Text.rich(widget.text);

      // Calculate the left, top, bottom position of the end of the last text
      // line.
      final lastBox = _calcLastLineEnd(context, constraints);

      // Calculate whether the timestamp fits into the last line or if it has
      // to be positioned after the last line.
      final fitsLastLine =
          constraints.maxWidth - lastBox.right > Timestamp.size.width + 10.0;

      return Stack(
        children: [
          // Ensure the stack is big enough to render the text and the
          // timestamp.
          SizedBox.fromSize(
              size: Size(
                constraints.maxWidth,
                (fitsLastLine ? lastBox.top : lastBox.bottom) +
                    10.0 +
                    Timestamp.size.height,
              ),
              child: Container()),
          // Render the text.
          textWidget,
          // Render the timestamp.
          Positioned(
            left: constraints.maxWidth - (Timestamp.size.width + 10.0),
            top: (fitsLastLine ? lastBox.top : lastBox.bottom) + 5.0,
            child: Timestamp(DateTime.now()),
          ),
        ],
      );
    });
  }

  // Calculate the left, top, bottom position of the end of the last text
  // line.
  TextBox _calcLastLineEnd(BuildContext context, BoxConstraints constraints) {
    final richTextWidget = Text.rich(widget.text).build(context) as RichText;
    final renderObject = richTextWidget.createRenderObject(context);
    renderObject.layout(constraints);
    final lastBox = renderObject
        .getBoxesForSelection(TextSelection(
            baseOffset: 0, extentOffset: widget.text.toPlainText().length))
        .last;
    return lastBox;
  }
}

class Timestamp extends StatelessWidget {
  Timestamp(this.timestamp);

  final DateTime timestamp;

  /// This size could be calculated similarly to the way the text size in
  /// [Bubble] is calculated instead of using magic values.
  static final Size size = Size(60.0, 25.0);

  @override
  Widget build(BuildContext context) => Container(
        padding: EdgeInsets.all(3.0),
        decoration: BoxDecoration(
          color: Colors.greenAccent,
          border: Border.all(color: Colors.yellow),
        ),
        child:
            Text('${timestamp.hour}:${timestamp.minute}:${timestamp.second}'),
      );
}

enter image description here

Sharilyn answered 25/10, 2018 at 13:56 Comment(11)
On the same line do we have a way to measure a Column which has a number of widgets? Just wanted to know the height of the column before it's built. This is to calculate the aspect ratio for GridView so that all content is shown correctly.Kristeenkristel
If it's not about text medium.com/@diegoveloper/… might what you are looking forShepp
Gone through that. But what I understood is that the widget needs to be in the tree for those examples. For me the widget will be in the tree only after I get the calculation.Kristeenkristel
I don't think there is a way to get dimension without rendering. You can render it invisible, but the constraints need to be the real ones, otherwise you might get dimensions that are not related to what it will look like with real constraints.Shepp
I was trying if this could do something api.flutter.dev/flutter/widgets/Offstage-class.html It says Offstage can be used to measure the dimensions of a widget without bringing it on screen (yet). But no idea on how to do thatKristeenkristel
It's similar to invisible. Offstage renders outside the visible screen. You have to set up constraints that match the final render area to get proper measurements. I'm sure there are examples out there how to use offstage. Basically you just use Offstage widget as a parent widget of the widget you want to measure. You still need to render to be able to get dimensions.Shepp
Günter Zöchbauer is right. I tried to create comics with the date based on the size of the text. The result was that each letter has its own weight, so it is really difficult to calculate where the date will be at the time the text was written.Talkfest
@GünterZöchbauer Thanks for the example. I have been looking for something like this for days. Without this my chat app doesn't look similar to the conventional layout used by whatsapp and telegram. I have implemented this in my app but my challenge is that the render time for the chatDetail page in my app is up a mammoth 2000ms from 100ms (still guilty of it being slow). Is recommend this i a chat app with a lot of messages to render?Antitype
Hard to tell. Perhaps don't process messages that are not visible anyway. Try caching the text size results. They don't change as long as the layout does not change (sidemenu show/hide, portrait/landscape rotate, ...)Shepp
hi @GünterZöchbauer, thanks for sharing this. As of now, you are using a Sizedbox with a fixed width of 240 as the parent of the Layout Builder. However, in the whatsapp photo example that you showed, the width of the container varies according to the length of the text. If you were to use a dynamic width for the parent of the layout builder, would you still be able to get the constraint.maxwidth? I tried using constraint.maxwidth when the parent is dynamically sized and the constraint.maxwidth returns infinity.Thagard
i made use of constraints: BoxConstraints( minWidth: width * 0.25, maxWidth: width * 0.69,) to dynamically size the message bubble. However, doing so makes the constraint.maxwidth takes the maxwidth in the box constraints. Do you have any suggestion of how to go about this? Thanks in advance!Thagard
A
30

I found another method without using the context:

final constraints = BoxConstraints(
  maxWidth: 800.0, // maxwidth calculated
  minHeight: 0.0,
  minWidth: 0.0,
);

RenderParagraph renderParagraph = RenderParagraph(
  TextSpan(
    text: text,
    style: TextStyle(
      fontSize: fontSize,
    ),
  ),
  textDirection: ui.TextDirection.ltr,
  maxLines: 1,
);
renderParagraph.layout(constraints);
double textlen = renderParagraph.getMinIntrinsicWidth(fontSize).ceilToDouble();
Angloirish answered 11/7, 2019 at 21:40 Comment(1)
gets me an error: The height argument to getMinIntrinsicWidth was null.Unexacting
K
12

Multiline text height (modified variant of Dmitry_Kovalov)

import 'package:flutter/cupertino.dart';

extension StringExtension on String {
  double textHeight(TextStyle style, double textWidth) {
    final TextPainter textPainter = TextPainter(
      text: TextSpan(text: this, style: style),
      textDirection: TextDirection.ltr,
      maxLines: 1,
    )..layout(minWidth: 0, maxWidth: double.infinity);

    final countLines = (textPainter.size.width / textWidth).ceil();
    final height = countLines * textPainter.size.height;
    return height;
  }
}

You may need to import 'dart:ui' as ui; if ltr is not visible. Then textDirection: ui.TextDirection.ltr

Kevenkeverian answered 11/5, 2021 at 7:51 Comment(0)
S
6

from inspiring of the Günter Zöchbauer

List<bool> _calcLastLineEnd(String msg) {
  // self-defined constraint
  final constraints = BoxConstraints(
    maxWidth: 800.0, // maxwidth calculated
    minHeight: 30.0,
    minWidth: 80.0,
  );
  final richTextWidget =
      Text.rich(TextSpan(text: msg)).build(context) as RichText;
  final renderObject = richTextWidget.createRenderObject(context);
  renderObject.layout(constraints);
  final boxes = renderObject.getBoxesForSelection(TextSelection(
      baseOffset: 0, extentOffset: TextSpan(text: msg).toPlainText().length));
  bool needPadding = false, needNextline = false;
  if (boxes.length < 2 && boxes.last.right < 630) needPadding = true;
  if (boxes.length < 2 && boxes.last.right > 630) needNextline = true;
  if (boxes.length > 1 && boxes.last.right > 630) needNextline = true;
  return [needPadding, needNextline];
}
Sluggish answered 7/5, 2019 at 18:44 Comment(1)
Very helpful. I used it Thai characters, whose width and height can vary greatly. Interestingly, the height for all characters never changed. Even though some are twice the height of others. Fortunately, the width value did vary depending on the character's observable width.Mammoth
T
2

Here's a fixed version of the answer by Amir_P, given that textScaleFactor is now deprecated:

Size measureText({
  required BuildContext context,
  required String text,
  required TextStyle textStyle,
}) {
  assert(textStyle.fontSize != null);
  return (TextPainter(
    text: TextSpan(
      text: text,
      style: textStyle.copyWith(
          fontSize:
              MediaQuery.textScalerOf(context).scale(textStyle.fontSize!)),
    ),
    maxLines: 1,
    textDirection: Directionality.of(context),
  )..layout()).size;
}

However I'm not sure if the new textScaler stuff will now scale the text size twice with the above solution, with the new Android 14 nonlinear scaling framework. If somebody could please enlighten me in the comments, I'd appreciate it.

https://docs.flutter.dev/release/breaking-changes/deprecate-textscalefactor

Trisyllable answered 25/11, 2023 at 0:7 Comment(0)
L
1

I just discovered that line breaks can break a couple of approaches here, so just for completeness I solved it very hacky like this:

  1. Replace all \n before doing the calculation: text.replace('\n', '')
  2. Get number of line breaks: text.split('\n').length
  3. Add this to the number of lines, before multiplying the line count with the line height

Also keep in mind that you have to account for any eventual padding, when calculating the number of lines (sizeFull.width / (mediaQuery.size.width - PADDING)).ceil()

Full Example:

  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);

    final Size sizeFull = (TextPainter(
      text: TextSpan(
        text: text.replaceAll('\n', ''),
        style: textStyle,
      ),
      textScaleFactor: mediaQuery.textScaleFactor,
      textDirection: TextDirection.ltr,
    )..layout())
        .size;


    final numberOfLinebreaks = text.split('\n').length;


    final numberOfLines =
        (sizeFull.width / (mediaQuery.size.width)).ceil() +
            numberOfLinebreaks;

    return Container(
              height: sizeFull.height * numberOfLines,
              child: Text(text),
           );
  }
Loran answered 25/4, 2022 at 13:1 Comment(1)
This is what I have also done. I was looking for a more elegant solution but could not find one. Just one question why are you using size on TextPainter? Even without Size, you can get the width from the TextPainter.Comptroller
R
0

Modified variant of Günter Zöchbauer solution wich can use text overflow

TextBox textBox(BuildContext context, String text, TextStyle textStyle,
    int maxLines, TextOverflow overflow, BoxConstraints constraints) {
  final textSpan = TextSpan(
    text: text,
    style: textStyle,
  );
  final richTextWidget = Text.rich(
    textSpan,
    maxLines: maxLines,
    overflow: overflow,
  ).build(context) as RichText;
  final renderObject = richTextWidget.createRenderObject(context);
  renderObject.layout(constraints);
  final boxesForSelection = renderObject.getBoxesForSelection(TextSelection(
      baseOffset: 0, extentOffset: richTextWidget.text.toPlainText().length));
  if (boxesForSelection.length == 0)
    return TextBox.fromLTRBD(0.0, 0.0, 0.0, 0.0, TextDirection.ltr);
  final List<double> widths = List();
  boxesForSelection.forEach((box) {
    widths.add(box.right);
  });
  widths.sort((a, b) => a.compareTo(b));
  return TextBox.fromLTRBD(
      0.0, 0.0, widths.last, boxesForSelection.last.bottom, TextDirection.ltr);
}
Rhubarb answered 13/12, 2020 at 15:48 Comment(0)
A
0

@IvanPavliuk 's answer is mostly correct, but there are some instances, when (textPainter.size.width / textWidth) is very close to a whole number, that countLines is one too few and the calculated height is too short.

The following worked for me:

double textHeight(String text, double width, {required TextStyle style}) {
    final span = TextSpan(text: text, style: style);
    final tp = TextPainter(text: span, textDirection: TextDirection.ltr);
    tp.layout(maxWidth: width);
    final numLines = tp.computeLineMetrics().length;
    return numLines * tp.size.height;
}
Abidjan answered 4/10, 2023 at 20:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.