Is there a way to achieve a dynamic height in bottom widget of SliverAppBar
Asked Answered
V

7

13

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,
                      ),
                    ),
                  ],
                ),
              ],
            )));
  }
Voelker answered 14/3, 2018 at 0:33 Comment(0)
S
4

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.

Synchrocyclotron answered 13/4, 2018 at 8:59 Comment(0)
P
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.

Primordium answered 14/3, 2018 at 0:42 Comment(2)
It would be really useful in this situation if I could determine the Text widget’s height given width and Text’s styleVoelker
@JerzyKiler use "textScaleFactor: 1" in the text widget to make its font size independent of the device font sizeRanger
S
4

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.

Synchrocyclotron answered 13/4, 2018 at 8:59 Comment(0)
B
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()),
          ],
        ),
      ),
    );
  }
}
Borries answered 15/12, 2021 at 12:29 Comment(1)
It works with static widgets. Thanks for your answer, I post a new approach which is for dynamically sized widgets.Anandrous
A
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(),
            )
          ],
        ),
      ),
    );
  }
}
Anandrous answered 18/6, 2022 at 21:21 Comment(2)
You can use Spacer()Blakeslee
This solution is pretty good but ran into performance issues when tested interactively. I tested using device_preview and I tested by varying the screen size quite extremely.Yearling
P
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,
            ),
          ),
        ),
      ],
    ),
  );
}
Pinta answered 30/7, 2021 at 14:2 Comment(0)
W
0

I've created a package that can help with this very well: https://pub.dev/packages/intrinsic_size_builder

Greatly simplifies:

  • SliverAppBars with dynamically sized flexibleSpace
  • SliverAppBars with dynamically sized bottom
  • Including when the flexibleSpace has a widget which can resize itself, like an Image.network, that has a different size when loading

Checkout the examples, they specifically target the issue OP describes.

W answered 1/2 at 19:8 Comment(0)
S
0

Here's my example using Martin Sellergren answer to calc size of SliverHader

@RoutePage()
class OrderAddressDetailScreen extends StatelessWidget {
  const OrderAddressDetailScreen({super.key});

  final double _toolBarDefaultHeight = 60;

  @override
  Widget build(BuildContext context) {
    return BlocProvider<OrderAddressDetailBloc>(
      create: (context) => locator<OrderAddressDetailBloc>(),
      child: BlocBuilder<OrderAddressDetailBloc, OrderAddressDetailState>(
        builder: (context, state) {
          if (state.pageTitle.isEmpty) {
            return Container(
              color: Theme.of(context).colorScheme.background,
              child: const CircularProgressIndicator(),
            );
          }
          return **IntrinsicSizeBuilder**(
              firstFrameWidget: Container(
                color: Theme.of(context).colorScheme.background,
              ),
              subject: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Flexible(
                    child: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 24),
                      child: Text(
                        style: AppBarTheme.of(context).titleTextStyle,
                        state.pageTitle,
                      ),
                    ),
                  ),
                ],
              ),
              builder: (context, titleSize, subject) {
                return Scaffold(
                  body: Padding(
                    padding: const EdgeInsets.only(bottom: 72),
                    child: CustomScrollView(
                      slivers: [
                        SliverHeader(
                          title: state.pageTitle,
                          expandedHeight: titleSize.height + _toolBarDefaultHeight,
                        ),
                        SliverToBoxAdapter(
                          child: Padding(
                            padding: const EdgeInsets.all(24),
                            child: _Content(state),
                          ),
                        ),
                        const SliverToBoxAdapter(
                          child: SizedBox(
                            height: 24,
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              });
        },
      ),
    );
  }
}
Sprang answered 15/4 at 15:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.