Handle row overflow using ellipsis similar to textoverflow

Issue

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

Solution

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;
}

Answered By – user1032613

Answer Checked By – Timothy Miller (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *