This tutorial explains how to get the size and position of a widget in Flutter.
Sometimes, you may need to know the size and position of a widget rendered in the screen programmatically. For example, if you need to control a child widget based on the parent's size and location. In Flutter, that information can be obtained from the RenderBox
of the respective widget. Below are the examples.
Using GlobalKey
The first method is using a GlobalKey
assigned to the widget. You can use the GlobalKey
to obtain the RenderBox
from which you can get the size and offset position information.
First, you need to assign a GlobalKey
to the widget.
final GlobalKey _widgetKey = GlobalKey();
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: const Text('Woolha.com Flutter Tutorial'),
backgroundColor: Colors.teal,
),
body: Stack(
children: [
Positioned(
left: 50,
top: 100,
child: Container(
key: _widgetKey,
width: 300,
height: 300,
color: Colors.teal,
),
),
],
),
);
}
If you've assigned the GlobalKey
to the widget, you can get the currentContext
property of the key and call the findRenderObject()
method. The result can be casted to RenderBox
.
You can get the size from the RenderBox
's size
property. There is a shorthand for getting the size from the context. You can get the size
property of the BuildContext
. It does the same thing and only returns a valid result if findRenderObject
returns a RenderBox
.
For the widget's offset position, call RenderBox
's localToGlobal
method whose return type is Offset
. The Offset
class has some properties. For getting the offset position in x and y axis, you can read the dx
and dy
properties respectively. The returned offset is the top left position of the widget. If you want to get the center of the widget, just add the size of the widget in the respective axis and divide the result with 2.
void _getWidgetInfo(_) {
final RenderBox renderBox = _widgetKey.currentContext?.findRenderObject() as RenderBox;
final Size size = renderBox.size; // or _widgetKey.currentContext?.size
print('Size: ${size.width}, ${size.height}');
final Offset offset = renderBox.localToGlobal(Offset.zero);
print('Offset: ${offset.dx}, ${offset.dy}');
print('Position: ${(offset.dx + size.width) / 2}, ${(offset.dy + size.height) / 2}');
}
You must be careful that you can only get the RenderBox
after the widget has been rendered. If not, the result of _widgetKey.currentContext
will be null
and you will get an error if trying to cast the result to RenderBox
. That's because the BuildContext
may not exist if the widget hasn't been rendered or not visible on the screen. In addition, you cannot get the RenderBox
during build. The solution is passing the above method as the callback of SchedulerBinding
's addPostFrameCallback
. A callback passed as addPostFrameCallback
will be called after the persistent frame callbacks (after the main rendering pipeline has been flushed). Therefore, it's possible to get the RenderBox
.
This method has another problem. It cannot handle the size changes during animation. So, if you need the size or offset information for each animation tick, you cannot use this method.
Using RenderProxyBox
Another alternative to get the size of a widget is by using RenderProxyBox
. It can be done by wrapping the widget as the child of another widget that extends SingleChildRenderObjectWidget
. Extending the class requires you to override the createRenderObject
method. That method requires you to return a RenderObject
and you can create your own RenderObject
by creating a class that extends RenderProxyBox
. You need to override the performLayout
method of the RenderProxyBox
to get the RenderBox
which can be used to get the size of the child.
With this method, you can get the size of a widget on every rebuild of the child and its descendants. Therefore, it overcomes the problem of the previous method in which you cannot get the widget size on each animation tick. However, this method is very expensive and should be avoided for performance reason.
In the example below, there is a callback function passed to the RenderProxyBox
. Inside the performLayout
widget, you need to get the child's size and call the callback if the current size is different than the previous size.
class WidgetSizeRenderObject extends RenderProxyBox {
final OnWidgetSizeChange onSizeChange;
Size? currentSize;
WidgetSizeRenderObject(this.onSizeChange);
@override
void performLayout() {
super.performLayout();
try {
Size? newSize = child?.size;
if (newSize != null && currentSize != newSize) {
currentSize = newSize;
WidgetsBinding.instance?.addPostFrameCallback((_) {
onSizeChange(newSize);
});
}
} catch (e) {
print(e);
}
}
}
class WidgetSizeOffsetWrapper extends SingleChildRenderObjectWidget {
final OnWidgetSizeChange onSizeChange;
const WidgetSizeOffsetWrapper({
Key? key,
required this.onSizeChange,
required Widget child,
}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return WidgetSizeRenderObject(onSizeChange);
}
}
Then, pass the widget to be measured as the child of the WidgetSizeOffsetWrapper
along with a callback function that will be invoked when the size changes.
WidgetSizeOffsetWrapper(
onSizeChange: (Size size) {
print('Size: ${size.width}, ${size.height}');
},
child: AnimatedContainer(
duration: const Duration(seconds: 3),
width: _size,
height: _size,
color: Colors.teal,
),
)
Full Code
Below is the full code that uses GlobalKey
.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Woolha.com Flutter Tutorial',
home: WidgetSizeAndPositionExample(),
);
}
}
class WidgetSizeAndPositionExample extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _WidgetSizeAndPositionExampleState();
}
}
class _WidgetSizeAndPositionExampleState extends State<WidgetSizeAndPositionExample> {
final
Key _widgetKey =
Key();
double _size = 300;
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback(_getWidgetInfo);
}
void _getWidgetInfo(_) {
final RenderBox renderBox = _widgetKey.currentContext?.findRenderObject() as RenderBox;
_widgetKey.currentContext?.size;
final Size size = renderBox.size;
print('Size: ${size.width}, ${size.height}');
final Offset offset = renderBox.localTo
(Offset.zero);
print('Offset: ${offset.dx}, ${offset.dy}');
print('Position: ${(offset.dx + size.width) / 2}, ${(offset.dy + size.height) / 2}');
}
@override
Widget build(BuildContext context) {
print('build');
return new Scaffold(
appBar: AppBar(
title: const Text('Woolha.com Flutter Tutorial'),
backgroundColor: Colors.teal,
),
body: Stack(
children: [
Positioned(
left: 50,
top: 100,
child: AnimatedContainer(
duration: const Duration(seconds: 3),
key: _widgetKey,
width: _size,
height: _size,
color: Colors.teal,
onEnd: () {
},
),
),
Positioned(
bottom: 0,
child: OutlinedButton(
onPressed: () {
setState(() {
_size = _size == 300 ? 100 : 300;
});
},
child: const Text('Change size'),
),
),
],
),
);
}
}
Below is the full code that uses RenderProxyBox
.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Woolha.com Flutter Tutorial',
home: WidgetSizeAndPositionExample(),
);
}
}
typedef void OnWidgetSizeChange(Size size);
class WidgetSizeRenderObject extends RenderProxyBox {
final OnWidgetSizeChange onSizeChange;
Size? currentSize;
WidgetSizeRenderObject(this.onSizeChange);
@override
void performLayout() {
super.performLayout();
try {
Size? newSize = child?.size;
if (newSize != null && currentSize != newSize) {
currentSize = newSize;
WidgetsBinding.instance?.addPostFrameCallback((_) {
onSizeChange(newSize);
});
}
} catch (e) {
print(e);
}
}
}
class WidgetSizeOffsetWrapper extends SingleChildRenderObjectWidget {
final OnWidgetSizeChange onSizeChange;
const WidgetSizeOffsetWrapper({
Key? key,
required this.onSizeChange,
required Widget child,
}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return WidgetSizeRenderObject(onSizeChange);
}
}
class WidgetSizeAndPositionExample extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _WidgetSizeAndPositionExampleState();
}
}
class _WidgetSizeAndPositionExampleState extends State<WidgetSizeAndPositionExample> {
double _size = 300;
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: const Text('Woolha.com Flutter Tutorial'),
backgroundColor: Colors.teal,
),
body: Stack(
children: [
Center(
// left: 50,
// top: 100,
child: WidgetSizeOffsetWrapper(
onSizeChange: (Size size) {
print('Size: ${size.width}, ${size.height}');
},
child: AnimatedContainer(
duration: const Duration(seconds: 3),
width: _size,
height: _size,
color: Colors.teal,
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: OutlinedButton(
onPressed: () {
setState(() {
_size = _size == 300 ? 100 : 300;
});
},
child: const Text('Change size'),
),
),
],
),
);
}
}
Summary
That's how to get the size and position of a widget rendered on the screen. Basically, you need to get the RenderBox
of the widget. Then, you can access the size
property to get the size of the widget and call localToGlobal
method to get the offset.