Ian
Ian

Reputation: 1717

Can RenderFlex use the entire Row() and adapt for overflowing Text() elements?

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:

ex 1

ex 2

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

Answers (2)

Ian
Ian

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

Baker
Baker

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),))
          ],
        ),
      ),
    );
  }
}

Here's the result: Example Row

The colored containers are just for visibility.

I go into some more detail how/why this works in this somewhat related answer.

Upvotes: 1

Related Questions