Robert Estivill
Robert Estivill

Reputation: 12497

How to measure widget width and check for fit in container

I have a list of Widgets that I want to display in non-scrollable horizontal layout.

In the example below, I have 5 Widgets, 2 of them are fully displayed and the other 3 do not fit, hence the '+3' label.

enter image description here

I do not mind if there is some empty space after the label, but the important thing is that we only display Widgets that fully fit in the row.

I guess the main problem is how can I test that a Widget fits based on it's width into a row?

I thought about VisibilityDetector, but I will ended up rendering all widgets, then removing the ones that are not 100% visible, and the entire logic seems quite flawed for this use case.

Any ideas? Thanks

Upvotes: 2

Views: 1103

Answers (2)

cloudpham93
cloudpham93

Reputation: 566

try my version:

class Home extends StatefulWidget {
  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // widget
    List<GlobalKey> keys = [];
    var widgets = List.generate(25, (index) {
      GlobalKey globalKey = GlobalKey();
      keys.add(globalKey);
      return FilterChip(
        key: globalKey,
        label: Text("asdasasd"),
      );
    });
    return Scaffold(
      appBar: AppBar(
        title: Text('AppBar'),
      ),
      body: Column(
        children: [
          OverRow(
            keys: keys,
            widgets: widgets,
          ),
        ],
      ),
    );
  }
}

class OverRow extends StatefulWidget {
  OverRow({Key key, this.keys, this.widgets}) : super(key: key);
  List<GlobalKey> keys = [];
  List<Widget> widgets = [];

  @override
  State<OverRow> createState() => _OverRowState();
}

class _OverRowState extends State<OverRow> {
  int overIndex = -1;

  @override
  void initState() {
    // TODO: implement initState
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      for (var i = 0; i < widget.keys.length; i++) {
        final box = widget.keys[i].currentContext.findRenderObject() as RenderBox;
        final pos = box.localToGlobal(Offset.zero);
        var over = pos.dx + box.size.width > MediaQuery.of(context).size.width;
        if (over) {
          overIndex = i;
          setState(() {});
          return;
        }
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Row(
        children: overIndex == -1
            ? widget.widgets
            : [
                ...widget.widgets.take(overIndex).toList(),
                Container(
                  child: Text("+${widget.widgets.skip(overIndex).length}"),
                )
              ]);
  }
}

Upvotes: 1

Allan Mitre
Allan Mitre

Reputation: 321


EDIT: Included dynamic width based in label length

Try this, with defined width of item container and the +label or with dynamic calculating the width based on font size.

Change useFixedWidth to true or false if you want dynamic or static width.

Example in a Stateless widget:

class MyWidget extends StatelessWidget {
  final double itemWidth = 100; //maxwidth if item container
  final double labelWidth = 40; //maxWidth of +label

  List<String> items = List.generate(
      10, (index) => 'Name ${index + 1}'); //replace with a real list of items
  final bool useFixedWidth =
      false; // define if want to calculate dynamic with of each item
  final double letterWidth =
      12; // defined width of a letter || change depending on font size

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      int totalItems = items.length;
      int maxDisplayItems = 0;
      List<double> dynamicItemsWidth = []; //if want to use dynamic width

      if (useFixedWidth) {
        //if want to use fixed width
        maxDisplayItems =
            (((constraints.maxWidth - labelWidth) / itemWidth).floor()).clamp(
                0, totalItems); //check how many items fit including the +label

      } else {
        //if want to calculate based on string length
        dynamicItemsWidth = items
            .map((e) => e.length * letterWidth)
            .toList(); //calculate individual item width

        double _acumWidth = 0.0;
        for (var width in dynamicItemsWidth) {
          _acumWidth = _acumWidth + width;
          if (_acumWidth < (constraints.maxWidth - labelWidth)) {
            maxDisplayItems++;
          }
        }
      }

      bool showPlusSign =
          maxDisplayItems < totalItems; //check if all items can be shown
      return Row(children: [
        Row(
            children: List.generate(maxDisplayItems, (index) {
          return SizedBox(
              height: itemWidth,
              width: useFixedWidth ? itemWidth : dynamicItemsWidth[index],
              child: Container(
                  //color: Colors.red,
                  //width: itemWidth,
                  child: Center(child: Text('Name ${index + 1}'))));
        }).toList()),
        if (showPlusSign)
          SizedBox(
              width: labelWidth,
              height: itemWidth,
              child: Center(child: Text('+${totalItems - maxDisplayItems}')))
      ]);
    });
  }
}

shows this dynamic layout for 10 items:

enter image description here

Upvotes: 1

Related Questions