Search code examples
flutterdartlayoutwidget

How can I get the height of a widget?


I don't understand how LayoutBuilder is used to get the height of a widget.

I need to display the list of Widgets and get their height, so I can compute some special scroll effects. I am developing a package and other developers provide a widget (I don't control them). I read that LayoutBuilder can be used to get the height.

In a very simple case, I tried to wrap a widget in LayoutBuilder.builder and put it in the stack, but I always get minHeight 0.0, and maxHeight INFINITY. Am I misusing the LayoutBuilder?

It seems that LayoutBuilder is a no go. I found the CustomSingleChildLayout which is almost a solution.

I extended that delegate, and I was able to get the height of widget in getPositionForChild(Size size, Size childSize) method. but, the first method that is called is Size getSize(BoxConstraints constraints) and as constraints, I get 0 to INFINITY because I'm laying these CustomSingleChildLayouts in a ListView.

My problem is that SingleChildLayoutDelegate getSize operates like it needs to return the height of a view. I don't know the height of a child at that moment. I can only return constraints.smallest (which is 0, and the height is 0), or constraints.biggest which is infinity and crashes the app.

In the documentation it even says:

...but the size of the parent cannot depend on the size of the child.

And that's a weird limitation.


Solution

  • To get the size/position of a widget on screen, you can use GlobalKey to get its BuildContext to then find the RenderBox of that specific widget, which will contain its global position and rendered size.

    There is just one thing to be careful of: That context may not exist if the widget is not rendered. Which can cause a problem with ListView as widgets are rendered only if they are potentially visible.

    Another problem is that you can't get a widget's RenderBox during the build call as the widget hasn't been rendered yet.


    But what if I need to get the size during the build! What can I do?

    There's one cool widget that can help: Overlay and its OverlayEntry. They are used to display widgets on top of everything else (similar to the stack).

    But the coolest thing is that they are on a different build flow; they are built after regular widgets.

    That have one super cool implication: OverlayEntry can have a size that depends on widgets of the actual widget tree.


    Okay. But don't OverlayEntry requires to be rebuilt manually?

    Yes, they do. But there's another thing to be aware of: ScrollController, passed to a Scrollable, is a listenable similar to AnimationController.

    Which means you could combine an AnimatedBuilder with a ScrollController. It would have the lovely effect to rebuild your widget automatically on a scroll. Perfect for this situation, right?


    Combining everything into an example:

    In the following example, you'll see an overlay that follows a widget inside a ListView and shares the same height.

    import 'package:flutter/material.dart';
    import 'package:flutter/scheduler.dart';
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key, this.title}) : super(key: key);
      final String? title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      final controller = ScrollController();
      OverlayEntry? sticky;
      GlobalKey stickyKey = GlobalKey();
    
      @override
      void initState() {
        sticky?.remove();
    
        sticky = OverlayEntry(
          builder: (context) => stickyBuilder(context),
        );
    
        SchedulerBinding.instance.addPostFrameCallback((_) {
          if (sticky != null) {
            Overlay.of(context).insert(sticky!);
          }
        });
    
        super.initState();
      }
    
      @override
      void dispose() {
        sticky?.remove();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.black,
          body: ListView.builder(
            controller: controller,
            itemBuilder: (context, index) {
              if (index == 6) {
                return Container(
                  key: stickyKey,
                  height: 100.0,
                  color: Colors.green,
                  child: const Text("I'm fat"),
                );
              }
              return ListTile(
                title: Text(
                  'Hello $index',
                  style: const TextStyle(color: Colors.white),
                ),
              );
            },
          ),
        );
      }
    
      Widget stickyBuilder(BuildContext context) {
        return AnimatedBuilder(
          animation: controller,
          builder: (context, child) {
            final keyContext = stickyKey.currentContext;
            if (keyContext != null) {
              // widget is visible
              final box = keyContext.findRenderObject() as RenderBox;
              final pos = box.localToGlobal(Offset.zero);
              return Positioned(
                top: pos.dy + box.size.height,
                left: 50.0,
                right: 50.0,
                height: box.size.height,
                child: Material(
                  child: Container(
                    alignment: Alignment.center,
                    color: Colors.purple,
                    child: const Text("^ Nah I think you're okay"),
                  ),
                ),
              );
            }
            return Container();
          },
        );
      }
    }
    

    Note:

    When navigating to a different screen, call the following. Otherwise, sticky would stay visible.

    sticky.remove();