Reputation: 1717
I have a Row() with two Text() elements in it. Usually there is ample space for both of them, but just occasionally there isn't and I get results like one of the following, depending on the layout settings I'm using:
What I would really like to happen is if there is sufficient space for both elements then they should be laid left and right aligned (as in the second tile). But, otherwise one, or both, should be truncated in such a way that the entire row width is used to show as much as possible, with some space between.
I've read the description of RenderFlex, and experimented with Flexible() and Expanded() wrappers, but can't make it behave like that.
The code I'm currently using is below. I can get close to what I want, in this particular case, by tweaking the flex values, but I'm hoping there's a solution rather more robust.
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 10,
fit: FlexFit.loose,
child: Text(
'${instrument.exchDisp}/${instrument.exch}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.instrumentExchangeText(context),
),
),
Spacer(flex: 1,),
Flexible(
flex: 10,
fit: FlexFit.loose,
child: Text(
'${instrument.typeDisp}/${instrument.type}',
textAlign: TextAlign.right,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.instrumentTypeText(context),
),
),
],
),
Upvotes: 0
Views: 531
Reputation: 1717
I believe the following is needed to fix this in the general case.
class DifficultRow extends StatelessWidget {
final Instrument instrument;
Size totalSpaceAvailable;
GlobalKey _keyText1 = GlobalKey(debugLabel: "key1");
GlobalKey _keyText2 = GlobalKey(debugLabel: "key2");
double widthText1 = 0;
double widthText2 = 0;
double widthSpace = 5;
static const double minimumProportionAllowed = 0.25;
DifficultRow(this.instrument, {Key key}) : super(key: key);
double _getContainerSize(BuildContext context, GlobalKey key) {
final RenderBox renderBoxText = key.currentContext.findRenderObject();
final size = renderBoxText.size;
print("Key: ${key}, Size: $size");
}
double _getTextSize(BuildContext context, GlobalKey key) {
Widget widget = key.currentContext.widget;
print("type: ${widget.runtimeType}");
assert(widget is Text, "Text1 is not of type Text");
Text textWidget = widget as Text;
print("text: ${textWidget}");
TextSpan span = TextSpan(
text: textWidget.data, style: textWidget.style); //todo textSpan might be needed here instead if span passed into Text()?
print("span: ${span}");
final BoxConstraints constraints =
BoxConstraints(maxWidth: double.infinity);
final richTextWidget = Text.rich(span).build(context) as RichText;
final renderObject = richTextWidget.createRenderObject(context);
renderObject.layout(constraints);
final boxes = renderObject.getBoxesForSelection(TextSelection(
baseOffset: 0,
extentOffset: span.toPlainText().length,
));
print("boxes: ${boxes}");
double textWidth = boxes.last.right;
return textWidth;
}
@override
Widget build(BuildContext context) {
// instrument.exchDisp = "EDEDEDEDMMMMMMMMMMMMMMMMM";
// instrument.exch = "E";
// instrument.typeDisp = ".";
// instrument.type = "T";
// After build, rebuild it again now we have the concrete sizes
// this idea from: https://fidev.io/water-drop/
// and https://github.com/marcelgarus/marquee/blob/master/lib/marquee.dart
if (totalSpaceAvailable == null) {
SchedulerBinding.instance.addPostFrameCallback((_) {
// setState(() {...}); can't use as stateless
// this performs the same as setState, but works for a StatelessWidget:
(context as Element).markNeedsBuild();
// this might be alternative: https://stackoverflow.com/questions/54733504/flutter-how-to-get-the-size-of-a-widget
totalSpaceAvailable = context.size;
debugPrint("*** totalSize: $totalSpaceAvailable");
// Doubles get floored somewhere in flutter, so make sure there's enough space by
// increasing required spaces and decreasing allowed spaces.
double spaceAvailableInRow = totalSpaceAvailable.width.floorToDouble();
double text1Allowed = _getContainerSize(context, _keyText1);
double text1RequiredSpace = _getTextSize(context, _keyText1).ceilToDouble();
double text2Allowed = _getContainerSize(context, _keyText2);
double text2RequiredSpace = _getTextSize(context, _keyText2).ceilToDouble();
if (text1RequiredSpace+text2RequiredSpace+widthSpace <= spaceAvailableInRow) {
// we have enough space to fit them both, just use them as-is
debugPrint("Enough space, $text1RequiredSpace+$text2RequiredSpace+$widthSpace=${text1RequiredSpace+text2RequiredSpace+widthSpace} <= ${spaceAvailableInRow}");
widthText1 = text1RequiredSpace;
widthText2 = text2RequiredSpace;
} else {
// we don't have enough space, have to shrink one or both
debugPrint("Not enough space, $text1RequiredSpace+$text2RequiredSpace+$widthSpace=${text1RequiredSpace+text2RequiredSpace+widthSpace} > ${totalSpaceAvailable.width}");
double spaceAvailableForText = spaceAvailableInRow - widthSpace;
double totalSpaceRequiredForText = text1RequiredSpace + text2RequiredSpace;
double text1RequiredSpaceProportion = text1RequiredSpace / totalSpaceRequiredForText;
double text2RequiredSpaceProportion = text2RequiredSpace / totalSpaceRequiredForText;
// Ensure one long text can't push out all of the other one
if ( min(text1RequiredSpaceProportion, text2RequiredSpaceProportion) < minimumProportionAllowed ) {
debugPrint("Will fix compressed proportions, $text1RequiredSpaceProportion:$text2RequiredSpaceProportion");
if (text1RequiredSpaceProportion < text2RequiredSpaceProportion) {
text1RequiredSpaceProportion = minimumProportionAllowed;
} else {
text1RequiredSpaceProportion = 1 - minimumProportionAllowed;
}
text2RequiredSpaceProportion = 1 - text1RequiredSpaceProportion;
debugPrint("Fixed compressed proportions, now: $text1RequiredSpaceProportion:$text2RequiredSpaceProportion");
}
double shrinkageFactor = spaceAvailableForText / totalSpaceRequiredForText;
widthText1 = totalSpaceRequiredForText * text1RequiredSpaceProportion * shrinkageFactor;
widthText2 = totalSpaceRequiredForText * text2RequiredSpaceProportion * shrinkageFactor;
// Check we haven't allocated excessive space to the smaller one
if (widthText1 > text1RequiredSpace) {
debugPrint("Text1 overly big, $widthText1 > $text1RequiredSpace");
widthText1 = text1RequiredSpace;
widthText2 = spaceAvailableForText - widthText1;
debugPrint("Text1 reduced to $widthText1, Text2 increased to $widthText2");
} else if (widthText2 > text2RequiredSpace) {
debugPrint("Text2 overly big, $widthText2 > $text2RequiredSpace");
widthText2 = text2RequiredSpace;
widthText1 = spaceAvailableForText - widthText2;
debugPrint("Text2 reduced to $widthText2, Text1 increased to $widthText1");
}
}
widthText1 = widthText1.floorToDouble();
widthText2 = widthText2.floorToDouble();
debugPrint("Setting widths $widthText1 and $widthText2");
}); // callback
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//
Container(
width: widthText1,
child: Text(
'${instrument.exchDisp}/${instrument.exch}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.instrumentExchangeText(context),
key: _keyText1,
),
),
//
SizedBox(width: widthSpace),
//
Container(
width: widthText2,
child: Text(
'${instrument.typeDisp}/${instrument.type}',
textAlign: TextAlign.right,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.instrumentTypeText(context),
key: _keyText2,
),
)
//
],
);
}
}
Upvotes: 0
Reputation: 28060
I think the key is to use only one Expanded
and leave the other Row
child widget as as fixed size.
Flutter's Flex
widgets (Row
and Column
) will lay out anything not Flexible
/ Expanded
first, then will lay out Flexible
/ Expanded
items with the remaining space.
So the below combination will layout the middle spacer and "right side" text first, then use any remaining space for the "left side" long text.
For the spacer in between, I just use a SizedBox
.
import 'package:flutter/material.dart';
class FlexTextWrapPage2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flex Text Wrap'),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(// ← uses any remaining space
child: Container(
color: Colors.redAccent.withOpacity(.2),
child: Text('left side may be really long',
style: TextStyle(fontSize: 30),
overflow: TextOverflow.ellipsis,
),
)),
SizedBox(width: 5), // ← laid out in first phase
Container( // ← laid out in first phase
color: Colors.indigoAccent.withOpacity(.2),
child: Text('right side',
style: TextStyle(fontSize: 30),))
],
),
),
);
}
}
The colored containers are just for visibility.
I go into some more detail how/why this works in this somewhat related answer.
Upvotes: 1