How to get height of a Widget?

165,035

Solution 1

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.

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 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 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 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 following otherwise sticky would stay visible.

sticky.remove();

Solution 2

This is (I think) the most straightforward way to do this.

Copy-paste the following into your project.

UPDATE: using RenderProxyBox results in a slightly more correct implementation, because it's called on every rebuild of the child and its descendants, which is not always the case for the top-level build() method.

NOTE: This is not exactly an efficient way to do this, as pointed by Hixie here. But it is the easiest.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';


typedef void OnWidgetSizeChange(Size size);

class MeasureSizeRenderObject extends RenderProxyBox {
  Size? oldSize;
  final OnWidgetSizeChange onChange;

  MeasureSizeRenderObject(this.onChange);

  @override
  void performLayout() {
    super.performLayout();

    Size newSize = child!.size;
    if (oldSize == newSize) return;

    oldSize = newSize;
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      onChange(newSize);
    });
  }
}

class MeasureSize extends SingleChildRenderObjectWidget {
  final OnWidgetSizeChange onChange;

  const MeasureSize({
    Key? key,
    required this.onChange,
    required Widget child,
  }) : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return MeasureSizeRenderObject(onChange);
  }
}

Then, simply wrap the widget whose size you would like to measure with MeasureSize.

var myChildSize = Size.zero;

Widget build(BuildContext context) {
  return ...( 
    child: MeasureSize(
      onChange: (size) {
        setState(() {
          myChildSize = size;
        });
      },
      child: ...
    ),
  );
}

So yes, the size of the parent cannot can depend on the size of the child if you try hard enough.


Personal anecdote - This is handy for restricting the size of widgets like Align, which likes to take up an absurd amount of space.

Solution 3

Here's a sample on how you can use LayoutBuilder to determine the widget's size.

Since LayoutBuilder widget is able to determine its parent widget's constraints, one of its use case is to be able to have its child widgets adapt to their parent's dimensions.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var dimension = 40.0;

  increaseWidgetSize() {
    setState(() {
      dimension += 20;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(children: <Widget>[
          Text('Dimension: $dimension'),
          Container(
            color: Colors.teal,
            alignment: Alignment.center,
            height: dimension,
            width: dimension,
            // LayoutBuilder inherits its parent widget's dimension. In this case, the Container in teal
            child: LayoutBuilder(builder: (context, constraints) {
              debugPrint('Max height: ${constraints.maxHeight}, max width: ${constraints.maxWidth}');
              return Container(); // create function here to adapt to the parent widget's constraints
            }),
          ),
        ]),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increaseWidgetSize,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Demo

demo

Logs

I/flutter (26712): Max height: 40.0, max width: 40.0
I/flutter (26712): Max height: 60.0, max width: 60.0
I/flutter (26712): Max height: 80.0, max width: 80.0
I/flutter (26712): Max height: 100.0, max width: 100.0

Update: You can also use MediaQuery to achieve similar function.

@override
Widget build(BuildContext context) {
  var screenSize = MediaQuery.of(context).size ;
  if (screenSize.width > layoutSize){
    return Widget(); 
  } else {
    return Widget(); /// Widget if doesn't match the size
  }
}

Solution 4

Let me give you a widget for that


class SizeProviderWidget extends StatefulWidget {
  final Widget child;
  final Function(Size) onChildSize;

  const SizeProviderWidget(
      {Key? key, required this.onChildSize, required this.child})
      : super(key: key);
  @override
  _SizeProviderWidgetState createState() => _SizeProviderWidgetState();
}

class _SizeProviderWidgetState extends State<SizeProviderWidget> {
  @override
  void initState() {
    ///add size listener for first build
    _onResize();
    super.initState();
  }

  void _onResize() {
    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
      if (context.size is Size) {
        widget.onChildSize(context.size!);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    ///add size listener for every build uncomment the fallowing
    ///_onResize();
    return widget.child;
  }
}

EDIT Just wrap the SizeProviderWidget with OrientationBuilder to make it respect the orientation of the device

Solution 5

If I understand correctly, you want to measure the dimension of some arbitrary widgets, and you can wrap those widgets with another widget. In that case, the method in the this answer should work for you.

Basically the solution is to bind a callback in the widget lifecycle, which will be called after the first frame is rendered, from there you can access context.size. The catch is that you have to wrap the widget you want to measure within a stateful widget. And, if you absolutely need the size within build() then you can only access it in the second render (it's only available after the first render).

Share:
165,035

Related videos on Youtube

JoKr
Author by

JoKr

Mobile and web developer student from Croatia, wannabe robot, currently working with Android.

Updated on January 03, 2022

Comments

  • JoKr
    JoKr over 2 years

    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 widget (I don't control them). I read that LayoutBuilder can be used to get height.

    In very simple case, I tried to wrap 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?

    EDIT: 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, the height is 0), or constraints.biggest which is infinity and crashes the app.

    In the docs it even says:

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

    And that's a weird limitation.

    • Jonah Williams
      Jonah Williams about 6 years
      LayoutBuilder will give you the box constraints of the parent. If you want the sizes of the child you need a different strategy. One example I can point to is the Wrap widget, it does layout based on the size of it's children in the associated RenderWrap class. This happens during layout though, not build().
    • JoKr
      JoKr about 6 years
      @JonahWilliams Hmm. I don't see how Wrap can help me since it's widget designed to layout children around (works something like flexbox grid from the web). I have one child widget that I need to find the height of. Please, see the edit in the question. I almost solved the problem with CustomSingleChildLayout but got stuck on its limitation.
    • Rémi Rousselet
      Rémi Rousselet about 6 years
      Can you explain what you want more specifically ? There are multiple solutions. But each have different use cases.
    • JoKr
      JoKr about 6 years
      Sure. I am developing a package. User/Developer provides a Widgets to my class. We are talking about any widget here, from new Text("hello") to more complex ones. I lay these widgets into ListView, and I need their height to compute some scroll effects. I am OK with getting the height at the layout time, just like what SingleChildLayoutDelegate is doing.
    • Rémi Rousselet
      Rémi Rousselet about 6 years
      What do you mean by "Scroll effects" ? Do you have an example ?
    • JoKr
      JoKr about 6 years
      Something like sticky-headers lists. I need a height of rows to calculate which row is at the top, how far is the next row, and then make the translation of the header row on the Y-axis that's drawn on top of the list. I already achieved this, but with height as an input. I would like to remove this height parameter, so user/developer doesn't need to know the height of every row.
    • JoKr
      JoKr about 6 years
      There is also the issue open at github.com/flutter/flutter/issues/16061
    • rmtmckenzie
      rmtmckenzie about 6 years
      I think that using a CustomScrollView and Slivers might be a solution in this case, but I have no time to investigate it. The only thing is that it is implemented for having only one AppBar at the top; you would have to write your own SliverHeader following SliverAppBar's implementation or something like it. The issue with using a CustomSingleChildLayout is that it does depend on the children' height, so have to calculate the entire height of the list each time which could be slow depending on how many widgets you're rendering (and imposes certain constraints on those widgets).
  • JoKr
    JoKr about 6 years
    Ok, finally got time to test it. So, I actually only needed the first part. I didn't know I can use access the context.height with GlobalKey. Great answer.
  • siddhesh
    siddhesh over 5 years
    how to import schedular binder?
  • siddhesh
    siddhesh over 5 years
    i have tried import 'package:flutter/scheduler.dart.'; but i got error target uri doesn't exists @rémi-rousselet
  • RoyalGriffin
    RoyalGriffin over 4 years
    @rémi-rousselet how do I make this work when I have a widget behind the ListView whose height, I want to control according to the ListView's height?
  • aytunch
    aytunch over 4 years
    Remi, Thanks for the clever solution and great explanation. I have a question. What if we want to be able to know the Rect of any ListItem of a ListView.builder when they are pressed. Would it be an overkill to set GlobalKey listItemKey = GlobalKey(); for every listItem? Let's say i have +10000 items. Is there a clever way to manage this without performance/memory issues?
  • Michel Feinstein
    Michel Feinstein about 4 years
    Remi, I don't think your answer helps in my case, does it? stackoverflow.com/q/60161132/1227166
  • Dev Aggarwal
    Dev Aggarwal about 4 years
    Thanks for the pointer, check out my complete solution - stackoverflow.com/a/60868972/7061265
  • aytunch
    aytunch almost 4 years
    stickyBuilder does not use context in it. is there a typo?
  • ch271828n
    ch271828n over 3 years
    Sorry it throws the following exception: RenderBox.size accessed beyond the scope of resize, layout, or permitted parent access. How to solve that? Thanks!
  • Harsh Bhikadia
    Harsh Bhikadia over 3 years
    Awesome solution brother. make this as a pub package.
  • alireza daryani
    alireza daryani over 3 years
    Its very nice solution for many height and width issues, Thanks
  • user2233706
    user2233706 over 3 years
    This works, but it's difficult to use in some places. For example, in a PreferredSizeWidget, preferredSize is only called once, so you can't set the height in a easy way.
  • Dev Aggarwal
    Dev Aggarwal over 3 years
    Hey, I've updated the implementation to support cases where build() does not get called for rebuilds. Hope this is more correct.
  • Sasha Prokhorenko
    Sasha Prokhorenko over 3 years
    This solution doesn't work for SliverAppBar.
  • Renan Coelho
    Renan Coelho over 3 years
    _textKey.currentContext.size is enough
  • Omatt
    Omatt over 3 years
    Could you elaborate the "dynamic size" that causes the issue? Do you have a minimal repro that I can check?
  • Renan Coelho
    Renan Coelho over 3 years
    to get the widget width/height dynamically you need to call .findRenderObejct() and then .size. RenderBox box = widget.context.findRenderObject(); print(box.size);
  • Renan Coelho
    Renan Coelho over 3 years
    you may also pass a GlobalKey as a key of widget and then call _myKey.currentContext.findRenderObject()
  • mattorb
    mattorb over 3 years
    Hi! This appeared to work well initially, but I discovered one caveat: the size did not update on device orientation changes. I suspect it is because state for a Stateful Widget does not get reinitialized on device rotation.
  • Yadu
    Yadu over 3 years
    hi, flutter is so modular like lego, just wrap the above Widget with a OrientationBuilder which starts to respect any orientation, i mean of the device haha
  • Vandit Mehta
    Vandit Mehta about 3 years
    Crashing on iOS 14.5 beta
  • Vandit Mehta
    Vandit Mehta about 3 years
    crashing on iOS 14.5 beta
  • Anup Gupta
    Anup Gupta over 2 years
    Non-nullable instance field 'oldSize' must be initialized. on 'MeasureSizeRenderObject(this.onChange);'
  • narrei
    narrei over 2 years
    anyone else getting this error? The argument type 'Widget Function(BuildContext, Widget)' can't be assigned to the parameter type 'Widget Function(BuildContext, Widget?)'.dart(argument_type_not_assignable)
  • Admin
    Admin over 2 years
    Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
  • Randal Schwartz
    Randal Schwartz over 2 years
    Look at Boxy, which encapsulates this kind of strategy of getting the size of children before fully rendering it.
  • Ara Hussein
    Ara Hussein about 2 years
    amazing solution, one of the techniques I want it forever.