Reputation: 3675
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,
),
),
],
),
],
)));
}
Upvotes: 13
Views: 16540
Reputation: 1
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,
),
),
],
),
),
);
});
},
),
);
}
}
Upvotes: 0
Reputation: 95
I've created a package that can help with this very well: https://pub.dev/packages/intrinsic_size_builder
Greatly simplifies:
Checkout the examples, they specifically target the issue OP describes.
Upvotes: 0
Reputation: 615
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(),
)
],
),
),
);
}
}
Upvotes: 3
Reputation: 2132
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()),
],
),
),
);
}
}
Upvotes: 3
Reputation: 3429
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,
),
),
),
],
),
);
}
Upvotes: 0
Reputation: 414
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.
Upvotes: 4
Reputation: 277597
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.
Upvotes: 4