How do I add widgets to the Sliver App Bar area? - flutter

I would like to add widgets in the Sliver AppBar area and make it feel more natural. The AppBar gives the liberty to add images but does it have any functionalities to add up more widgets into it?
I am focusing on how to use those CircleAvatar and Text widgets on the sliver appbar.

You won't be able to do that with the SliverAppBar :/.
What you should do is using SliverPersistentHeader with SliverPersistentHeaderDelegate.
You'll have to write a little bit more code but no too much :).
Example:
slivers = [
SliverPersistentHeader(
pinned: True,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 250.
),
),
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Column(
children: ...
),
)
]),
),
];
...
class _SliverAppBarDelegate extends x {
_SliverAppBarDelegate({
#required this.minHeight,
#required this.maxHeight,
#required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent)
{
return new SizedBox.expand(
child: LayoutBuilder(
builder: (_, constraints) {
DO WHAT YOU WANT HERE !
}
)
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}

There is a sample using SliverPersistentHeader with SliverPersistentHeaderDelegate.
In this sample when you scroll up the header will resized and button dissapear
slivers: <Widget>[
makeHeader(),
// You can put more slivers here
],
This is the method makeHeader:
SliverPersistentHeader makeHeader() {
return SliverPersistentHeader(
pinned: pinned,
floating: true,
delegate: _SliverAppBarDelegate(
minHeight: 60.0,
maxHeight: 200.0,
),
);
}
And the class _SliverAppBarDelegate:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double minHeight;
final double maxHeight;
_SliverAppBarDelegate(
{#required this.minHeight,
#required this.maxHeight,
this.hideButtonWhenExpanded = true});
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final appBarSize = maxHeight - shrinkOffset;
final proportion = 2 - (maxHeight / appBarSize);
final photoToButton = 160 * proportion;
final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
return new SizedBox.expand(
child: Container(
color: Colors.white,
child: Stack(
alignment: Alignment.topCenter,
children: <Widget>[
Positioned(
top: 10.0,
child: CircleAvatar(
minRadius: 20.0,
maxRadius: 75.0 * proportion > 20 ? 75.0 * proportion : 20.0,
backgroundImage: NetworkImage(
'https://randomuser.me/api/portraits/men/75.jpg'),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: photoToButton,
child: Opacity(
opacity: percent,
child: FlatButton(
onPressed: () {},
child: Text(
'Add Contact',
style: TextStyle(
color: Colors.blue, fontSize: 14.0 * proportion),
),
),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: appBarSize - 1.0 > 59.0 ? appBarSize - 1 : 59.0,
child: const Divider(
// color: Colors.grey,
height: 1,
thickness: 0.5,
),
)
],
),
),
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight !=
oldDelegate
.minHeight;
}
}

Related

Flutter sliver persistent

I have the code below, which I shortened as much as possible to make it easier to deal with. I want to scroll down to show the default appBar, not the background. I did some solutions, but it didn't work. Switch between them with a smooth motion.
I want to use the same existing code because I built on it.
I have attached an illustration of the problem
The main code:
import 'package:flutter/material.dart';
import 'home_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
HomePage code:
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
const SliverPersistentHeader(pinned: true, delegate: SliverHeaderDelegateComponent(expandedHeight: 300)),
SliverList(
delegate: SliverChildListDelegate(
[
Container(
height: 1000,
color: Colors.blue.withOpacity(0.5),
child: const Center(child: Text('Body')),
)
],
),
),
],
),
);
}
}
The SliverHeaderDelegateComponent code :
class SliverHeaderDelegateComponent extends SliverPersistentHeaderDelegate {
final double expandedHeight;
const SliverHeaderDelegateComponent({required this.expandedHeight});
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final appBarSize = expandedHeight - shrinkOffset;
final proportion = 2 - (expandedHeight / appBarSize);
final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) => SizedBox(
height: expandedHeight + expandedHeight / 2,
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
height: 500,
decoration: const BoxDecoration(
color: Colors.black,
image: DecorationImage(
image: NetworkImage(
'https://www.digitalartsonline.co.uk/cmsdata/slideshow/3662115/baby-driver-rory-hi-res.jpg'),
fit: BoxFit.cover,
),
),
),
PositionedDirectional(
start: 0.0,
end: 0.0,
top: appBarSize > 0 ? appBarSize : 0,
bottom: -100,
child: Opacity(
opacity: percent,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 30 * percent),
child: const Card(
elevation: 20.0,
child: Center(
child: Text("Widget"),
),
),
),
),
),
],
),
),
);
}
#override
double get maxExtent => expandedHeight + expandedHeight / 2;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
Here is the solution using SliverHeaderDelegateComponent as you requested.
In this example, the AppBar is shown when collapsed, but you can uncomment the commented part if you want to show it on expand. (UPDATE: improved fading as requested in the comment section)
class SliverHeaderDelegateComponent extends SliverPersistentHeaderDelegate {
final double expandedHeight;
const SliverHeaderDelegateComponent({required this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final deadline = (expandedHeight + minExtent);
double percent = shrinkOffset > deadline ? 1 : shrinkOffset / deadline;
final appBarSize = expandedHeight - shrinkOffset;
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) => SizedBox(
height: expandedHeight + expandedHeight / 2,
child: Stack(
clipBehavior: Clip.none,
children: [
// shrinkOffset == 0 // if you want to show it on expand
shrinkOffset > expandedHeight + minExtent // show it on collapse
? AppBar(title: Text('App Bar'))
: Container(
height: 500,
decoration: const BoxDecoration(
color: Colors.black,
image: DecorationImage(
image: NetworkImage(
'https://www.digitalartsonline.co.uk/cmsdata/slideshow/3662115/baby-driver-rory-hi-res.jpg'),
fit: BoxFit.cover,
),
),
),
PositionedDirectional(
start: 0.0,
end: 0.0,
top: appBarSize > 0 ? appBarSize : 0,
bottom: -100,
child: Opacity(
opacity: 1 - percent,
// opacity: percent < 0.5 ? 1 : (1 - percent) * 2, // if you want to start fading when reach half way scroll
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 30 * percent),
child: const Card(
elevation: 20.0,
child: Center(
child: Text("Widget"),
),
),
),
),
),
],
),
),
);
}
#override
double get maxExtent => expandedHeight + expandedHeight / 2;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}

How to change opacity of text and angle of icon while using slider button in flutter

I want that as I move my slider button towards right, the opacity of text decreases and arrow icon rotates exactly oppposite, i.e. it strts rotating and at last last it should point backwards. I want to use opacity and Transform.rotate widgets, but how do I keep updating the value of dx ,so I can divide it with total width of container and use the fraction for calculation.
If there is another way, please do tell me.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:passenger_flutter_app/utils/colors.dart';
import 'package:passenger_flutter_app/widgets/custom_sliding_button.dart';
class CommonSwipeButton extends StatelessWidget {
final String? buttonText1;
final String buttonText2;
final VoidCallback buttonCallBack2;
final bool isInfo;
final VoidCallback? buttonCallBack1;
final Widget itemWidget;
CommonSwipeButton(
{this.buttonCallBack1,
required this.buttonCallBack2,
this.isInfo = false,
this.buttonText1,
required this.buttonText2,
required this.itemWidget});
#override
Widget build(BuildContext context) {
return Container(
child: Column(
//crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0, top: 16), child: itemWidget),
Padding(
padding:
const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: 44,
child: CustomSlidingButton(
//text: buttonText2,
),
),
),
)
],
),
);
}
}
/*
class SwipeButton extends StatefulWidget {
final ValueChanged<double>? valueChanged;
final String? text;
final Function? callBack;
SwipeButton({this.valueChanged, this.text, this.callBack});
#override
SwipeButtonState createState() {
return new SwipeButtonState();
}
}
class SwipeButtonState extends State<SwipeButton> {
ValueNotifier<double> valueListener = ValueNotifier(.0);
GlobalKey swipeKey = GlobalKey();
ValueNotifier<double> x=ValueNotifier<double>(0);
ValueNotifier<bool> isVisible = ValueNotifier<bool>(true);
#override
void initState() {
valueListener.addListener(notifyParent);
super.initState();
}
void notifyParent() {
if (widget.valueChanged != null) {
widget.valueChanged!(valueListener.value);
}
}
void getPos(double totalSize) {
RenderBox box = swipeKey.currentContext?.findRenderObject() as RenderBox;
Offset position = box.localToGlobal(Offset.zero); //this is global position
x.value = position.dx;
print(x);
if(x.value>355) {
print("Reached");
isVisible.value=false;
}
}
#override
Widget build(BuildContext context) {
return Container(
color: colorPrimary,
height: 40.0,
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Stack(
children: [
Center(
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Text(
"${widget.text}",
style: TextStyle(
color: Colors.white,
fontSize: 17,
),
),
),
),
Builder(
builder: (context) {
final handle = GestureDetector(
onHorizontalDragUpdate: (details) {
valueListener.value = (valueListener.value +
details.delta.dx / context.size!.width)
.clamp(.0, 1.0);
getPos(context.size!.width-5);
print(context.size?.width);
},
child: ValueListenableBuilder(
valueListenable: isVisible,
builder: (BuildContext context, bool val, Widget? child) {
return Container(
key: swipeKey,
height: 25.0,
width: 25.0,
color: val ? Colors.white : colorPrimary,
child: Center(
child: ValueListenableBuilder(
valueListenable: x,
builder: (BuildContext context, double d, Widget? child) {
return Transform.rotate(
angle: -pi*(d/350),
child: Icon(
Icons.arrow_forward,
color: Colors.orange,
size: 12,
),
);
},
),
),
);
},
),
);
return AnimatedBuilder(
animation: valueListener,
builder: (context, child) {
return Align(
alignment: Alignment(valueListener.value * 2 - 1, 0),
child: child,
);
},
child: handle,
);
},
),
],
),
);
}
}*/
You can use Slider widget from Flutter framework and update a local variable in the onChange function:
Slider(
value: _currentSliderValue,
max: 100, //or any max value you need
onChanged: (double value) {
setState(() {
_value = value;
});
},
);
And the _value variable you will use in Opacity and Transform widgets.

How to change view after hide SliverPersistentHeader

I need AppBar that shows other view on up scrolling.
Is it possible?
Bellow is my attempts to realize it. But...
This is what I need:
when scrolled down:
and when up scrolled
but I have now this:
Here is how I do this:
class DetailedEvent extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: false,
delegate: _SliverAppBarDelegate(),//HERE IS CUSTOM HEADER CLASS
)
This class used in MainPage:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
double scrollAnimationValue(double shrinkOffset) {
double maxScrollAllowed = maxExtent - minExtent;
return ((maxScrollAllowed - shrinkOffset) / maxScrollAllowed)
.clamp(0, 1)
.toDouble();
}
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
final tableTitleStyle =
TextStyle(color: AppColors.appWhiteDarker, fontSize: 16);
final tableValueStyle = TextStyle(color: AppColors.pureWhite, fontSize: 16);
final double visibleMainHeight = max(maxExtent - shrinkOffset, minExtent);
final double animationVal = scrollAnimationValue(shrinkOffset);
return Container(
height: visibleMainHeight,
width: MediaQuery.of(context).size.width,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
...,
Positioned(
bottom: 0.0,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[...],
),
),
)
],
),
);
}
#override...
}

How can i fix this SliverPersistentHeader Renderflexerror?

Gif of said error
I have a SliverPersistentHeader, that as a sliver does what it is supposed to do. However the content in the sliver causes renderflex errors.
How do i fix it, so that the content inside the sliver resizes with the sliver? I choose the SliverPersistentHeader to create a stack of headers, maybe there is another widget that would be more fitting?
The slivers are built in a CustomScrollView as objects in a list of slivers.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:location/location.dart';
import 'package:weather/weather.dart';[enter image description here][1]
import 'dart:math' as math;
class TracksPage extends StatelessWidget{
TracksPage();
List tracks = List();
#override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
),
body: CollapsingList()
);
}}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
#required this.minHeight,
#required this.maxHeight,
#required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
#override
double get minExtent => minHeight;
#override
double get maxExtent => math.max(maxHeight, minHeight);
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent)
{
return new SizedBox.expand(child: child);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
class CollapsingList extends StatelessWidget {
SliverPersistentHeader makeHeader(List<int> sums, int tracks, DateTime day) {
return SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 0.0,
maxHeight: 80.0,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[400]),
color: Colors.white
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.grey,
child: Text(tracks.toString(), style: TextStyle(color: Colors.black),),),
title: Text(" ${day.day}/${day.month}/${day.year}"),
subtitle: Row(children: <Widget>[
statCircle(Image.asset("assets/images/steps.png", height: 20, fit: BoxFit.cover,), [Color(0xFFC312E4), Color(0xFF841779)], sums[0].toString(), 0),
Padding(padding: EdgeInsets.only(right: 30),),
statCircle(Icon(Icons.timer, size: 15,), [Color(0xFF73E412), Color(0xFF5B9B1A)], "", 5),
Padding(padding: EdgeInsets.only(right: 30),),
statCircle(Icon(Icons.pin_drop, size: 15,), [Color(0xFF12D6E4), Color(0xFF118470)], sums[1].toString(), 5),]),
)
)
),
);
}
Column statCircle(Widget child, List<Color> colors, String info, double padding){
return Column(children: <Widget>[
Container(
child: child,
padding: EdgeInsets.all(7),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: colors,
stops: [0.3, 0.6],
begin: Alignment.topCenter,
end: Alignment.bottomCenter
)
),
),
Padding(padding: EdgeInsets.only(bottom: padding),),
Text(info)
],);
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Firestore.instance.collection("collectionName").where("fieldName", isEqualTo:fieldVariable).snapshots(),
builder: (context, snapshot){
if(snapshot.hasData){
List dates = extractAndSort(snapshot);
return CustomScrollView(
slivers: makeSlivers(dates, context)
);
}
else return Container();
}
);
}
makeSlivers(List dates, BuildContext context){
List<Widget> slivers = new List<Widget>();
List totals = summarize(Globals.tracks);
int length;
if(Globals.tracks != null){
length = Globals.tracks.length;
} else length = 0;
slivers.add( makeCirclesHeader(totals[0], length, totals[1], context));
slivers.add(SliverPadding(
padding: EdgeInsets.fromLTRB(0, 0, 0, 0),
sliver: SliverToBoxAdapter(
child: Container(
height: 20,
color: Colors.white)
)
));
if(dates != null)
{dates.forEach((date) {
List tracks = Globals.tracks.where((track) => track.trackCreated.toDate().day == date.day).toList();
slivers.add(makeHeader(summarize(tracks), tracks.length, date));
slivers.add(sliverListDay(tracks));
});}
return slivers;
}
You need to wrap it like this: _SliverAppBarDelegate -> Container -> ClipRect -> OverflowBox -> ListTile
delegate: _SliverAppBarDelegate(
minHeight: 0.0,
maxHeight: 80.0,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[400]),
color: Colors.white
),
child: ClipRect(
child: OverflowBox(
alignment: Alignment.topLeft,
maxHeight: 80.0,
child: ListTile(
leading: CircleAvatar(
...

How to get the SliverPersistentHeader to "overgrow"

I'm using a SliverPersistentHeader in my CustomScrollView to have a persistent header that shrinks and grows when the user scrolls, but when it reaches its maximum size it feels a bit stiff since it doesn't "overgrow".
Here is a video of the behaviour I want (from the Spotify app) and the behaviour I have:
.
While looking for a solution for this problem, I came across three different ways to solve it:
Create a Stack that contains the CustomScrollView and a header widget (overlaid on top of the scroll view), provide a ScrollController to the CustomScrollView and pass the controller to the header widget to adjust its size
Use a ScrollController, pass it to the CustomScrollView and use the value of the controller to adjust the maxExtent of the SliverPersistentHeader (this is what Eugene recommended).
Write my own Sliver to do exactly what I want.
I ran into problems with solution 1 & 2:
This solution seemed a bit "hackish" to me. I also had the problem, that "dragging" the header didn't scroll anymore, since the header was not inside the CustomScrollView anymore.
Adjusting the size of the sliver during scrolling results in strange side effects. Notably, the distance between the header and slivers below increases during the scroll.
That's why I opted for solution 3. I'm sure the way I implemented it, is not the best, but it works exactly as I want:
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'dart:math' as math;
/// The delegate that is provided to [ElSliverPersistentHeader].
abstract class ElSliverPersistentHeaderDelegate {
double get maxExtent;
double get minExtent;
/// This acts exactly like `SliverPersistentHeaderDelegate.build()` but with
/// the difference that `shrinkOffset` might be negative, in which case,
/// this widget exceeds `maxExtent`.
Widget build(BuildContext context, double shrinkOffset);
}
/// Pretty much the same as `SliverPersistentHeader` but when the user
/// continues to drag down, the header grows in size, exceeding `maxExtent`.
class ElSliverPersistentHeader extends SingleChildRenderObjectWidget {
final ElSliverPersistentHeaderDelegate delegate;
ElSliverPersistentHeader({
Key key,
ElSliverPersistentHeaderDelegate delegate,
}) : this.delegate = delegate,
super(
key: key,
child:
_ElSliverPersistentHeaderDelegateWrapper(delegate: delegate));
#override
_ElPersistentHeaderRenderSliver createRenderObject(BuildContext context) {
return _ElPersistentHeaderRenderSliver(
delegate.maxExtent, delegate.minExtent);
}
}
class _ElSliverPersistentHeaderDelegateWrapper extends StatelessWidget {
final ElSliverPersistentHeaderDelegate delegate;
_ElSliverPersistentHeaderDelegateWrapper({Key key, this.delegate})
: super(key: key);
#override
Widget build(BuildContext context) =>
LayoutBuilder(builder: (context, constraints) {
final height = constraints.maxHeight;
return delegate.build(context, delegate.maxExtent - height);
});
}
class _ElPersistentHeaderRenderSliver extends RenderSliver
with RenderObjectWithChildMixin<RenderBox> {
final double maxExtent;
final double minExtent;
_ElPersistentHeaderRenderSliver(this.maxExtent, this.minExtent);
#override
bool hitTestChildren(HitTestResult result,
{#required double mainAxisPosition, #required double crossAxisPosition}) {
if (child != null) {
return child.hitTest(result,
position: Offset(crossAxisPosition, mainAxisPosition));
}
return false;
}
#override
void performLayout() {
/// The amount of scroll that extends the theoretical limit.
/// I.e.: when the user drags down the list, although it already hit the
/// top.
///
/// This seems to be a bit of a hack, but I haven't found a way to get this
/// information in another way.
final overScroll =
constraints.viewportMainAxisExtent - constraints.remainingPaintExtent;
/// The actual Size of the widget is the [maxExtent] minus the amount the
/// user scrolled, but capped at the [minExtent] (we don't want the widget
/// to become smaller than that).
/// Additionally, we add the [overScroll] here, since if there *is*
/// "over scroll", we want the widget to grow in size and exceed
/// [maxExtent].
final actualSize =
math.max(maxExtent - constraints.scrollOffset + overScroll, minExtent);
/// Now layout the child with the [actualSize] as `maxExtent`.
child.layout(constraints.asBoxConstraints(maxExtent: actualSize));
/// We "clip" the `paintExtent` to the `maxExtent`, otherwise the list
/// below stops moving when reaching the border.
///
/// Tbh, I'm not entirely sure why that is.
final paintExtent = math.min(actualSize, maxExtent);
/// For the layout to work properly (i.e.: the following slivers to
/// scroll behind this sliver), the `layoutExtent` must not be capped
/// at [minExtent], otherwise the next sliver will "stop" scrolling when
/// [minExtent] is reached,
final layoutExtent = math.max(maxExtent - constraints.scrollOffset, 0.0);
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintExtent: paintExtent,
layoutExtent: layoutExtent,
maxPaintExtent: maxExtent,
);
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
/// This sliver is always displayed at the top.
context.paintChild(child, Offset(0.0, 0.0));
}
}
}
Now you can create your own SliverPersistentHeaderDelegate and override this param"
#override
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
OverScrollHeaderStretchConfiguration();
By default if null, but once you added it will allow you to stretch the view.
This is the class I use:
class CustomSliverDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final Widget title;
final Widget background;
final double topSafeArea;
final double maxExtent;
CustomSliverDelegate({
this.title,
this.child,
this.maxExtent = 350,
this.background,
this.topSafeArea = 0,
});
#override
Widget build(BuildContext context, double shrinkOffset,
bool overlapsContent) {
final appBarSize = maxExtent - shrinkOffset;
final proportion = 2 - (maxExtent / appBarSize);
final percent = proportion < 0 || proportion > 1 ? 0.0 : proportion;
return Theme(
data: ThemeData.dark(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: maxExtent),
child: Stack(
children: [
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
top: 0,
child: background,
),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Opacity(opacity: percent, child: child),
),
Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: AppBar(
title: Opacity(opacity: 1 - percent, child: title),
backgroundColor: Colors.transparent,
elevation: 0,
),
),
],
),
),
);
}
#override
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
OverScrollHeaderStretchConfiguration();
#override
double get minExtent => kToolbarHeight + topSafeArea;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
EDIT: I found another way how to stretch an image in AppBar
here is minimal reproducible example:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
));
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 200,
title: Text('Title'),
stretch: true,
flexibleSpace: FlexibleSpaceBar(
background: Image.network('https://i.imgur.com/2pQ5qum.jpg', fit: BoxFit.cover),
),
),
SliverToBoxAdapter(
child: Column(
children: List.generate(50, (index) {
return Container(
height: 72,
color: Colors.blue[200],
alignment: Alignment.centerLeft,
margin: EdgeInsets.all(8),
child: Text('Item $index'),
);
}),
),
),
],
),
);
}
}
The magic is in - stretch: true and BouncingScrollPhysics() properties.
There is not complicated listeners, stageful widgets so on. Just FlexibleSpaceBar with an image on background.
I solved this problem by simply creating a custom SliverPersistentHeaderDelegate.
Just override the getter for stretchConfiguration. Here's my code in case this is useful.
class LargeCustomHeader extends SliverPersistentHeaderDelegate {
LargeCustomHeader(
{this.children,
this.title = '',
this.childrenHeight = 0,
this.backgroundImage,
this.titleHeight = 44,
this.titleMaxLines = 1,
this.titleTextStyle = const TextStyle(
fontSize: 30,
letterSpacing: 0.5,
fontWeight: FontWeight.bold,
height: 1.2,
color: ColorConfig.primaryContrastColor)}) {}
final List<Widget> children;
final String title;
final double childrenHeight;
final String backgroundImage;
final int _fadeDuration = 250;
final double titleHeight;
final int titleMaxLines;
final double _navBarHeight = 56;
final TextStyle titleTextStyle;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
constraints: BoxConstraints.expand(),
decoration: BoxDecoration(
// borderRadius: BorderRadius.vertical(bottom: Radius.circular(35.0)),
color: Colors.black,
),
child: Stack(
fit: StackFit.loose,
children: <Widget>[
if (this.backgroundImage != null) ...[
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: FadeInImage.assetNetwork(
placeholder: "assets/images/image-placeholder.png",
image: backgroundImage,
placeholderScale: 1,
fit: BoxFit.cover,
alignment: Alignment.center,
imageScale: 0.1,
fadeInDuration: const Duration(milliseconds: 500),
fadeOutDuration: const Duration(milliseconds: 200),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: Container(
color: Color.fromRGBO(0, 0, 0, 0.6),
),
),
],
Positioned(
bottom: 0,
left: 0,
right: 0,
top: _navBarHeight + titleHeight,
child: AnimatedOpacity(
opacity: (shrinkOffset >= childrenHeight / 3) ? 0 : 1,
duration: Duration(milliseconds: _fadeDuration),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[if (children != null) ...children],
))),
Positioned(
top: _navBarHeight,
left: 0,
right: 0,
height: titleHeight,
child: Padding(
padding: const EdgeInsets.only(
right: 30, bottom: 0, left: 30, top: 5),
child: AnimatedOpacity(
opacity: (shrinkOffset >= childrenHeight + (titleHeight / 3))
? 0
: 1,
duration: Duration(milliseconds: _fadeDuration),
child: Text(
title,
style: titleTextStyle,
maxLines: titleMaxLines,
overflow: TextOverflow.ellipsis,
),
),
),
),
Container(
color: Colors.transparent,
height: _navBarHeight,
child: AppBar(
elevation: 0.0,
backgroundColor: Colors.transparent,
title: AnimatedOpacity(
opacity:
(shrinkOffset >= childrenHeight + (titleHeight / 3))
? 1
: 0,
duration: Duration(milliseconds: _fadeDuration),
child: Text(
title,
),
)),
)
],
));
}
#override
double get maxExtent => _navBarHeight + titleHeight + childrenHeight;
#override
double get minExtent => _navBarHeight;
// #override
// FloatingHeaderSnapConfiguration get snapConfiguration => FloatingHeaderSnapConfiguration() ;
#override
OverScrollHeaderStretchConfiguration get stretchConfiguration =>
OverScrollHeaderStretchConfiguration(
stretchTriggerOffset: maxExtent,
onStretchTrigger: () {},
);
double get maxShrinkOffset => maxExtent - minExtent;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
//TODO: implement specific rebuild checks
return true;
}
}
May be I have a easy way to code.
By use SliverAppBar and inside child widget leading, FlexibleSpaceBar and inside child widget title.
And by LayoutBuilder we can make some animation.
Full code link
SliverAppBar(
toolbarHeight: _appBarHeight,
collapsedHeight: _appBarHeight,
backgroundColor: Colors.white.withOpacity(1),
shadowColor: Colors.white.withOpacity(0),
expandedHeight: maxWidth,
/// ========================================
/// custom your app bar
/// ========================================
leading: Container(
width: 100,
height: _appBarHeight,
// color: Colors.blueAccent,
child: Center(
child: Icon(Icons.arrow_back_ios, color: Colors.black),
),
),
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
stretchModes: [
StretchMode.fadeTitle,
StretchMode.blurBackground,
StretchMode.zoomBackground,
],
titlePadding: EdgeInsets.all(0),
title: LayoutBuilder(
builder: (_, __) {
var height = __.maxHeight;
/// ========================================
/// custom animate you want by height change
/// ========================================
// Logger.debug(__.maxHeight);
return Stack(
children: [
if (height > 100)
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
),
],
);
},
),
background: Image.network(
'https://xx',
fit: BoxFit.cover,
),
),
),
You can try using SliverAppBar with stretch:true and pass the widget you want to display in the appbar as flexibleSpace.
Here is an example
CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
floating: true,
backgroundColor: Colors.black,
expandedHeight: 300,
centerTitle: true,
title: Text("My Custom Bar"),
leading: IconButton(
onPressed: () {},
icon: Icon(Icons.menu),
),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: Icon(Icons.search),
)
],
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
stretchModes:
[
StretchMode.zoomBackground,
StretchMode.blurBackground
],
background: YourCustomWidget(),
),
),
SliverList(
delegate: SliverChildListDelegate(
[
Container(color: Colors.red, height: 300.0),
Container(color: Colors.blue, height: 300.0),
],
),
),
],
);