4

I have a row with multiple children and I want to show the children of the row on a single line. However, if there is not enough room for all of the children, I want to show an ellipsis at the end of the line, just as we have the overflow and maxlines properties for text to handle the overflow problem. Is there any workaround to do it for row?

Row(
  children: [
    TagItem('Education'),
    TagItem('GPA'),
    TagItem('Books'),
    TagItem('University'),
    TagItem('Library'),
  ],
)

I want something like this:enter image description here

Ehsan Askari
  • 843
  • 7
  • 19
  • Can you include your snippet that you've tried so far? – Md. Yeasin Sheikh Feb 28 '22 at 06:24
  • @YeasinSheikh I am not even close to it. So there is no point in including the code here. In my code, there is only a row with some children. – Ehsan Askari Feb 28 '22 at 06:32
  • I think that is enough to reproduce the error, check more about [minimal-reproducible-example](https://stackoverflow.com/help/minimal-reproducible-example) – Md. Yeasin Sheikh Feb 28 '22 at 06:33
  • Those guidelines are very helpful in complex situations, but for a scenario as simple as a row with some children, it is not necessary. However, I have included the code snippet. – Ehsan Askari Feb 28 '22 at 06:43
  • You can measure the size of each text box and max width to control overflow, not sure about efficiency. You can read about [size of the Text Widget](https://stackoverflow.com/q/52659759/10157127) IF every `tagItem` comes with same size, it will be simpler by comparing width. – Md. Yeasin Sheikh Feb 28 '22 at 07:24

1 Answers1

4

Two possible approaches:

This is one of the harder questions on flutter layout. There are 2 approaches, one is kinda hard, the other one is harder. If needed, I can provide some example code, but I will first describe the approaches here and see if you can do it yourself.

  1. use CustomMultiChildLayout. In this widget, you are given (almost) full control of the flutter layout pipeline, you can measure the total available size, and more importantly, you can measure the size of each of your children, and then determine what to render. The downside of this solution is, you cannot set the parent (CustomMultiChildLayout) size based on its children's size. In your case, this means you cannot dynamically decide the height of your widget. If setting fixed height with a SizedBox (say height: 120 or whatever) sounds reasonable, you should go with this approach.

  2. write your own RenderBox. In your case, you should look into extending MultiChildRenderObjectWidget. Basically, all these convenient widgets you use every day, like Row or Column, are all RenderBox under the hood (that someone working on Flutter already implemented for you). So if those aren't enough to suit your needs, you can always create more!


Proof of concept using method #1:

A custom RowWithOverflow widget that takes in a fixed height, an overflow widget, and all the children.

Example Usage:

RowWithOverflow(
  height: 100,
  overflow: Chip(label: Text('...')),
  children: [
    Chip(label: Text('Item 1')),
    Chip(label: Text('Item 2')),
    Chip(label: Text('Item 3')),
  ],
)

Demo App:

demo screenshot

Source Code for Demo App:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _count = 3;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: Column(
          children: [
            Text('CustomMultiChildLayout demo:'),
            RowWithOverflow(
              height: 50,
              overflow: Chip(label: Text('...')),
              children: [
                for (int i = 0; i < _count; i++)
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 1.0),
                    child: Chip(label: Text('Item $i')),
                  ),
              ],
            ),
            ElevatedButton(
              onPressed: () => setState(() => _count += 1),
              child: Text('Add item'),
            ),
          ],
        ),
      ),
    );
  }
}

class RowWithOverflow extends StatelessWidget {
  final double height;
  final List<Widget> children;
  final Widget overflow;

  const RowWithOverflow({
    Key? key,
    required this.height,
    required this.children,
    required this.overflow,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: height,
      child: ClipRect(
        child: CustomMultiChildLayout(
          delegate: MyDelegate(children.length),
          children: [
            for (int i = 0; i < children.length; i++)
              LayoutId(
                id: i,
                child: children[i],
              ),
            LayoutId(
              id: 'overflow',
              child: overflow,
            ),
          ],
        ),
      ),
    );
  }
}

class MyDelegate extends MultiChildLayoutDelegate {
  final int _childrenCount;

  MyDelegate(this._childrenCount);

  @override
  void performLayout(Size size) {
    // Get the size of the overflow item.
    final Size overflowSize = layoutChild(
      'overflow',
      BoxConstraints.loose(size),
    );
    // Get sizes of all children.
    final List<Size> childrenSizes = [
      for (int i = 0; i < _childrenCount; i++)
        layoutChild(i, BoxConstraints.loose(size)),
    ];
    // Hide everything for now.
    positionChild('overflow', Offset(0, -2000));
    for (int i = 0; i < _childrenCount; i++) {
      positionChild(i, Offset(0, -2000));
    }

    // Carefully position each child until we run out of space.
    Offset offset = Offset.zero;
    for (int i = 0; i < _childrenCount; i++) {
      if (offset.dx + childrenSizes[i].width < size.width) {
        positionChild(i, offset);
        offset += Offset(childrenSizes[i].width, 0);
      } else {
        positionChild('overflow', offset);
        offset = Offset(0, 200);
        break;
      }
    }
  }

  @override
  bool shouldRelayout(oldDelegate) => false;
}
WSBT
  • 33,033
  • 18
  • 128
  • 133
  • thanks for the solutions. I think the first approach works for me. I went through it and found it pretty difficult to implement the ```performLayout``` of the ```MultiChildLayoutDelegate```. I would be super glad if you include an example for this scenario. Actually, the difficult part was that there are unknown number of children and laying out those ellipsis. – Ehsan Askari Feb 28 '22 at 10:37
  • @EhsanAskari Sure, I've included a proof-of-concept for method #1. You can take the source code as a starting point and modify it to suit your needs. – WSBT Mar 04 '22 at 18:41
  • Thank you for the example. However, there is a problem with the solution. When positioning the ```overflow``` widget, we do not know if there is enough space for it, and if there isn't, the ```overflow``` widget will be positioned partly or fully out of visible UI. – Ehsan Askari Mar 05 '22 at 05:29
  • However, I had managed to work it out myself but my code is very messy unlike yours which is very clean and organized. I will try to adapt to your coding style. – Ehsan Askari Mar 05 '22 at 05:33
  • This is just a proof-of-concept, it's meant to give you an example as a starting point, it does not cover everything for you. For example, it also does not consider the case where the "overflow widget is bigger than the last widget". You should take my code as a starting point, and handle edge cases yourself. – WSBT Mar 05 '22 at 05:48