Reputation: 523
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.
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:
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:
Upvotes: 3
Views: 1647
Reputation: 26
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
Upvotes: 1
Reputation: 523
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:
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();
Upvotes: 2