Flutter - Get WIdget Size and Position

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.