11

The SliverAppBar has an attribute bottom which has to have preferredSize.

Right now I have it returning constant value:

  ...
  new SliverAppBar(
    expandedHeight: _kFlexibleSpaceMaxHeight,
    flexibleSpace: new FlexibleSpaceBar(.....)
    ...                   
    bottom: new BottomBar(...), // has to have preferredSize
  ),
  ...

class BottomBar extends StatelessWidget implements PreferredSizeWidget {
    ...
    @override
      Size get preferredSize {
        return new Size.fromHeight(my_constant_height_value);
      }

    ...
    }

I want to put a Text inside this bottom Widget and I don't know how long the text inside it is going to be.

How can I achieve the dynamic height of the bottom widget ?

Is there a way to measure a widget's height before it is layed out ?

EDIT 25/04/2018

Eventually, I followed Thibault's instructions and ended up with this:

// 'as rendering' to avoid conflict with 'package:intl/intl.dart'
import 'package:flutter/rendering.dart' as rendering; 

...

// this is the function that returns the height of a Text widget
// given the text
double getHeight(String text, BuildContext context, bool isTitle) {
  var rp = rendering.RenderParagraph(
    new TextSpan(
        style: isTitle
            ? Theme.of(context).primaryTextTheme.title
            : Theme.of(context).primaryTextTheme.subhead,
        text: text,
        children: null,
        recognizer: null),

    // important as the user can have increased text on his device
    textScaleFactor: MediaQuery.of(context).textScaleFactor, 

    textDirection: rendering.TextDirection.ltr,
  );
  var horizontalPaddingSum = 20; // optional 
  var width = MediaQuery.of(context).size.width - horizontalPaddingSum;
  // if your Text widget has horizontal padding then you have to 
  // subtract it from available width to get the needed results
  var ret = rp.computeMinIntrinsicHeight(width);
  return ret;
}

...


  _kPreferredBBTextHeight =
      getHeight(mTitle ?? "", context, true);

  var verticalPaddingSum = 10;
  _kPreferredBBSubTextHeight = getHeight(mSubtitle ?? "", context,false) + verticalPaddingSum;

  _kPreferredBottomBarSize =
      _kPreferredBBTextHeight + _kPreferredBBSubTextHeight + 48;

  _kFlexibleSpaceMaxHeight =
      _kPreferredBottomBarSize + _kPreferredBottomBarSize + kToolbarHeight;

  _backgroudBottomPadding = _kPreferredBottomBarSize;

...
new CustomSliverAppBar(
                pinned: true,
                automaticallyImplyLeading: false,
                primary: true,
                expandedHeight: _kFlexibleSpaceMaxHeight,
                flexibleSpace: new FlexibleSpaceBar(
                  background: new Padding(
                      padding:
                          new EdgeInsets.only(bottom: _backgroudBottomPadding),
                      child: new Image(
                        image: new NetworkImage(mImageUrl),
                        fit: BoxFit.cover,
                      )),
                ),
                bottom: new BottomBar(
                  fixedHeight: _kPreferredBottomBarSize,
                ),
              ),

...

class BottomBar extends StatelessWidget implements PreferredSizeWidget {
  final double fixedHeight;

  BottomBar({this.fixedHeight});

  @override
  Size get preferredSize {
    return new Size.fromHeight(this.fixedHeight);
  }

  @override
  Widget build(BuildContext context) {
    // https://github.com/flutter/flutter/issues/3782
    return new Container(
        height: this.fixedHeight,
        child: new Material(
            color: Theme.of(context).primaryColor,
            child: new Column(
              children: <Widget>[
                new Row(
                  children: <Widget>[
                    new IconButton(
                      icon: new Icon(Icons.arrow_back, color: Colors.white),
                      onPressed: () {
                        Navigator.of(context).pop();
                      },
                    ),
                    new Expanded(
                      child: new Container(),
                    ),
                    new IconButton(
                      icon: new Icon(Icons.share, color: Colors.white),
                      onPressed: () {
                        print("share pressed");
                      },
                    )
                  ],
                ),
                new Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    new Padding(
                        padding: new EdgeInsets.only(left: 10.0, right: 10.0),
                        child: new Container(
                          child: new Container(
                            alignment: Alignment.centerLeft,
                            child: new Text(
                              mTitle ?? "",
                              style: Theme.of(context).primaryTextTheme.title,
                            ),
                          ),
                        )),
                    new Container(
                      padding: new EdgeInsets.only(
                          left: 10.0, right: 10.0, top: 5.0, bottom: 5.0),
                      alignment: Alignment.centerLeft,
                      child: new Text(
                        mSubtitle ?? "",
                        style: Theme.of(context).primaryTextTheme.subhead,
                      ),
                    ),
                  ],
                ),
              ],
            )));
  }
Jerzy Kiler
  • 2,795
  • 4
  • 18
  • 12

5 Answers5

4

The whole point of PreferredSizeWidget is that no, you can't dynamically size this widget.

The reason behind it is Scaffold using that preferred size to do some computation. Which would be impossible if the appbar size was unknown until rendered.

You'll have to rethink your UI accordingly.

Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • 2
    It would be really useful in this situation if I could determine the Text widget’s height given width and Text’s style – Jerzy Kiler Mar 14 '18 at 01:57
  • 1
    @JerzyKiler use "textScaleFactor: 1" in the text widget to make its font size independent of the device font size – Omar Fayad May 28 '23 at 13:14
3

Is there a way to measure a widget's height before it is layed out ?

In general you can use LayoutBuilder when building the UI in the build() method, but it probably won't help you in this case.

Here, you could try RenderParagraph to render your text and measure it before building the Scaffold. You can use the screen width as the width constraint, layout the RenderParagraph, retrieve the height, and use that as the preferred size.

That said, you won't be able to change the preferred height later on if your text changes during the lifetime of your Scaffold.

Thibault
  • 375
  • 2
  • 4
  • 15
3

You can use this widget as a workaround for this issue.

class DynamicSliverAppBar extends StatefulWidget {
  final Widget child;
  final double maxHeight;

  DynamicSliverAppBar({
    @required this.child,
    @required this.maxHeight,
    Key key,
  }) : super(key: key);

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

class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
  final GlobalKey _childKey = GlobalKey();
  bool isHeightCalculated = false;
  double height;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (!isHeightCalculated) {
        isHeightCalculated = true;
        setState(() {
          height = (_childKey.currentContext.findRenderObject() as RenderBox)
              .size
              .height;
        });
      }
    });

    return SliverAppBar(
      expandedHeight: isHeightCalculated ? height : widget.maxHeight,
      flexibleSpace: FlexibleSpaceBar(
        background: Column(
          children: [
            Container(
              key: _childKey,
              child: widget.child,
            ),
            Expanded(child: SizedBox.shrink()),
          ],
        ),
      ),
    );
  }
}
mahdi shahbazi
  • 1,882
  • 10
  • 19
  • It works with static widgets. Thanks for your answer, I post a new approach which is for dynamically sized widgets. – Ataberk Jun 18 '22 at 21:23
3

My widget's size wasn't static so I need another workaround. I improved mahdi shahbazi's answer with help of SizeChangedLayoutNotification. Thanks for hime, the SizedBox.shrink approach was clever.

class DynamicSliverAppBar extends StatefulWidget {
  final Widget child;
  final double maxHeight;

  const DynamicSliverAppBar({
    required this.child,
    required this.maxHeight,
    Key? key,
  }) : super(key: key);

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

class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
  final GlobalKey _childKey = GlobalKey();
  bool isHeightCalculated = false;
  double? height;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (!isHeightCalculated) {
        isHeightCalculated = true;
        setState(() {
          height = (_childKey.currentContext?.findRenderObject() as RenderBox)
              .size
              .height;
        });
      }
    });

    return SliverAppBar(
      expandedHeight: isHeightCalculated ? height : widget.maxHeight,
      flexibleSpace: FlexibleSpaceBar(
        background: Column(
          children: [
            NotificationListener<SizeChangedLayoutNotification>(
              onNotification: (notification) {
                WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
                  isHeightCalculated = true;
                  setState(() {
                    height = (_childKey.currentContext?.findRenderObject()
                            as RenderBox)
                        .size
                        .height;
                  });
                });
                return false;
              },
              child: SizeChangedLayoutNotifier(
                child: Container(
                  key: _childKey,
                  child: widget.child,
                ),
              ),
            ),
            const Expanded(
              child: SizedBox.shrink(),
            )
          ],
        ),
      ),
    );
  }
}
Ataberk
  • 557
  • 1
  • 6
  • 26
  • 1
    You can use `Spacer()` – Ng Lok Chun Jun 29 '22 at 04:21
  • 1
    This solution is pretty good but ran into performance issues when tested interactively. I tested using [device_preview](https://pub.dev/packages/device_preview) and I tested by varying the screen size quite extremely. – Septian Dika Dec 30 '22 at 16:45
0

I used the below code for this issue. the toolbarHeight is the text height (is dynamic).

note: this page render twice.

  var toolbarHeight;
  BuildContext? renderBoxContext;

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

    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
      var renderBox = renderBoxContext?.findRenderObject() as RenderBox;
      toolbarHeight = renderBox.size.height;
      setState(() {});
    });
  }

@override
  Widget build(BuildContext context) {
    
    return Material(
          child: getBody(context),
        );
  }


getBody(BuildContext context) {
  var mediaQuery = MediaQuery.of(context).size;
  state.toolbarHeight ??= mediaQuery.height;

  return SizedBox(
    width: mediaQuery.width,
    height: mediaQuery.height,
    child: CustomScrollView(
      slivers: <Widget>[

        SliverAppBar(
          pinned: false,
          floating: true,
          snap: false,
          backwardsCompatibility: true,
          centerTitle: true,
          bottom: PreferredSize(
            preferredSize: Size(mediaQuery.width, state.toolbarHeight),
            child: Builder(
              builder: (ctx){
                state.renderBoxContext = ctx;

                return Align(
                  alignment: Alignment.topLeft,
                  child: ColoredBox(
                    color: Colors.green,
                    child: Text('line1\nline2\nline3'),
                  ),
                );
              },
            ),
          ),
          flexibleSpace: FlexibleSpaceBar(
            title: Text('FlexibleSpaceBar'),
            centerTitle: true,
            collapseMode: CollapseMode.pin,
          ),
        ),

        SliverPadding(
          padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
          sliver: SliverFixedExtentList(
            itemExtent: 110,
            delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return Text('   item  $index');
                  },
              childCount: 10,
            ),
          ),
        ),
      ],
    ),
  );
}
Ali Bagheri
  • 3,068
  • 27
  • 28