Occasionally in Flutter, you will need to get the size and/or position of a widget. Some common use cases for this are:
- You need to measure something and make some decision based on that. For example, you might switch from an expanded column, to a scrolling column, at a certain height threshold.
- You may want enable or disable a scrollbar based on the height of some content
- The parent needs to know your position. For example if when you tap a menu button, the parent wants to animate a dot to that position, we need the button to be able to report its position somehow.
Before digging into these tricks, we should mention that the recommended way for getting widget size, is the LayoutBuilder widget. This is a great widget and works well, but crucially it measures the parents size, not its own size so its behavior can be a little tricky to grasp and is not always exactly what you need.
If you just want to get the size directly, read on!
Trick #1: Widget Size
In order to get the size of a Widget, you can use the RenderBox.size property:
RenderBox rb = context.findRenderObject() as RenderBox;
return rb?.size ?? Size.zero;
That’s it! Note that because of how Flutter does layout, size will sometimes be zero, and then will size subsequently in a 2nd layout pass. This always happens in the same frame, so it’s usually not a problem, but just be prepared to handle/ignore 0 here.
Trick #2: Widget Size (in a bit)
Occasionally you need to get the size as it will be in the next frame. For example you may be in a button handler, do some action, and you want to know the new size of the view. To do this you can you can wait one frame, and then check. To do that, you must assign your widget a GlobalKey, and then use the handy Future.microtask
method:
Future.microtask(() {
RenderBox rb = key.currentContext.findRenderObject() as RenderBox;
if (rb?.size != null) {
callback(size);
}
});
This code will let you pass it a context, and one frame later, will give you a callback with the widget size. You don’t need this often, but when you do it can really save some headaches (or at least let you ship, until you figure out what you are doing wrong!). This will usually work without the key (just pass context directly) but we’ve seen cases where a context can get invalidated between frames, so a global key is the most bullet-proof approach.
Also, worth noting that Future.microtask
can be used to fire any delayed action and is a very handy little tool in its own right.
Trick #3: Position
The final trick is to get the position of the Widget. To do this, you need its context
and then use RenderBox.localToGlobal
:
RenderBox rb = context.findRenderObject() as RenderBox;
return rb?.localToGlobal(offset ?? Offset.zero);
That will give you a position, relative to the top left corner of the screen plus whatever Offset
you provide. You can use this combined with the size of the widget, to determine its center point.
RenderBox rb = context.findRenderObject() as RenderBox;
if(rb != null && size rb.size != Size.zero){
Offset centerPtInWidgetSpace = Offset(size.width/2, size.height/2);
Offset centerPtInScreenSpace = rb.localToGlobal(centerPtInLocal);
print("Our global center is: $centerPtInScreenSpace);
}
If you are interested, the RenderBox class itself contains a bunch of interesting functions, and it’s probably worthwhile to spend some time with the docs.
Putting it all together…
For our projects we like to wrap this up in a dedicated class called ContextUtils.dart
:
import 'package:flutter/material.dart';
class ContextUtils {
// Takes a key, and in 1 frame, returns the size of the context attached to the key
static void getFutureSizeFromGlobalKey(GlobalKey key, Function(Size size) callback) {
Future.microtask(() {
Size size = getSizeFromContext(key.currentContext);
if (size != null) {
callback(size);
}
});
}
// Shortcut to get the renderBox size from a context
static Size getSizeFromContext(BuildContext context) {
RenderBox rb = context.findRenderObject() as RenderBox;
return rb?.size ?? Size.zero;
}
// Shortcut to get the global position of a context
static Offset getOffsetFromContext(BuildContext context, [Offset offset = null]) {
RenderBox rb = context.findRenderObject() as RenderBox;
return rb?.localToGlobal(offset ?? Offset.zero);
}
}
We hope you find these Tricks (hacks?) useful in your Flutter projects going forward!
Need help building something cool in Flutter? We’d love to chat.
To know the size of a widget, if i wrap its child in a layoutBuilder and get the size from this layoutBuilder. Won’t this size be the widget’s size i am interested in? Just a thought crossed my mind. Felt logic, might delete later.
I have a layout (think header, nav, footer content), where I need to take a widget (nav) and position its top about -50 from where it would layout if I was using Column/Row to do it. In your head, you can think of the nav as overlapping about half the header. The nav also can shrink/grow in height based on items expanding contracting. If it grows larger than the content, the footer should push down to accommodate the taller of the nav or content areas.
This is pretty standard without the overlap involved. A column with a header, a row and a footer. The row contains my nav and content.
Once my “overlap” requirement comes in to play, normal layout won’t cut it. I can’t use Stack because positioned elements can’t be placed relative to each other. If I keep all elements but nav unpositioned, it comes close, but the expansion of nav won’t affect the position of the footer. It just slides under the footer.
So, now I am at the CustomMultiChildLayout phase, and the layout is ok, but now it has to respond to animation (nav expansion is animated to shrink/grow). More work to do there.
I’m wondering if I missed something simple along the way in RenderBox or some approach that let’s me relatively position an element from where it intended to be rendered. (CSS makes this very simple, position: relative; top: -50px)
`Transform.translate(offset: Offset(0, -50), child…)` should do the job as long as you don’t have a hit region there.