I have a requirement to build a view that has a tab bar within a collapsible app bar (so when the app bar collapses, just the tabs are visible) which controls a tab view, each of which will host a different sub view. Further, within those subviews on each tab, the content should be scrollable (which impacts the collapsed state of the app bar), and widgets within that scrollable view need to have headers that "stick" to the bottom of the app bar/tab bar.
A sliver-based implementation of sticky headers are available here: Flutter Sticky Headers
I took the first sample from Flutter's documentation on this page:
https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html#widgets.NestedScrollView.1
and wrapped the SliverFixedExtentList in a SliverStickyHeader element and the headers are not sticking to the top of the list. I've tried this in a variety of places but can't seem to make a sticky header work using this library or even the original which used RenderObjects. Any thoughts as to why this might be?
Here's the full code as an example:
import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatelessWidget(),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final List<String> _tabs = <String>['Tab 1', 'Tab 2'];
return DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title:
const Text('Books'), // This is the title in the app bar.
pinned: true,
expandedHeight: 150.0,
// The "forceElevated" property causes the SliverAppBar to show
// a shadow. The "innerBoxIsScrolled" parameter is true when the
// inner scroll view is scrolled beyond its "zero" point, i.e.
// when it appears to be scrolled below the SliverAppBar.
// Without this, there are cases where the shadow would appear
// or not appear inappropriately, because the SliverAppBar is
// not actually aware of the precise position of the inner
// scroll views.
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
];
},
body: TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
// This Builder is needed to provide a BuildContext that is
// "inside" the NestedScrollView, so that
// sliverOverlapAbsorberHandleFor() can find the
// NestedScrollView.
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber
// above.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: SliverStickyHeader(
header: Text('header'),
sliver: SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return ListTile(
title: Text('Item $index'),
);
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 30,
),
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}
I think the problem has something to do with the SliverOverlapInjector there is a workaround mentioned in this issue comment: https://github.com/flutter/flutter/issues/91972#issuecomment-995462622
Basically you need an improved overlap injector ( SliverPinnedOverlapInjector):
DISCLAIMER: The following source code is directly copied from the issue comment:
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class SliverPinnedOverlapInjector extends SingleChildRenderObjectWidget {
const SliverPinnedOverlapInjector({
required this.handle,
Key? key,
}) : super(key: key);
final SliverOverlapAbsorberHandle handle;
#override
RenderSliverPinnedOverlapInjector createRenderObject(BuildContext context) {
return RenderSliverPinnedOverlapInjector(
handle: handle,
);
}
#override
void updateRenderObject(
BuildContext context,
RenderSliverPinnedOverlapInjector renderObject,
) {
renderObject.handle = handle;
}
}
class RenderSliverPinnedOverlapInjector extends RenderSliver {
RenderSliverPinnedOverlapInjector({
required SliverOverlapAbsorberHandle handle,
}) : _handle = handle;
double? _currentLayoutExtent;
double? _currentMaxExtent;
SliverOverlapAbsorberHandle get handle => _handle;
SliverOverlapAbsorberHandle _handle;
set handle(SliverOverlapAbsorberHandle value) {
if (handle == value) return;
if (attached) {
handle.removeListener(markNeedsLayout);
}
_handle = value;
if (attached) {
handle.addListener(markNeedsLayout);
if (handle.layoutExtent != _currentLayoutExtent ||
handle.scrollExtent != _currentMaxExtent) markNeedsLayout();
}
}
#override
void attach(PipelineOwner owner) {
super.attach(owner);
handle.addListener(markNeedsLayout);
if (handle.layoutExtent != _currentLayoutExtent ||
handle.scrollExtent != _currentMaxExtent) markNeedsLayout();
}
#override
void detach() {
handle.removeListener(markNeedsLayout);
super.detach();
}
#override
void performLayout() {
_currentLayoutExtent = handle.layoutExtent;
final paintedExtent = min(
_currentLayoutExtent!,
constraints.remainingPaintExtent - constraints.overlap,
);
geometry = SliverGeometry(
paintExtent: paintedExtent,
maxPaintExtent: _currentLayoutExtent!,
maxScrollObstructionExtent: _currentLayoutExtent!,
paintOrigin: constraints.overlap,
scrollExtent: _currentLayoutExtent!,
layoutExtent: max(0, paintedExtent - constraints.scrollOffset),
hasVisualOverflow: paintedExtent < _currentLayoutExtent!,
);
}
}
Just copy the SliverPinnedOverlapInjector to your project and use it as a drop-in replacement for the SliverOverlapInjector. In my case this was the only change necessary. Everything else should stay the same.
Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
centerTitle: false,
automaticallyImplyLeading: false,
backgroundColor: Colors.black,
title: Text('Murder series (Event That Happen)'),
actions: <Widget>[
],
child: Container(
height: 20,
width: 20,
child: SvgPicture.asset(
'assets/library_pre.svg',
color: Colors.white,
),
),
),
),
],
expandedHeight: 200.0,
floating: true,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Column(
children: [
SizedBox(
height: 70,
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CircleAvatar(
radius: 40,
backgroundImage:
AssetImage('assets/forumPage.png'),
),
Text(
'74 Members',
style: TextStyle(
fontSize: 25,
color: Colors.white,
),
),
overlapped(),
Text(
'Leave',
style: TextStyle(
fontSize: 25,
color: Colors.red,
),
)
],
),
),
),
Container(
height: 30,
width: double.infinity,
decoration: BoxDecoration(color: Colors.red),
)
],
),
)),
),
];
},
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
],
),
],
),
))),
);
Is it possible to make the FlexibleSpaceBar begin at the bottom of the SliverAppBar?
I really like the effects that the FlexibleSpaceBar has but the only way I'm currently able to do this is by adding a some padding to the top, e.g. 50 pixels or so.
import 'package:flutter/material.dart';
class AccountScreen extends StatefulWidget {
static const routeName = '/account';
#override
_AccountScreenState createState() => _AccountScreenState();
}
class _AccountScreenState extends State<AccountScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[900],
drawer: Drawer(),
body: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('Title'),
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
background: Column(
children: <Widget>[
Text('Some Text')
],
),
),
),
],
)),
);
}
}
As you can see the FlexibleSpaceBar begins at the top of the screen behind the SliverAppBar
I use as this. This makes the top of FlexibleSpaceBar start at the bottom of SilverAppBar.
SliverAppBar(
elevation: 0,
expandedHeight: 300, // your wanted height
flexibleSpace: FlexibleSpaceBar()
)
I have home screen with bottom navigation consist of two items which all have ListView inside with infinite list and I want SliverAppBar to be scrollable when user scrolls in one of the list.
Here is what I have so far
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: new Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
snap: false,
floating: false,
pinned: true,
expandedHeight: 160.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Title'),
),
),
SliverFillRemaining(
child: TabBarView(
children: <Widget>[Items(), Activities()], //THESE HAVE LIST VIEW IN EACH
),
)
],
),
));
}
}
And here is code of one of TabBarView children.
class Items extends StatelessWidget {
#override
Widget build(BuildContext context) {
List<Widget> itemsWidgets = List.from(getItemsList()
.map((Item item) => createItemWidget(context, item)));
return Scaffold(
body: Center(
child: ListView(
children: itemsWidgets,
),
),
);
}
ListTile createItemWidget(BuildContext context, Item item) {
return new ListTile(
title: Text(item.sender.name),
subtitle: Text('10:30 am'),
);
}
}
How can SilverAppBar be scrollable when user scrolls in one of the list? Any help/suggestion will be appreciated.
Use NestedScrollView. There is an example in the api
do anyone know how to have the screen to pull down and it would show a search icon button and then a search filter bar on the top?
For example in Spotify, there is a search function for the album, that when you pull the screen down and it show a search filter bar to search for music. (i think).
What you're looking for is RefreshIndicator. Which you insert above your ListView/GridView/scroll.
You can use SilverAppBar inside CustomScrollView to do this. Every time u scroll up in any position, appbar will appear
You can checkout preview for this example i write here
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: MyWidget(),
),
);
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers:[
SliverAppBar(
// pinned: true,
floating: true,
snap: true,
title: Text('Your appbar'),
),
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'),
);
},
),
),
],
),
);
}
}
Currently I'm creating an application using Flutter which involves several lists which can be ordered and filtered. When an ordering or filter is active, persistent bar appears under the app bar which indicates what filters are active. This is achieved by stacking int on top of a ListView using a StackView, and moving the ListView down a little using it's padding. An example of this can be seen in the image below:
Now I'm trying to create a second view using a CustomScrollView and a SliverAppBar, and I want this same "filter bar" behaviour here. I've tried using a SliverList with a single item, and a SliverList below that with the filtered items, but then both lists scroll as if they were one long list. I've also tried adding a bar through the bottom property of the SliverAppBar, but this results in a bar which is placed over the title of the app bar, and as far as I know, there is no way to move this title up. This result can be seen in the image below (the red bar is a placeholder for the filter bar in the first image).
Is there any way to achieve this "persistent" bar effect in a CustomScrollView with a SliverAppBar?
Since there are multiple scrollable views to be managed on the screen, you'll need to use NestedScrollView. This should solve the issue where both scrollable widgets are scrolled like a single long list.
Here's a sample on how it can be implemented similar to the screenshots you've provided.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: MyStatelessWidget(),
);
}
}
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
// This will help manage the SliverAppBar and ListView
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/images/background.jpeg"),
fit: BoxFit.cover,
),
),
child: Container(
padding: EdgeInsets.all(16.0),
alignment: Alignment.centerLeft,
child: Text(
'Title',
style: TextStyle(color: Colors.white, fontSize: 36.0),
),
),
),
),
// title: const Text('Title'),
floating: true,
pinned: true,
expandedHeight: 300.0,
forceElevated: innerBoxIsScrolled,
bottom: AppBar(
backgroundColor: Colors.red,
toolbarHeight: 64.0,
),
),
];
},
body: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
child: Center(child: Text('Item $index')),
);
},
),
),
);
}
}