Sticky headers on SliverList - flutter

I've seen new flutter video and seen some interesting. (It's not typical sticky header or expandable list, so I don't know how to name it)
Video - watch from 0:20
Does anybody know how can I create such type of list with headers using SliverList?

One way is to create a CustomScrollView and pass a SliverAppBar pinned to true and a SliverFixedExtentList object with your Widgets.
Example:
List<Widget> _sliverList(int size, int sliverChildCount) {
var widgetList = <Widget>[];
for (int index = 0; index < size; index++)
widgetList
..add(SliverAppBar(
title: Text("Title $index"),
pinned: true,
))
..add(SliverFixedExtentList(
itemExtent: 50.0,
delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: Text('list item $index'),
);
}, childCount: sliverChildCount),
));
return widgetList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Slivers"),
),
body: CustomScrollView(
slivers: _sliverList(50, 10),
),
);
}

SliverPersistentHeader is the more generic widget behind SliverAppBar that you can use.
SliverPersistentHeader(
delegate: SectionHeaderDelegate("Section B"),
pinned: true,
),
And the SectionHeaderDelegate can be implement with something like:
class SectionHeaderDelegate extends SliverPersistentHeaderDelegate {
final String title;
final double height;
SectionHeaderDelegate(this.title, [this.height = 50]);
#override
Widget build(context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Theme.of(context).primaryColor,
alignment: Alignment.center,
child: Text(title),
);
}
#override
double get maxExtent => height;
#override
double get minExtent => height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
}

Related

How to make this sliver in flutter?

How to make this sliver flutter?
I want to do the same thing as in the following video
enter image description here
You can do like this.
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text("App bar"),
),
SliverPersistentHeader(
delegate: DelegateHeader(),
pinned: true,
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index){
return ListTile(
title: Text("new item $index"),
);
},
childCount: 50
)
)
],
),
);
}
and
class DelegateHeader extends SliverPersistentHeaderDelegate {
#override
double get maxExtent => 100;
#override
double get minExtent => 100;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.black26,
child: Center(child: Text("Always here")),
);
}
}

Add SafeArea into SliverAppBar

How to make FlexibleSpaceBar in SliverAppBar honour SafeArea?.
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
title: FittedBox(
fit: BoxFit.fitWidth,
child: Image.asset('assets/images/user.png')),
),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
)
I need the image be below os header at all time
I tried to wrap it with SafeArea widget but that didn't work and crashed
The following should work:
class TestSafeArea extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: SafeAreaPersistentHeaderDelegate(
expandedHeight: 200,
child: Image.asset('assets/YOUR_IMAGE.png'))),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
}
}
class SafeAreaPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final double expandedHeight;
SafeAreaPersistentHeaderDelegate({this.child, this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SafeArea(bottom: false, child: SizedBox.expand(child: child));
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(SafeAreaPersistentHeaderDelegate old) {
if (old.child != child) {
return true;
}
return false;
}
}
Sorry about the confusion!
EDIT #2 - Just saw you don't want the entire AppBar in the SafeArea
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(
backgroundColor: Colors.blue,
automaticallyImplyLeading: false,
title: title,
flexibleSpace: (title == null && flexibleSpace != null)
? Semantics(child: flexibleSpace, header: true)
: flexibleSpace,
toolbarOpacity: 1,
bottomOpacity: 1.0),
);
return appBar;
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => kToolbarHeight;
#override
bool shouldRebuild(SafeAreaPersistentHeaderDelegate old) {
if (old.flexibleSpace != flexibleSpace) {
return true;
}
return false;
}
}
This will give your desired effect.
By using a SliverPersistentHeader with a custom SliverPersistentHeaderDelegate that returns an AppBar wrapped in the SafeArea widget.
class TestSafeArea extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
delegate: SafeAreaPersistentHeaderDelegate(
expandedHeight: 200,
flexibleSpace: SafeArea(
child: Container(
color: Colors.red,
),
)),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
}
}

Animate widget alignment

I'm building a custom flexible app bar to use in a NestedScrollView and i'm running into issues with the animation.
What I want to achieve is something like this:
In the expanded state, the text is aligned with the top of the Profile picture (in orange), but when the bar collapse, it ends up aligned in the center. I also need all the elements (text + picture) to scale accordingly.
I have access to the current expand factor of the bar using a LayoutBuilder and a bit of math
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double paddingTop = MediaQuery.of(context).padding.top;
double maxExtent = kExpandedHeight + paddingTop;
double minExtent = kToolbarHeight + paddingTop;
final double deltaExtent = maxExtent - minExtent;
// 0.0 -> Expanded
// 1.0 -> Collapsed to toolbar
final double t = (1.0 - (constraints.maxHeight - minExtent) / deltaExtent)
.clamp(0.0, 1.0);
// t can be used to animate here
});
I have managed to scale elements with the Transform widget and the value of t but what I can't figure out is how to animate the switch of alignment of the text part so that it end up perfectly aligned in the center with the picture.
Any ideas? :)
try this,
class Act_Demo extends StatefulWidget {
#override
_Act_DemoState createState() => _Act_DemoState();
}
class _Act_DemoState extends State<Act_Demo> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: CustomScrollView(
slivers: <Widget>[
TransitionAppBar(
backgroundColor: Colors.red,
extent: 150,
avatar: ListTile(
title: Text("Name", style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),),
subtitle: Text("abc#gmail.com"),
trailing: CircleAvatar(backgroundColor: Colors.orange,radius: 30.0,),
),
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return Container(
child: ListTile(
title: Text("${index}a"),
));
}, childCount: 25))
],
),
),
);
}
}
.
class TransitionAppBar extends StatelessWidget {
final Widget avatar;
final double extent;
final Color backgroundColor;
TransitionAppBar({this.avatar, this.backgroundColor = Colors.transparent, this.extent = 200, Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
pinned: true,
delegate: _TransitionAppBarDelegate(
avatar: avatar,
backgroundColor: backgroundColor,
extent: extent > 150 ? extent : 150
),
);
}
}
class _TransitionAppBarDelegate extends SliverPersistentHeaderDelegate {
final _avatarAlignTween = AlignmentTween(begin: Alignment.center, end: Alignment.topCenter);
final Widget avatar;
final double extent;
final Color backgroundColor;
_TransitionAppBarDelegate({this.avatar, this.backgroundColor, this.extent = 200})
: assert(avatar != null),
assert(backgroundColor != null),
assert(extent == null || extent >= 150);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final progress = shrinkOffset / maxExtent;
final avatarAlign = _avatarAlignTween.lerp(progress);
return Container(
color: backgroundColor,
child: Align(
alignment: avatarAlign,
child: Container(
child: avatar,
),
),
);
}
#override
double get maxExtent => extent;
#override
double get minExtent => 70;
#override
bool shouldRebuild(_TransitionAppBarDelegate oldDelegate) {
return avatar != oldDelegate.avatar;
}
}

How to change pinned of Sliver Persistent Header

I am using 2 Sliver Headers above GridView and a ListView on a CustomScrollView . I want only 1 of the headers (the one I am scrolling over) to be pinned When I scroll down. I want to be able to scroll down and only one of the headers is pinned when I pass over Gridview.
EDIT:
Added _SliverAppBarDelegate
Scaffold(
body: SafeArea(
child: DefaultTabController(
length: 2,
child: CustomScrollView(
slivers: [
makeHeader('Categories', false),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
margin: EdgeInsets.all(5.0),
color: Colors.blue,
),
childCount: 10),
),
makeHeader('Watchlist', false),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
margin: EdgeInsets.all(5.0),
color: Colors.red,
),
childCount: 10),
),
],
),
),
),
)
SliverPersistentHeader makeHeader(String headerText, bool pinned) {
return SliverPersistentHeader(
pinned: pinned,
floating: true,
delegate: _SliverAppBarDelegate(
minHeight: 40.0,
maxHeight: 60.0,
child: Container(
child: Text(
headerText,
style: TextStyle(fontSize: 24, color: Colors.green,fontWeight: FontWeight.bold),
)),
),
);
}
///////////////////////////EDIT
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
#required this.minHeight,
#required this.maxHeight,
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;
}
}
This is an old question but I'm putting my solution here in case anyone else needs the sticky header effect without a plugin.
My solution is to have the sliver headers' minExtent value in a map where the key is the number of sliverList items above the header.
final _headersMinExtent = <int, double>{};
We can then reduce the minExtent when the header needs to be pushed out of the view. To accomplish that we listen to the scrollController. Note that we could also update the pinned status but we wouldn't get a transition.
We calculate the minExtent from :
The number of items above the header : key.
The number of headers already pushed out the view : n.
_scrollListener() {
var n = 0;
setState(() {
_headersMinExtent.forEach((key, value) {
_headersMinExtent[key] = (key * 30 + n * 40 + 190 - _scrollController.offset).clamp(0, 40);
n++;
});
});
}
When we construct our widget list we have to pass the minExtent parameter to the SliverPersistentHeaderDelegate :
List<Widget> _constructList() {
var widgetList = <Widget>[];
for (var i = 0; i < itemList.length; i++) {
// We want a header every 5th item
if (i % 5 == 0) {
// Don't forget to init the minExtent value.
_headersMinExtent[i] = _headersMinExtent[i] ?? 40;
// We pass the minExtent as a parameter.
widgetList.add(SliverPersistentHeader(pinned: true, delegate: HeaderDelegate(_headersMinExtent[i]!)));
}
widgetList.add(SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
decoration: BoxDecoration(color: Colors.yellow, border: Border.all(width: 0.5)),
height: 30,
),
childCount: 1,
)));
}
return widgetList;
}
This is the result :
Full application code :
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _scrollController = ScrollController();
final _headersMinExtent = <int, double>{};
final itemList = List.filled(40, "item");
#override
void initState() {
super.initState();
_scrollController.addListener(() {
_scrollListener();
});
}
_scrollListener() {
var n = 0;
setState(() {
_headersMinExtent.forEach((key, value) {
_headersMinExtent[key] = (key * 30 + n * 40 + 190 - _scrollController.offset).clamp(0, 40);
n++;
});
});
}
List<Widget> _constructList() {
var widgetList = <Widget>[];
for (var i = 0; i < itemList.length; i++) {
// We want a header every 5th item
if (i % 5 == 0) {
// Don't forget to init the minExtent value.
_headersMinExtent[i] = _headersMinExtent[i] ?? 40;
// We pass the minExtent as a parameter.
widgetList.add(SliverPersistentHeader(pinned: true, delegate: HeaderDelegate(_headersMinExtent[i]!)));
}
widgetList.add(SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
decoration: BoxDecoration(color: Colors.yellow, border: Border.all(width: 0.5)),
height: 30,
),
childCount: 1,
)));
}
return widgetList;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(),
body: CustomScrollView(
controller: _scrollController,
slivers: _constructList(),
),
),
);
}
}
class HeaderDelegate extends SliverPersistentHeaderDelegate {
final double _minExtent;
HeaderDelegate(this._minExtent);
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
decoration: BoxDecoration(color: Colors.green, border: Border.all(width: 0.5)),
);
}
#override
double get minExtent => _minExtent;
#override
double get maxExtent => 40;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
}
class ListChildDelegate extends SliverChildDelegate {
#override
Widget? build(BuildContext context, int index) {
// TODO: implement build
throw UnimplementedError();
}
#override
bool shouldRebuild(covariant SliverChildDelegate oldDelegate) => true;
}
#diegoveloper I found this plugin. https://pub.dev/packages/flutter_sticky_header
I wish there is still an easier way to do it without a plugin. However, this plugin exactly solves the issue I am describing.

Allow GridView to overlap SliverAppBar

I am trying to reproduce the following example from the earlier Material design specifications (open for animated demo):
Until now I was able to produce the scrolling effect, but the overlap of the content is still missing. I couldn't find out how to do this properly.
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('Title'),
expandedHeight: 200.0,
primary: true,
pinned: true,
),
SliverFixedExtentList(
itemExtent: 30.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) => Text('Item $i')
),
),
],
),
);
}
}
I managed to get this functionality, using the ScrollController and a couple of tricks:
Here's the code:
ScrollController _scrollController;
static const kHeaderHeight = 235.0;
double get _headerOffset {
if (_scrollController.hasClients) if (_scrollController.offset > kHeaderHeight)
return -1 * (kHeaderHeight + 50.0);
else
return -1 * (_scrollController.offset * 1.5);
return 0.0;
}
#override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(() => setState(() {}));
}
#override
Widget build(BuildContext context) {
super.build(context);
return StackWithAllChildrenReceiveEvents(
alignment: AlignmentDirectional.topCenter,
children: [
Positioned(
top: _headerOffset,
child: Container(
height: kHeaderHeight,
width: MediaQuery.of(context).size.width,
color: Colors.blue,
),
),
Padding(
padding: EdgeInsets.only(left: 20.0, right: 20.0),
child: Feed(controller: _scrollController, headerHeight: kHeaderHeight),
),
],
);
}
To make the Feed() not overlap the blue container, I simply made the first child of it a SizedBox with the required height property.
Note that I am using a modified Stack class. That is in order to let the first Widget in the stack (the blue container) to detect presses, so it will fit my uses; unfortunately at this point the default Stack widget has an issue with that, you can read more about it over https://github.com/flutter/flutter/issues/18450.
The StackWithAllChildrenReceiveEvents code can be found over https://github.com/flutter/flutter/issues/18450#issuecomment-575447316.
I had the same problem and could not solve it with slivers. This example from another stackoverflow question solved my problem.
flutter - App bar scrolling with overlapping content in Flexible space
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Scroll demo',
home: new Scaffold(
appBar: new AppBar(elevation: 0.0),
body: new CustomScroll(),
),
);
}
}
class CustomScroll extends StatefulWidget {
#override
State createState() => new CustomScrollState();
}
class CustomScrollState extends State<CustomScroll> {
ScrollController scrollController;
double offset = 0.0;
static const double kEffectHeight = 100.0;
#override
Widget build(BuildContext context) {
return new Stack(
alignment: AlignmentDirectional.topCenter,
children: <Widget> [
new Container(
color: Colors.blue,
height: (kEffectHeight - offset * 0.5).clamp(0.0, kEffectHeight),
),
new Positioned(
child: new Container(
width: 200.0,
child: new ListView.builder(
itemCount: 100,
itemBuilder: buildListItem,
controller: scrollController,
),
),
),
],
);
}
Widget buildListItem(BuildContext context, int index) {
return new Container(
color: Colors.white,
child: new Text('Item $index')
);
}
void updateOffset() {
setState(() {
offset = scrollController.offset;
});
}
#override
void initState() {
super.initState();
scrollController = new ScrollController();
scrollController.addListener(updateOffset);
}
#override
void dispose() {
super.dispose();
scrollController.removeListener(updateOffset);
}
}
Change the list to a grid and its what you want