How can I get the height of a widget?

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
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final controller = ScrollController();
  OverlayEntry sticky;
  GlobalKey stickyKey = GlobalKey();

  @override
  void initState() {
    if (sticky != null) {
      sticky.remove();
    }
    sticky = OverlayEntry(
      builder: (context) => stickyBuilder(context),
    );

    SchedulerBinding.instance.addPostFrameCallback((_) {
      Overlay.of(context).insert(sticky);
    });

    super.initState();
  }

  @override
  void dispose() {
    sticky.remove();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      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: (_,Widget 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();

Leave a Comment