CustomScrollView: Body scrolls under SliverAppBar - flutter

Flutter DartPad
I have multiple SliverAppBar's within a CustomScrollView, the body of the screen is within the SliverFillRemaining.
The Top SliverAppBar is pinned
The Middle SliverAppBar is an image and will collapse as the user scrolls
Bottom SliverAppBar is a TabBar that is pinned and will stay under the First SliverAppBar after the Image has fully collapsed
The current experience is that when you scroll initially, the body scrolls under the lowest SliverAppBar. I have already tried to use SliverOverlapAbsorber/Injector, but that just adds a space to the top of the body so that the spaces get overlapped rather than the body, but this is not what I want.
I want the body and the SliverAppBars to scroll together until the Middle SliverAppBar has collapsed completely, then I want the body to scroll.
I have been working on this for hours, How do you stop the body from being overlapped on scroll?

To achieve this kind of scrolling behaviour it's easier to use NestedScrollView and note that the main appbar isn't in the slivers anymore
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('title'),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {},
),
),
body: NestedScrollView(
floatHeaderSlivers: true,
physics: const BouncingScrollPhysics(),
body: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: [
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
children: List.generate(
1000,
(index) => Text('Tab One: $index'),
),
),
),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
children: List.generate(
1000,
(index) => Text('Tab Two: $index'),
)),
)
],
),
headerSliverBuilder: (context, innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: true,
floating: false,
elevation: 0,
toolbarHeight: 0,
collapsedHeight: null,
automaticallyImplyLeading: false,
expandedHeight: MediaQuery.of(context).size.height * .4,
flexibleSpace: const FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: Placeholder()),
titleSpacing: 0,
primary: false,
),
SliverAppBar(
pinned: true,
forceElevated: true,
primary: false,
automaticallyImplyLeading: false,
expandedHeight: 50,
collapsedHeight: null,
toolbarHeight: 50,
titleSpacing: 0,
title: Align(
alignment: Alignment.topCenter,
child: TabBar(
controller: _tabController,
isScrollable: true,
tabs: [
const Text('Tab One'),
const Text('Tab Two'),
]),
),
),
];
},
),
);
}
}

Related

Flutter large sliverappbar not working with tabs

I have a SliverAppBar.large and Tab inside NestedScrollView. However the app bar not displaying properly. Title in app bar hides behind Tabs and a padding above appbar title is present while scrolling. Is this a bug ?
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({super.key});
#override
Widget build(BuildContext context) {
final List<String> tabs = <String>['Tab 1', 'Tab 2'];
return DefaultTabController(
length: tabs.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
leading: const Icon(Icons.arrow_back),
title: const Text('Books'),
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: tabs.map((name) => Tab(text: name)).toList(),
),
),
),
];
},
body: TabBarView(
children: tabs.map((name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}

Flutter NestedScrollView with TabBarView scrolls way too much when the body content is less

Here is the scenario -
Need TabBarView as user can swipe to change the screen.
Want to load more items when user scrolls to the bottom of the screen.
The first code is the NestedScrollView with TabBarView which has two tabs containing listview with 4 items. Even though the body height is less than screen height the body scrolls. I understand the default height is set to view port height but if I want achieve point number 2, I cant since the scroll is way too much. Is there a way to wrap the body to the height of the content?
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested Scroll Demo with TabBarView',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: NestedScrollViewTest(),
);
}
}
class NestedScrollViewTest extends StatelessWidget {
const NestedScrollViewTest({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var _tabs = ["One", "Two"];
return Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
forceElevated: true,
elevation: 2.0,
primary: true,
pinned: true,
stretch: true,
backgroundColor: Colors.white,
expandedHeight: 500,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
centerTitle: true,
background: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 4,
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Colors.red,
),
),
),
SizedBox(
height: 5,
),
Expanded(
flex: 5,
child: Container(
color: Colors.amber,
),
),
SizedBox(
height: 5,
)
],
),
),
),
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
// shrinkWrap: true, // even with this it is not working.
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 4,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}
In the second code, I am using a CustomScrollView instead. Here since there is no SliverTabBarView, I am using a SliverFillRemaining widget to wrap the TabBarView and place it in the CustomScrollView. Even here the body scrolls way too much since SliverFillRemaining default height is view port height. Without using the TabBarView the CustomScrollView wraps the body based on the height of the content but I need TabBarView.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested Scroll Demo with TabBarView',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CustomScrollViewTest(),
);
}
}
class CustomScrollViewTest extends StatelessWidget {
const CustomScrollViewTest({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var _tabs = ["One", "Two"];
return Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: CustomScrollView(
slivers: [
SliverAppBar(
forceElevated: true,
elevation: 2.0,
primary: true,
pinned: true,
stretch: true,
backgroundColor: Colors.white,
expandedHeight: 500,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
centerTitle: true,
background: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 4,
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Colors.red,
),
),
),
SizedBox(
height: 5,
),
Expanded(
flex: 5,
child: Container(
color: Colors.amber,
),
),
SizedBox(
height: 5,
)
],
),
),
),
SliverFillRemaining(
// hasScrollBody: false,
child: TabBarView(
children: _tabs.map((String name) {
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
itemCount: 4,
);
}).toList(),
),
)
],
),
),
);
}
}
Steps I have tried,
If I change the property hasScrollBody: false in SliverFillRemaining, I get the error -
RenderViewport does not support returning intrinsic dimensions.
If I use SliverToBoxAdapter instead of SliverFillRemaining then I get this error since TabBarView height is dependent on the parent.
Horizontal viewport was given unbounded height.
Is there a way to wrap the content based on the body height keeping TabBarView in mind.
Edit: adding images -
Initial
start scroll
end scroll

How to stack Circle Avatar over SliverAppBar and SliverList?

I want to achieve the above design
I did somewhat like this using slivers
import 'package:flutter/material.dart';
class ProfilePage extends StatefulWidget {
#override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: DefaultTabController(
length: 4,
child: NestedScrollView(
body: TabBarView(children: [
//pages of tabBar
]),
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
//for the pinned appBar
elevation: 0.0,
backgroundColor: Colors.red,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(backgroundColor: Colors.red.withOpacity(0.4),
child: Icon(Icons.arrow_back,color: Colors.white,),
),
),
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
"https://images.pexels.com/photos/396547/pexels-photo-396547.jpeg?auto=compress&cs=tinysrgb&h=350",
fit: BoxFit.cover,
),
),
expandedHeight: 150,
),
SliverList(delegate: SliverChildListDelegate([
// for bio and other stuff
])),
SliverAppBar(
toolbarHeight: 0.0,
primary: false,
pinned: true,
bottom: TabBar(tabs: [
// tab bars
],),
)
];
},
),
),
),
);
}
}
I tried adding stack inside my sliver list with positioned but Circle Avatar gets display behind the app bar.
How can we achieve such a design or what could be an alternative way to achieve it.

Custom Scroll View getting scrolled under Sliver Persistent Header

DefaultTabController(
length: _subCategory.tabLength,
initialIndex: 0,
child:
NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (headerCtx, innnerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
backgroundColor: _productColor.backgroundColor,
pinned: true,
elevation: 0,
forceElevated: innnerBoxIsScrolled,
flexibleSpace: FlexibleSpaceBar(
title: Text("${_subCategory.currentSubCategoryName()}"),
background: Container(
margin: const EdgeInsets.only(
top: 4,
bottom: 50.0,
),
child: Hero(
tag: _subCategory.currentSubCategoryId(),
child: Image.asset(
'asset/images/grocery.jpeg',
),
),
),
),
),
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(headerCtx),
sliver: SliverPersistentHeader(
pinned: true,
delegate: _ProductTabSliver(
TabBar(
labelColor: Colors.white,
unselectedLabelColor: Colors.black87,
tabs: [
..._subCategory.currentTab().map(
(tabValue) {
return Tab(text: "${tabValue.fullName}");
},
).toList()
],
),
),
),
),
];
},
body:CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, pdIndex) {
final heightVisible =
_subCategory.advanceCompanyProductCount(pdIndex);
return ProductLayout();
},
childCount: _subCategory.differentProductCount(),
),
),
],
);,
));
CustomScrollList getting scrolled under sliverPersistentHeader Tab.
DefaultTabController
NestedScrollView
SliverAppBar
SliverPersistentHeader
-body: CustomScrollView
- slivers: SliverChildBuilderDelegate
SliverPersistentHeader had all the tabs displayed on the top(TabBar)
Body of Nested ScrollView is CustomScrollView which has SliverChildBuilderDelegate has a child.
On scrolling the list, my list scroll behind the tabs of sliver persistent header. Seems like sliverPersistentHeader is transparent and list scrolls can be seen behind.
To solve this problem, I had tried SliverOverlapInjector and SliverOverlapAbsorber, but that didn't help.
CustomScrollView scroll problem image is 4th for better understanding. Sunflower oil card on scrolling reaches behind the tab bar.
Images:
Sliver Overlap Absorber
Sliver Overlap Injector
Custom Scroll View
Overlapping Problem
class ProductAppBar extends StatelessWidget {
#override
Widget build(BuildContext context) {
return NestedScrollView(
physics: BouncingScrollPhysics(),
headerSliverBuilder: (headerCtx, innnerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
backgroundColor: _productColor.backgroundColor,
pinned: true,
elevation: 0,
forceElevated: innnerBoxIsScrolled,
flexibleSpace: FlexibleSpaceBar(
title: Text("${_subCategory.currentSubCategoryName()}"),
background: Container(
margin: const EdgeInsets.only(
top: 4,
bottom: 50.0,
),
child: Hero(
tag: _subCategory.currentSubCategoryId(),
child: Image.asset(
'asset/images/grocery.jpeg',
),
),
),
),
),
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(headerCtx),
sliver: SliverPersistentHeader(
pinned: true,
delegate: _ProductTabSliver(
TabBar(
onTap: (index) {
_subCategory.updateTabIndex(index);
},
labelColor: Colors.white,
unselectedLabelColor: Colors.black87,
tabs: [
..._subCategory.currentTab().map(
(tabValue) {
return Tab(text: "${tabValue.fullName}");
},
).toList()
],
),
),
),
),
];
},
body: TabBarView(
children: _subCategory.currentTab().map((tabElement) {
return ProductScreenLayout();
}).toList(),
),
);
}
}
class _ProductTabSliver extends SliverPersistentHeaderDelegate {
final TabBar _tabBar;
_ProductTabSliver(this._tabBar);
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final _productColor =
Provider.of<ColorConfig>(context, listen: false).randomProductColor();
return Container(
decoration: BoxDecoration(
color: _productColor.backgroundColor,
),
child: _tabBar);
}
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
Instead of returning just TabBar Widget from the SliverPersistentHeaderDelegate wrapping it with Container and setting backgroundColor solve my problem.
Inside class _ProductTabSliver build method I had wrapped the Container

Flutter: collapsing app bar with pinned tabBar

How add build collapsing app bar with pinned tabBar in Flutter like in this GIF
I managed to build it like so
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
floating: true,
pinned: true,
title: Text('Test'),
bottom: TabBar(
tabs: [
Tab( text: "Call"),
Tab( text: "Message"),
],
),
),
];
},
body: TabBarView(
children: [
Container(child: ListView.builder(
itemCount: 100,
itemBuilder: (context,index){
return Text("Item $index");
})),
Container(child: ListView.builder(
itemCount: 100,
itemBuilder: (context,index){
return Text("Item $index");
})),
],
),
),
),
);
}
}
You can use the SilverAppBar
SliverAppBar(
expandedHeight: 150.0,
floating:true,
pinned: true,
flexibleSpace: const FlexibleSpaceBar(
title: Text('Available seats'),
),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.add_circle),
tooltip: 'Add new entry',
onPressed: () { /* ... */ },
),
]
)
References
SliverAppBar Class
SliverAppBar Widget of the week
Keep the TabBarView seperate from the SliverAppBar.
Make it look continuous by setting SliverAppBar's elevation to 0 and use shadow or elevation for the TabBarView, whatever you prefer.