I'm trying to mimic iOS contact form app bar.
expanded
collapsed
Here is where I get so far
Main Screen
class CompanyScreen extends StatefulWidget {
@override
_CompanyScreenState createState() => _CompanyScreenState();
}
class _CompanyScreenState extends State<CompanyScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: SafeAreaPersistentHeaderDelegate(
expandedHeight: 200,
flexibleSpace:
SafeArea(child: Image.asset('assets/images/user.png'))),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
}
}
SliverHeader
class SafeAreaPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget title;
final Widget flexibleSpace;
final double expandedHeight;
SafeAreaPersistentHeaderDelegate(
{this.title, this.flexibleSpace, this.expandedHeight});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: 1,
child: AppBar(
actions: <Widget>[
Container(
height: 60,
child: FlatButton(
child: Text('Done'),
),
)
],
backgroundColor: Colors.blue,
automaticallyImplyLeading: false,
title: title,
flexibleSpace: (title == null && flexibleSpace != null)
? Semantics(child: flexibleSpace, header: true)
: flexibleSpace,
centerTitle: true,
toolbarOpacity: 1,
bottomOpacity: 1.0),
);
return appBar;
}
@override
double get maxExtent => expandedHeight;
@override
double get minExtent => 80;
@override
bool shouldRebuild(SafeAreaPersistentHeaderDelegate old) {
if (old.flexibleSpace != flexibleSpace) {
return true;
}
return false;
}
}
UPDATE: It all works but I have a problem add the text under the image (Add Photo) and make that text disappear when collapsed. With this solution, if I wrap the image into a column then image expands overflow and doesn't scale.
Requirements:
AppBar and the flex area must be in safe area
Widget with image must have text at the bottom which can be changed dynamically (Add image or Change image) and it must be clickable
The text under the image area must disappear when flex area is collapsed with some transition
Ability to add title in app bar lined up with action buttons
When title in app bar is provided then flex area should scale bellow the title, if not flex area should scale into the title area as on the above image
Any help with this greatly appreciated
I gave it a try.. I'm not an expert on slivers so this solution might not be perfect. I have taken your code as starting point. The column seems to deactivate all scaling so I scaled all manually.
here is your app bar
UPDATE I have tweaked it a little so it feels more like iOS app bar plus I've added extra feature
import 'dart:math';
import 'package:flutter/material.dart';
double _defaultTextHeight = 14;
double _defaultTextPadding = 5;
double _defaultAppBarHeight = 60;
double _defaultMinAppBarHeight = 40;
double _unknownTextValue = 1;
class AppBarSliverHeader extends SliverPersistentHeaderDelegate {
final String title;
final double expandedHeight;
final double safeAreaPadding;
final Widget flexibleImage;
final double flexibleSize;
final String flexibleTitle;
final double flexiblePadding;
final bool flexToTop;
final Function onTap;
final Widget rightButton;
final Widget leftButton;
AppBarSliverHeader(
{this.title,
this.onTap,
this.flexibleImage,
@required this.expandedHeight,
@required this.safeAreaPadding,
this.flexibleTitle = '',
this.flexToTop = false,
this.leftButton,
this.rightButton,
this.flexibleSize = 30,
this.flexiblePadding = 4});
double _textPadding(double shrinkOffset) {
return _defaultTextPadding * _scaleFactor(shrinkOffset);
}
double _widgetPadding(double shrinkOffset) {
double offset;
if (title == null) {
offset = _defaultMinAppBarHeight * _scaleFactor(shrinkOffset);
} else {
if (flexToTop) {
offset = _defaultAppBarHeight * _scaleFactor(shrinkOffset);
} else {
offset = (_defaultAppBarHeight - _defaultMinAppBarHeight) *
_scaleFactor(shrinkOffset) +
_defaultMinAppBarHeight;
}
}
return offset;
}
double _topOffset(double shrinkOffset) {
double offset;
if (title == null) {
offset = safeAreaPadding +
(_defaultMinAppBarHeight * _scaleFactor(shrinkOffset));
} else {
if (flexToTop) {
offset = safeAreaPadding +
(_defaultAppBarHeight * _scaleFactor(shrinkOffset));
} else {
offset = safeAreaPadding +
((_defaultAppBarHeight - _defaultMinAppBarHeight) *
_scaleFactor(shrinkOffset)) +
_defaultMinAppBarHeight;
}
}
return offset;
}
double _calculateWidgetHeight(double shrinkOffset) {
double actualTextHeight = _scaleFactor(shrinkOffset) * _defaultTextHeight +
_textPadding(shrinkOffset) +
_unknownTextValue;
final padding = title == null
? (2 * flexiblePadding)
: flexToTop ? (2 * flexiblePadding) : flexiblePadding;
final trueMinExtent = minExtent - _topOffset(shrinkOffset);
final trueMaxExtent = maxExtent - _topOffset(shrinkOffset);
double minWidgetSize =
trueMinExtent - padding;
double widgetHeight =
((trueMaxExtent - actualTextHeight) - shrinkOffset) - padding;
return widgetHeight >= minWidgetSize ? widgetHeight : minWidgetSize;
}
double _scaleFactor(double shrinkOffset) {
final ratio = (maxExtent - minExtent) / 100;
double percentageHeight = shrinkOffset / ratio;
double limitedPercentageHeight =
percentageHeight >= 100 ? 100 : percentageHeight;
return 1 - (limitedPercentageHeight / 100);
}
Widget _builtContent(BuildContext context, double shrinkOffset) {
_topOffset(shrinkOffset);
return SafeArea(
bottom: false,
child: Semantics(
child: Padding(
padding: title == null
? EdgeInsets.symmetric(vertical: flexiblePadding)
: flexToTop
? EdgeInsets.symmetric(vertical: flexiblePadding)
: EdgeInsets.only(bottom: flexiblePadding),
child: GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LimitedBox(
maxWidth: _calculateWidgetHeight(shrinkOffset),
maxHeight: _calculateWidgetHeight(shrinkOffset),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(
_calculateWidgetHeight(shrinkOffset))),
color: Colors.white),
child: ClipRRect(
borderRadius: BorderRadius.circular(
_calculateWidgetHeight(shrinkOffset)),
child: flexibleImage,
),
)),
Padding(
padding: EdgeInsets.only(top: _textPadding(shrinkOffset)),
child: Text(
flexibleTitle,
textScaleFactor: _scaleFactor(shrinkOffset),
style: TextStyle(
fontSize: _defaultTextHeight,
color: Colors.white
.withOpacity(_scaleFactor(shrinkOffset)), height: 1),
),
)
],
),
),
),
button: true,
),
);
}
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: 1,
child: AppBar(
actions: <Widget>[rightButton == null ? Container() : rightButton],
leading: leftButton == null ? Container() : leftButton,
backgroundColor: Colors.blue,
automaticallyImplyLeading: false,
title: title != null
? Text(
title,
style: TextStyle(
color: flexToTop
? Colors.white.withOpacity(_scaleFactor(shrinkOffset))
: Colors.white),
)
: null,
flexibleSpace: Padding(
padding: EdgeInsets.only(top: _widgetPadding(shrinkOffset)),
child: _builtContent(context, shrinkOffset),
),
centerTitle: true,
toolbarOpacity: 1,
bottomOpacity: 1.0),
);
return appBar;
}
@override
double get maxExtent => expandedHeight + safeAreaPadding;
@override
double get minExtent => title == null
? _defaultAppBarHeight + safeAreaPadding
: flexToTop
? _defaultAppBarHeight + safeAreaPadding
: _defaultAppBarHeight + safeAreaPadding + flexibleSize;
@override
bool shouldRebuild(AppBarSliverHeader old) {
if (old.flexibleImage != flexibleImage) {
return true;
}
return false;
}
}
and here is usage
Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: AppBarSliverHeader(
expandedHeight: 250,
safeAreaPadding: MediaQuery.of(context).padding.top,
title: 'New Contact',
flexibleImage: Image.asset('assets/images/avatar.png'),
flexibleTitle: 'Add Image',
flexiblePadding: 6,
flexibleSize: 50,
flexToTop: true,
onTap: () {
print('hello');
},
leftButton: IconButton(
icon: Text('Cancel'),
iconSize: 60,
padding: EdgeInsets.zero,
onPressed: () {},
),
rightButton: IconButton(
icon: Text('Done'),
iconSize: 60,
padding: EdgeInsets.zero,
onPressed: () {},
)),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
There are some things which took me by surprise as well. First is text size. It seems like text size is not an actual text size so I've added _unknownTextValue
there for compensation. Also even if text size is set to 0 then the Text widget has still 1px size so I've compensated that in commented code. Another thing is I wanted to use CircularAvatar
for the image but apparently the CircularAvatar
widget has built in animation when changing the size which interfere with app bar animation so I've built custom avatar.
UPDATE: To make actual text height same as font size, I have added height property 1 to TextStyle. It seems to work however there is still occasional overflow on the textfield of up to 1px so I've kept _unknownTextValue at 1px
As I said I'm not sliver expert so there might be a better solutions out there so I would suggest you to wait for other answers
NOTE: I only tested it on 2 iOS devices so you should test further to use it
With Title
Without Title
With Title and flexToTop activated