Search code examples
flutterdartmobileresize

Flutter: what is the best way to get the size of an unrendered widget before rendering?


I am trying to build a custom AppBar with a progress bar underneath it. I use different colored an animated Containers with rounded borders. The progress bar should have a height of 6 px. And it should look like this.

mockup

But I encounter the following problem: The black Container mostly overlaps the progress bar on different screen sizes as the SafeArea changes and the IconButton (back arrow) changes size.

So I would like to get a better way to find out how much size the black Container with the Row will need so I can set the progress bar Container's height accordingly.

My solution so far is setting up a WidgetsBinding.instance.addPostFrameCallback where I calculate the size of the row using a GlobalKey.

This is the code I use in my custom flutter hook to get the size with the GlobalKey:

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

PositionAndSize useGetSize(GlobalKey k) {
  return use(_SizeHook(k));
}

class PositionAndSize {
  PositionAndSize(this.position, this.size);

  Offset position;
  Size size;
}

class _SizeHook extends Hook<PositionAndSize> {
  const _SizeHook(this.k);

  final GlobalKey k;

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


class _SizeHookState extends HookState<PositionAndSize, _SizeHook> {
  late PositionAndSize positionAndSize =
      PositionAndSize(Offset.zero, const Size(0, 0));

  void calculateSizeAndPosition() {
    final RenderBox renderBox =
        hook.k.currentContext!.findRenderObject() as RenderBox;
    var size = renderBox.size;
    var offset = renderBox.localToGlobal(Offset.zero);
    positionAndSize = PositionAndSize(offset, size);
  }

  @override
  void initHook() {
    super.initHook();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      calculateSizeAndPosition();
    });
  }

  @override
  PositionAndSize build(BuildContext context) {
    return positionAndSize;
  }

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

Here is the code of my CustomAppBar:

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spendtrend/shared/models/size_hook.dart';

class ProgressHeader extends HookWidget implements PreferredSizeWidget {
  ProgressHeader(
      {super.key, required this.title, required this.begin, required this.end})
      : preferredSize = const Size.fromHeight(64),
        rowSizeKey = GlobalKey();

  final String title;
  final double begin;
  final double end;
  final GlobalKey rowSizeKey;

  @override
  final Size preferredSize;

  @override
  Widget build(BuildContext context) {
    var rowSize = useGetSize(rowSizeKey);
    double headerSize = MediaQuery.of(context).padding.top +
        rowSize.size.height +
        8 + // padding bottom
        6; // size of progress bar
    return Container(
      constraints: BoxConstraints(maxHeight: headerSize),
      decoration: BoxDecoration(
        color: const Color.fromRGBO(170, 217, 149, 1),
        borderRadius: const BorderRadius.vertical(
          bottom: Radius.circular(20),
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(.16),
            blurRadius: 4,
            offset: const Offset(0, 3),
          ),
          BoxShadow(
            color: Colors.black.withOpacity(.16),
            blurRadius: 6,
            offset: const Offset(0, 6),
          )
        ],
      ),
      child: Stack(
        children: [
          // active progress bar
          Container(
            decoration: const BoxDecoration(
              color: Color.fromRGBO(127, 181, 103, 1),
              borderRadius: BorderRadius.only(
                bottomLeft: Radius.circular(20),
              ),
            ),
          ),

          Container(
            decoration: const BoxDecoration(
                color: Colors.black,
                borderRadius:
                    BorderRadius.vertical(bottom: Radius.circular(25))),
            child: Padding(
              padding: const EdgeInsets.only(
                left: 16.0,
                right: 16.0,
                bottom: 8,
              ),
              child: SafeArea(
                bottom: false,
                child: Row(
                  key: rowSizeKey,
                  children: [
                    IconButton(
                        iconSize: 18,
                        icon: const Icon(Icons.arrow_back, color: Colors.white),
                        onPressed: () {
                          Navigator.pop(context);
                        }),
                    const SizedBox(
                      width: 16,
                    ),
                    Text(
                      title,
                      style: Theme.of(context)
                          .textTheme
                          .bodyLarge!
                          .copyWith(color: Colors.white),
                    ),
                  ],
                ),
              ),
            ),
          )
        ],
      ),
    );
  }
}

Is there a better way to achieve this or am I missing something when using Flutter? I am still quite new to this framework.


Solution

  • I took a deeper look into your question and I dont think you have to do anything complicated like calculate height before rendering

    How does my solution work out for you? https://dartpad.dev/?id=efb667ce370926d82d5ec9ef245a696e

    class ProgressHeader extends StatelessWidget implements PreferredSizeWidget {
      ProgressHeader({
        super.key,
        required this.title,
      })  : preferredSize = const Size.fromHeight(64),
            rowSizeKey = GlobalKey();
    
      final String title;
      final GlobalKey rowSizeKey;
    
      @override
      final Size preferredSize;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          clipBehavior: Clip.hardEdge,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.vertical(
              bottom: Radius.circular(26),
            ),
            boxShadow: [
              BoxShadow(
                color: Colors.black.withOpacity(.16),
                blurRadius: 4,
                offset: const Offset(0, 3),
              ),
              BoxShadow(
                color: Colors.black.withOpacity(.16),
                blurRadius: 6,
                offset: const Offset(0, 6),
              )
            ],
          ),
          child: Stack(
            children: [
              LinearProgressIndicator(
                backgroundColor: const Color.fromRGBO(170, 217, 149, 1),
                color: Color.fromRGBO(127, 181, 103, 1),
                minHeight: preferredSize.height,
              ),
              Positioned.fill(
                bottom: 6,
                child: AppBar(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.vertical(
                      bottom: Radius.circular(30),
                    ),
                  ),
                  backgroundColor: Colors.black,
                  foregroundColor: Colors.white,
                  title: Text(title),
                ),
              )
            ],
          ),
        );
      }
    }