In Flutter, how to create a SliverAppBar with frosted glass effect? - flutter

I want to create a top app bar similar to the one in the Apple News app.
It's a bit difficult to see the blur in the screenshot because the bar is so thin, but you get the idea.
I want the bar to expand and contract by scrolling and be pinned at the top when contracted, just like in the screenshot, the SliverAppBar does all that, except that I can't wrap it in ClipRect, BackdropFilter and Opacity to create the frosted glass effect because CustomScrollView only takes RenderSliver child classes.
My test code:
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('SliverAppBar'),
elevation: 0,
floating: true,
pinned: true,
backgroundColor: Colors.grey[50],
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.network("https://i.imgur.com/cFzxleh.jpg", fit: BoxFit.cover)
),
)
,
SliverFixedExtentList(
itemExtent: 150.0,
delegate: SliverChildListDelegate(
[
Container(color: Colors.red),
Container(color: Colors.purple),
Container(color: Colors.green),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink,
child: Image.network("https://i.imgur.com/cFzxleh.jpg", fit: BoxFit.cover)
),
],
),
),
],
);
}
Is there a way to achieve what I want?

I managed to get it working by wrapping an AppBar inside of a SliverPersistentHeader (this is basically what SliverAppBar does).
Ignore the un-blurred edges it's an iOS simulator bug.
Here is a proof of concept code example:
class TranslucentSliverAppBar extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
pinned: true,
delegate: _TranslucentSliverAppBarDelegate(
MediaQuery.of(context).padding,
)
);
}
}
class _TranslucentSliverAppBarDelegate extends SliverPersistentHeaderDelegate {
/// This is required to calculate the height of the bar
final EdgeInsets safeAreaPadding;
_TranslucentSliverAppBarDelegate(this.safeAreaPadding);
#override
double get minExtent => safeAreaPadding.top;
#override
double get maxExtent => minExtent + kToolbarHeight;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return ClipRect(child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Opacity(
opacity: 0.93,
child: Container(
// Don't wrap this in any SafeArea widgets, use padding instead
padding: EdgeInsets.only(top: safeAreaPadding.top),
height: maxExtent,
color: Colors.white,
// Use Stack and Positioned to create the toolbar slide up effect when scrolled up
child: Stack(
overflow: Overflow.clip,
children: <Widget>[
Positioned(
bottom: 0, left: 0, right: 0,
child: AppBar(
primary: false,
elevation: 0,
backgroundColor: Colors.transparent,
title: Text("Translucent App Bar"),
),
)
],
)
)
)
));
}
#override
bool shouldRebuild(_TranslucentSliverAppBarDelegate old) {
return maxExtent != old.maxExtent || minExtent != old.minExtent ||
safeAreaPadding != old.safeAreaPadding;
}
}

Related

Flutter animated sliver header

I am trying to create a profile header sliver that can animate.
If you consider above image, Section 1 is what we see in the fully expanded sliver, and Section 2 is what we want to see in pinned mode.
Now I would like transition to move the image - purple circle - to the side, shrink it slightly, and also move the name and the links.
I can achieve all of that but one thing: How to center them in the expanded view.
As I have to use transform to move widgets around, I cannot simply use a centring widget like column or center. And I didn't find a way to calculate the exact position to center the widget, as it needs the size of the widget, that I don't have.
Firstly I am using SliverPersistentHeaderDelegate and it provides shrinkOffset that will be used on linear interpolation(lerp method).
Then CompositedTransformTarget widget to follow the center widget.
On this example play with targetAnchor and followerAnchor and use t/shrinkOffset to maintain other animation.
class SFeb223 extends StatelessWidget {
const SFeb223({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
delegate: MySliverPersistentHeaderDelegate(),
pinned: true,
),
SliverToBoxAdapter(
child: SizedBox(
height: 1333,
),
)
],
),
);
}
}
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final LayerLink layerLink = LayerLink();
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
double t = shrinkOffset / maxExtent;
return Material(
color: Colors.cyanAccent.withOpacity(.2),
child: Stack(
children: [
Align(
alignment:
Alignment.lerp(Alignment.center, Alignment.centerLeft, t)!,
child: CompositedTransformTarget(
link: layerLink,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: lerpDouble(100, kToolbarHeight - 10, t),
width: lerpDouble(100, kToolbarHeight - 10, t),
decoration: const ShapeDecoration(
shape: CircleBorder(),
color: Colors.deepPurple,
),
),
),
),
),
CompositedTransformFollower(
link: layerLink,
targetAnchor: Alignment.lerp(
Alignment.bottomCenter, Alignment.centerRight, t)!,
followerAnchor:
Alignment.lerp(Alignment.topCenter, Alignment.centerLeft, t)!,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Column(
children: [Text("Sheikh")],
),
),
),
),
],
),
);
}
#override
double get maxExtent => kToolbarHeight * 6;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
}

Flutter animated SliverAppBar with Animated Positioned Image

I am trying to replicate this animation with my appBar:
I know I can use SliverAppBar and simply animate the textSize. But how would I implement the logic for the image? It moves to the right and slightly shrinks.
This is what I have for the text:
SliverAppBar(
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
title: Text('Test', textScaleFactor: 1),
),
pinned: true,
),
Any idea how I could solve this?
You play with SliverPersistentHeaderDelegate
class AppSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final t = shrinkOffset / maxExtent;
return Stack(
children: [
Align(
alignment: Alignment(0, .7), //perhaps it should also use lerp
child: Text(
"Title",
style: TextStyle(fontSize: ui.lerpDouble(34, 14, t)),
),
),
Align(
alignment:
Alignment.lerp(Alignment.topCenter, Alignment.topRight, t)!,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.settings),
),
)
],
);
}
#override
double get maxExtent => 200;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
And used on
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: AppSliverPersistentHeaderDelegate(),
),
SliverToBoxAdapter(
child: Container(
height: 12222,
color: Colors.red,
),
)
],
),
),
);
}
}
You can try using AnimatedPositioned class which flutter already provide .
Check this link
https://api.flutter.dev/flutter/widgets/AnimatedPositioned-class.html
You can use it and the change the position and size depending on a specific action .

Do not want rounded corners in the AppBar when the Sliver App Bar is collapsed

I'm trying to implement a layout, where the Sliver App Bar has rounded bottom corners when expanded, but when it is collapsed I do not want those rounded corners.
Actual Behaviour:
enter image description here
Expected Behaviour:
Here's my SliverAppBar code:
`SliverAppBar(
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Color(0xFFE0E64B),
),
backgroundColor: Color(0xFFE0E64B),
expandedHeight: 300.0,
floating: false,
pinned: true,
collapsedHeight: 60.0,
onStretchTrigger: () async {
setState(() {});
},
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'Pokedex',
style: TextStyle(
color: Colors.white,
),
),
Text(
'#025',
style: TextStyle(
color: Colors.white,
),
),
],
),
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: Container(
decoration: const BoxDecoration(
color: Color(0xFFE0E64B),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(50.0),
bottomRight: Radius.circular(50.0),
),
),
child: Hero(
tag: 'pokemon_container$index',
child: Column(
children: [
const SizedBox(
height: 120.0,
),
Expanded(
child: ClipRRect(
child: Image.network(
imageUrl,
fit: BoxFit.scaleDown,
),
),
),
const SizedBox(
height: 30.0,
),
],
),
),
),
),
),`
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30))),
Here is your code. Put it inside sliverAppBar
NestedScrollView / SliverAppBar solution
This is definitely achievable. SliverAppBar does support what we need, it has support for rounded borders, the shadow effect and changing sizes. For handling the border requirement we can use a RoundedRectangleBorder.
Although for getting a smooth transition for the border change, we need to update the values frequently, when changing the size of the SliverAppBar.
Example code
Do note that the package flutter_riverpod (version 1.0.3) is used for state management in this example.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class RoundedSliverExampleScreen extends StatelessWidget {
const RoundedSliverExampleScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
ExpandingAppBar(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Flexible is important for the children widgets added here.
Flexible(child: Container(color: Colors.yellow, width: 50, height: 50,))
],
)
];
},
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text("Hello!")
],
),
)
);
}
}
/// An SliverAppBar widget with alternating rounded border depending on the
/// expandedHeight.
///
/// Provides easy support for adding children widgets in the
/// expanded area as if it was a Column, although children widgets should be
/// wrapped in a Flexible widget.
class ExpandingAppBar extends ConsumerWidget {
const ExpandingAppBar({
Key? key,
this.children = const <Widget>[],
this.mainAxisAlignment = MainAxisAlignment.start
}) : super(key: key);
final List<Widget> children;
final MainAxisAlignment mainAxisAlignment;
#override
Widget build(BuildContext context, WidgetRef ref) {
RoundedHeaderState state = ref.watch(roundedHeaderProvider);
return SliverAppBar(
expandedHeight: state.highestHeight,
pinned: true,
primary: true,
forceElevated: true,
title: const Text('Pokèdex'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(state.radius)),
),
flexibleSpace: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// We update the state here.
ref.read(roundedHeaderProvider.notifier).updateHeight(constraints.maxHeight);
return Opacity(
opacity: state.scrollFraction,
child: Padding(
padding: EdgeInsets.only(top: state.smallestHeight),
child: Column(mainAxisAlignment: mainAxisAlignment, children: children),
),
);
},
),
);
}
}
#immutable
class RoundedHeaderState {
final double highestHeight = 256;
final double smallestHeight = kToolbarHeight + 24;
final double currentHeight;
final double contentOpacity = 1;
const RoundedHeaderState({this.currentHeight = 256});
double get scrollFraction => min(max((currentHeight - smallestHeight) / (highestHeight - smallestHeight), 0), 1);
double get radius => 64 * scrollFraction;
}
class RoundedHeaderNotifier extends StateNotifier<RoundedHeaderState> {
RoundedHeaderNotifier(): super(const RoundedHeaderState());
updateHeight(double currentHeight) {
final newState = RoundedHeaderState(currentHeight: currentHeight);
// Check that the new state is not equal to the next (prevents rebuild loop)
if(state.currentHeight != newState.currentHeight) {
// Setting state triggers an rebuild, the PostFrameCallback let Flutter
// postpone the upcoming rebuild at a later time.
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
state = newState;
});
}
}
}
final roundedHeaderProvider = StateNotifierProvider<RoundedHeaderNotifier, RoundedHeaderState>((ref) {
return RoundedHeaderNotifier();
});
// Pay attention to the ProviderScope wrapping the MaterialApp. Riverpod requires this.
void main() => runApp(
const ProviderScope(
child: MaterialApp(home: RoundedSliverExampleScreen())
)
);
Result - Gif of the SliverAppBar's transition.

Is it possible to have both 'expand' and 'contract' effects with the slivers in Flutter?

I have implemented a screen with the CustomScrollView, SliverAppBar and FlexibleSpaceBar like the following:
Now, I'm stuck trying to further expand the functionality by trying to replicate the following effect:
Expand image to fullscreen on scroll
Can something like this be done by using the slivers in Flutter?
Basically, I want the image in it's initial size when screen opens, but depending on scroll direction, it should animate -> contract/fade (keeping the list scrolling functionality) or expand to fullscreen (maybe to new route?).
Please help as I'm not sure in which direction I should go.
Here's the code for the above screen:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const double bottomNavigationBarHeight = 48;
#override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
home: SliverPage(),
);
}
class SliverPage extends StatefulWidget {
#override
_SliverPageState createState() => _SliverPageState();
}
class _SliverPageState extends State<SliverPage> {
double appBarHeight = 0.0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
centerTitle: true,
expandedHeight: MediaQuery.of(context).size.height * 0.4,
pinned: true,
flexibleSpace: LayoutBuilder(builder: (context, boxConstraints) {
appBarHeight = boxConstraints.biggest.height;
return FlexibleSpaceBar(
centerTitle: true,
title: AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: appBarHeight < 80 + MediaQuery.of(context).padding.top ? 1 : 0,
child: Padding(padding: EdgeInsets.only(bottom: 2), child: Text("TEXT"))),
background: Image.network(
'https://images.pexels.com/photos/443356/pexels-photo-443356.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
fit: BoxFit.cover,
),
);
}),
),
SliverList(delegate: SliverChildListDelegate(_buildList(40))),
],
),
);
}
List _buildList(int count) {
List<Widget> listItems = List();
for (int i = 0; i < count; i++) {
listItems.add(
new Padding(padding: new EdgeInsets.all(20.0), child: new Text('Item ${i.toString()}', style: new TextStyle(fontSize: 25.0))));
}
return listItems;
}
}
use CustomScrollView with SliverPersistentHeader
child: LayoutBuilder(
builder: (context, constraints) {
return CustomScrollView(
controller: ScrollController(initialScrollOffset: constraints.maxHeight * 0.6),
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: Delegate(constraints.maxHeight),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, i) => Container(height: 100, color: i.isOdd? Colors.green : Colors.green[700]),
childCount: 12,
),
),
],
);
},
),
the Delegate class used by SliverPersistentHeader looks like:
class Delegate extends SliverPersistentHeaderDelegate {
final double _maxExtent;
Delegate(this._maxExtent);
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
var t = shrinkOffset / maxExtent;
return Material(
elevation: 4,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Image.asset('images/bg.jpg', fit: BoxFit.cover,),
Opacity(
opacity: t,
child: Container(
color: Colors.deepPurple,
alignment: Alignment.bottomCenter,
child: Transform.scale(
scale: ui.lerpDouble(16, 1, t),
child: Text('scroll me down',
style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white)),
),
),
),
],
),
);
}
#override double get maxExtent => _maxExtent;
#override double get minExtent => 64;
#override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
}

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),
],
),
),
],
);