Issue when I try creating Sliver app bar and pageview in the same widget - flutter

I am facing issues when it comes to logic while I am working with my app
I want a sliver appBar with and a page view in the same widget when i
do that and assign a custom scroll view for each page of my pageviews I get problems but if I declared a sliver app bar on each page of the pages it works fine and at the same time I should not have a nested scroll view in my pageview widget now I don't think that I should write an app bar for each one of them when I could just write it my page view widget
any thoughts
this is my code
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: true,
bottom: false,
child:
NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
sliver:
SliverAppBar(
// toolbarHeight: 50,
backgroundColor: Color.fromRGBO(255, 255, 255, 1),
title: const Text(
'Partnerna',
style: TextStyle(
fontSize: 21,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.normal,
color: linerColorUp),
),
actions: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 0, horizontal: 10),
child: Container(
alignment: Alignment.centerRight,
// color: Colors.amber,
// width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: const [
CircelCntainerBackgroundWidget(
backGroundColor: buttonbackgroundcolor,
child: Padding(
padding: EdgeInsets.all(3),
child: FaIcon(
FontAwesomeIcons.squarePlus,
size: 18,
),
)),
SizedBox(
width: 20,
),
CircelCntainerBackgroundWidget(
backGroundColor: buttonbackgroundcolor,
child: Icon(
Icons.notification_add_rounded,
size: 21,
)),
SizedBox(
width: 20,
),
// SizedBox(width: 10,),
CircleAvatar(
radius: 14,
backgroundImage: NetworkImage(
"https://th.bing.com/th/id/OIP.2tWiaVWFJjvC1HhJQuTtCwHaHt?w=173&h=181&c=7&r=0&o=5&pid=1.7"),
),
],
),
),
)
],
// expandedHeight: 200,
floating: true,
pinned: false,
snap: true,
forceElevated: innerBoxIsScrolled,
elevation: 0,
),
)];
},
body:
PageView(
children: [
HomeScreen(),
ConnectScreen(),
ConnectRequestScreen(),
MessagScrenn(),
SettingScreen(),
],
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: onPageChange,
),
),),

try to make a custom Scaffold same as your code, to wrap each of the PageView children.
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: PageView(
children: [
Page1(),
Page2(),
Page3(),
]
),
);
}
}
class Page1 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return CustomScaffold(
body: (...)
);
}
}
class Page2 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return CustomScaffold(
body: (...)
);
}
}
class Page3 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return CustomScaffold(
body: (...)
);
}
}
class CustomScaffold extends StatefulWidget {
final Widget body;
const CustomScaffold({ Key? key, required this.body}) : super(key: key);
#override
_CustomScaffoldState createState() => _CustomScaffoldState();
}
class _CustomScaffoldState extends State<CustomScaffold> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled){
(...)
},
body: widget.body,
),
);
}
}

Related

Double scroll sections in row

I'm trying to achieve a scroll section containing a row which contains a list view and a details view. The goal is to have one scroll view that scroll both the list and the details view. From an UX perspective, both sections should always be seen, such as if one section is much larger than the other, the smaller section will always be seen.
I'm under the impression that this is the use case for NestedScrollView so ultimately what I'm after is something using it. I know it's possible to sync multiple controllers together programmatically, but I'm hoping for something simpler.
Like the following (this is just an example, my screen does not look anything like this):
The code below is where I got to. It throws an error that scrollController is used in multiple places. Another problem is that scrolling the left side till the end does not scroll the right side till the end.
Here is a full repro:
https://github.com/cedvdb/flutter_repros/tree/double_scroll
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
title: const Text('example'),
)
],
body: Row(
children: [
SizedBox(
width: 500,
child: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) => ListTile(
title: Text('tile $index'),
),
),
),
const SizedBox(
width: 20,
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
Container(height: 500, color: Colors.yellow),
Container(height: 500, color: Colors.orange),
Container(height: 500, color: Colors.blue),
Container(height: 500, color: Colors.yellow),
],
),
),
),
const SizedBox(
width: 20,
),
],
),
),
);
}
}
Try this one, it works for me.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
title: const Text('example'),
)
],
body: SingleChildScrollView (
child: Row(
crossAxisAlignment : CrossAxisAlignment.start,
children: [
SizedBox(
width: 500,
child: ListView.builder(
itemCount: 30,
shrinkWrap: true,
itemBuilder: (context, index) => ListTile(
title: Text('tile $index'),
),
),
),
const SizedBox(
width: 20,
),
Expanded(
// child: SingleChildScrollView(
child: Column(
children: [
Container(height: 500, color: Colors.yellow),
Container(height: 500, color: Colors.orange),
Container(height: 500, color: Colors.blue),
Container(height: 500, color: Colors.yellow),
],
// ),
),
),
const SizedBox(
width: 20,
),
],
),
),
),
);
}
}
So basically when you are using one scrollview with two scrollable widgets flutter can't decide how to draw the scrollbar. so if you disable scrollbar then your code will stop throwing error(if you are okay with no scrollbar it will scroll but no scrollbars will be displayed).
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
//this line added here!
scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
title: const Text('example'),
)
],
body: Row(
children: [
SizedBox(
width: 500,
child: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) => ListTile(
title: Text('tile $index'),
),
),
),
const SizedBox(
width: 20,
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
Container(height: 500, color: Colors.yellow),
Container(height: 500, color: Colors.orange),
Container(height: 500, color: Colors.blue),
Container(height: 500, color: Colors.yellow),
],
),
),
),
const SizedBox(
width: 20,
),
],
),
),
);
}
}
What i understood is that there are two scrolls and items from one point to items in the other and you need a method to move the second one on a event (scroll) but by the example having different size lines i asume it's some kind of cursor in a array thing; if that is what you need you only have to implement the exact movement you want, the example just scrolls to the place of the line you click
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Page_Home(),
),
);
}
}
class List_Element {
String title;
String content;
final GlobalKey _key;
List_Element(
this.title,
this.content,
) : this._key = GlobalKey();
GlobalKey get_key() => _key;
}
class Page_Home extends StatefulWidget {
const Page_Home({Key? key}) : super(key: key);
#override
_Page_Home createState() => _Page_Home();
}
class _Page_Home extends State<Page_Home> {
late List<List_Element> elements;
final scroll_view = ScrollController();
double _get_heigth(key) {
final size = key.currentContext!.size;
if (size != null) {
return size.height;
} else {
return 0;
}
}
Widget build_list(BuildContext ctx) {
return SingleChildScrollView(
child: Column(
children: List<Widget>.generate(elements.length, (i) {
return InkWell(
onTap: () {
double pos = 0;
for (int e = 0; e < i; e++) {
pos += _get_heigth(elements[e].get_key());
}
scroll_view.animateTo(
pos,
duration: const Duration(seconds: 1),
curve: Curves.easeIn,
);
print("Jump $i to $pos");
},
child: Container(
margin: EdgeInsets.only(top: 10),
color: Colors.blueGrey,
width: double.infinity,
height: 30,
child: Center(child: Text("Line ${elements[i].title}")),
));
}),
),
);
}
Widget build_element_view(BuildContext ctx) {
return SingleChildScrollView(
controller: scroll_view,
child: Column(
children: List<Widget>.generate(elements.length, (i) {
return Container(
margin: EdgeInsets.only(top: 10),
color: Colors.blueGrey,
width: double.infinity,
child: Text(elements[i].content),
key: elements[i].get_key(),
);
}),
),
);
}
#override
void dispose() {
scroll_view.dispose();
super.dispose();
}
#override
Widget build(BuildContext ctx) {
Size vsize = MediaQuery.of(ctx).size;
this.elements = List<List_Element>.generate(
30,
(i) => List_Element(
i.toString(),
i.toString() * Random().nextInt(300),
));
return ConstrainedBox(
constraints: BoxConstraints.tight(vsize),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
width: vsize.width * 0.4,
height: vsize.height,
child: this.build_list(ctx),
),
SizedBox(
width: vsize.width * 0.5,
height: vsize.height,
child: this.build_element_view(ctx),
),
],
),
);
}
}
I could not get the behavior I wanted (and I'm not even sure it makes sens anymore) so I made 2 scroll in the row:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController left = ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar.medium(
title: const Text('example'),
),
],
body: LayoutBuilder(builder: (context, constraints) {
return Row(
children: [
Container(
color: Colors.greenAccent,
height: constraints.maxHeight,
width: 400,
child: MyListView(controller: left),
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
Container(height: 500, color: Colors.yellow),
Container(height: 500, color: Colors.orange),
Container(height: 500, color: Colors.blue),
Container(height: 500, color: Colors.yellow),
],
),
),
),
],
);
}),
),
);
}
}
class MyListView extends StatelessWidget {
final ScrollController? controller;
const MyListView({Key? key, this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return CustomScrollView(
controller: controller,
slivers: [
SliverFixedExtentList(
itemExtent: 60,
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('tile $index'),
),
childCount: 30),
),
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text('secon list'),
),
),
SliverFixedExtentList(
itemExtent: 60,
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('tile $index'),
),
childCount: 30,
),
),
],
);
}
}

Using NestedScrollView how do I stop the body from scrolling under the headerSliverBuilder if the body still fits into the screen

I am creating a Sliver scroll but I noticed that even when the body is small enough to fit into the screen, the body still scrolls under the NestedScrollView header. My Ideal experience is that the SliverAppBar Collapse but every item in the body stays underneath it and does not slide under.
Here is a code snippet of this:
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: 3,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
elevation: 0,
title: const Text('HOME',),
backgroundColor: Colors.green,
pinned: true,
stretchTriggerOffset: 10,
onStretchTrigger: () async{
print('hello');
return;
},
automaticallyImplyLeading: false,
actions: const [
Padding(
padding: EdgeInsets.only(right: 30),
child: Icon(Icons.ac_unit),
)
],
expandedHeight: 120,
flexibleSpace: const FlexibleSpaceBar(
title: Text('Charity',style:
TextStyle(fontSize: 25,
fontWeight: FontWeight.bold,
color: Colors.white),),
stretchModes: [
StretchMode.fadeTitle
],
),
),
];
},
body: SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverPadding(
padding: EdgeInsets.only(left: 16, right: 16, top: 11),
sliver: SliverList(
delegate: SliverChildListDelegate(List.generate(
6,
(index) => Text('Tab One: $index'),
),
),
),
),
],
);
},
),
),
),
),
);
}
}
How can I make it such that if it is not small enough to fit into the page, it doesn't scroll under the header?

How can i add PageView widget In a Stack widget?

Im developing a quotes app as a beginner in flutter.I had used a stack widget to use both image and text in the center of my screen.Right now i want to add a pageview widget to scroll and land in the next page.Please suggest me where and how should i use the pageview widget?
class OverviewScreen extends StatefulWidget {
#override
_OverviewScreenState createState() => _OverviewScreenState();
}
class _OverviewScreenState extends State<OverviewScreen> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: Text("Hu R Rehman",
style: TextStyle(fontFamily: "MonteCarlo"),),
centerTitle: true,
leading: Icon(Icons.menu),
shape:RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16))
),
backgroundColor: Colors.transparent,
elevation: 0,
),
body:
Stack(
children:<Widget>[
Image(
image:AssetImage('Image/soc1.jpg'),
fit:BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
Align(alignment: Alignment.center,
child: Text(' This is my text '
,style: TextStyle(fontSize: 30.0,
fontFamily:"MonteCarlo",
color: Colors.white,
fontWeight: FontWeight.w700 ) )
)
],
)
),
);
}
}
Check this out if it solves your problem:
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key,}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: Center(
child: Stack(
alignment: Alignment.center,
children: [
PageView(
controller: _pageController,
children:[
Image(image: AssetImage('Location of your image')),
//Add more Image Widgets
]
),
const Text(
'Add you Text here',
),
],
),
),
);
}
}

Flutter Listview inside column not taking full height

Above one is requirement. Actually requirement is that first widget (Vertical List header with below list view should take full height) means if list view has 18 items. It should show 18 items. Then below that it should show horizontal scroll.
I tried in my way but due to column list view taking same height as other element due to which its not showing all item all the way. It takes half of height of screen and user need to scroll in that height itself.
Need your help to sort out it. As first listview should take full height and second widget should come below that.
Please see below code.
import 'dart:math';
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Flutter test",
style: TextStyle(color: Colors.black),
),
elevation: 5,
backgroundColor: Colors.white,
),
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ListHeader("Vertical List Header"),
VerticalList(),
ListHeader("Horizontal List Header"),
HorizontalList(),
ListHeader("Vertical List Header"),
VerticalList(),
ScrollUp()
],
),
);
}
}
class ScrollUp extends StatelessWidget {
const ScrollUp({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
child: Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
height: 50,
child: Center(
child: Text(
"Click to scroll up",
style: TextStyle(
fontSize: 18,
),
),
),
),
);
}
}
class ListHeader extends StatelessWidget {
final String title;
const ListHeader(String title) : title = title;
#override
Widget build(BuildContext context) {
return Text(
title,
style: TextStyle(fontSize: 18),
);
}
}
class HorizontalList extends StatelessWidget {
const HorizontalList({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: 30,
itemBuilder: (BuildContext context, int index) => Container(
margin:
const EdgeInsets.symmetric(vertical: 20, horizontal: 10),
width: 30,
height: 20,
color: Colors.grey,
)),
);
}
}
class VerticalList extends StatelessWidget {
const VerticalList({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemBuilder: (ctx, int) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
width: double.infinity,
height: 50,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
);
},
itemCount: 8,
),
);
}
}
Change the Column widget with a ListView widget.
And add shrinkWrap: true to its child ListView's'.
Remove the Expanded widget on both ListView.Builder
The horizontal ListView.Builder must have a Fixed height ( Link )
Add physics: NeverScrollableScrollPhysics() to the vertical ListView.Builder
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Flutter test",
style: TextStyle(color: Colors.black),
),
elevation: 5,
backgroundColor: Colors.white,
),
body: ListView(
children: [
ListHeader("Vertical List Header"),
VerticalList(),
ListHeader("Horizontal List Header"),
HorizontalList(),
ListHeader("Vertical List Header"),
VerticalList(),
ScrollUp()
],
),
);
}
}
class ScrollUp extends StatelessWidget {
const ScrollUp({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
child: Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
height: 50,
child: Center(
child: Text(
"Click to scroll up",
style: TextStyle(
fontSize: 18,
),
),
),
),
);
}
}
class ListHeader extends StatelessWidget {
final String title;
const ListHeader(String title) : title = title;
#override
Widget build(BuildContext context) {
return Text(
title,
style: TextStyle(fontSize: 18),
);
}
}
class HorizontalList extends StatelessWidget {
const HorizontalList({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
height: 400,
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: 30,
itemBuilder: (BuildContext context, int index) => Container(
margin:
const EdgeInsets.symmetric(vertical: 20, horizontal: 10),
width: 30,
height: 20,
color: Colors.grey,
)),
);
}
}
class VerticalList extends StatelessWidget {
const VerticalList({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (ctx, int) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
width: double.infinity,
height: 50,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
);
},
itemCount: 8,
);
}
}
This is my solution for same kind of problem.
import 'package:flutter/material.dart';
class MealDetailsScreen extends StatelessWidget {
Widget buildSectionTitle(BuildContext context, String title) {
return Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: Text(
title,
style: Theme.of(context).textTheme.bodyText1,
),
);
}
Widget buildContainer({Widget child}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(10)),
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(10),
height: 300,
width: 300,
child: child,
);
}
#override
Widget build(BuildContext context) {
final mealId = ModalRoute.of(context).settings.arguments;
final selectedMeal =
DUMMY_MEALS.firstWhere((element) => element.id == mealId);
return Scaffold(
appBar: AppBar(
title: Text('${selectedMeal.title}'),
),
body: SingleChildScrollView(
child: Column(
children: [
Container(
height: 300,
width: double.infinity,
child: Image.network(
selectedMeal.imageUrl,
fit: BoxFit.cover,
),
),
buildSectionTitle(context, 'Ingredients'),
buildContainer(
child: ListView.builder(
itemBuilder: (ctx, index) => Card(
color: Theme.of(context).accentColor,
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
child: Text(selectedMeal.ingredients[index]),
),
),
itemCount: selectedMeal.ingredients.length,
),
),
buildSectionTitle(context, 'Steps'),
buildContainer(
child: ListView.builder(
itemBuilder: (ctx, index) => Column(
children: [
ListTile(
leading: CircleAvatar(
child: Text('# ${(index + 1)}'),
),
title: Text(selectedMeal.steps[index]),
),
Divider(),
],
),
itemCount: selectedMeal.steps.length,
),
),
],
),
),
);
}
}

How to stop pinned SliverAppBar from covering floating SliverPersistentHeader

I'm learning Flutter and I am currently trying to make a home page with a cool scrolling effect. I'm trying to implement a CustomScrollView with 3 elements: a SliverAppBar, a horizontal scrolling list and a SliverList. The first two were easy enough and after some struggling I managed to implement the horizontal scrolling list by using a SliverPersistentHeader.
However, I ran into an issue. I want the SliverAppBar to be pinned and the SliverPersistentHeader containing the horizontal scrolling list to be floating. Everything works fine, except the floating element gets covered by the pinned one when scrolling back up after scrolling down. I basically want the floating element to "know" there is another element above it and offset itself when scrolling up.
You can see the issue here, alongside my code:
https://dartpad.dev/32d3f2a890d4a676decb014744fcc9ba
Make sure you click and drag to scroll in order to see the issue!
How can I fix this? Is there anything I am missing that causes this issue?
Thank you for your time!
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
// I had to change this class to a StatefulWidget to be able to listen to the scroll event
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
// Here I declared the ScrollController for the CustomScrollView
ScrollController _controller;
// And here is a boolean to check when the user scrolls up or down the view
bool sliverPersistentHeader = false;
#override
void initState() {
super.initState();
// The ScrollController is initialized in the initState and listens for when the user starts scrolling up and changes the boolean value accordingly
_controller = ScrollController();
_controller.addListener(() {
if (_controller.position.userScrollDirection == ScrollDirection.reverse) {
setState(() {
sliverPersistentHeader = false;
});
} else {
setState(() {
sliverPersistentHeader = true;
});
}
});
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _controller,
slivers: <Widget>[
SliverAppBar(
floating: true,
pinned: true,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text('App Title'),
),
),
SliverPersistentHeader(
// The SliverPersisitentHeader checks the boolean value and either pins or unpins the the Header
pinned: sliverPersistentHeader ? true : false,
delegate: CustomSliver(
expandedHeight: 150.0,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, index) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: Container(
height: 50.0,
color: Colors.amber,
),
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab1'),
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab2'),
),
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Tab3'))
],
currentIndex: 0,
),
);
}
}
class CustomSliver extends SliverPersistentHeaderDelegate {
final double expandedHeight;
CustomSliver({#required this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Scrollbar(
child: Container(
color: Theme.of(context).canvasColor,
padding: EdgeInsets.fromLTRB(10.0, 15.0, 0, 5.0),
child: ListView.separated(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.only(right: 10.0, top: 10.0, bottom: 10.0),
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(20.0)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.16),
offset: Offset(0, 3.0),
blurRadius: 6.0),
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.navigation),
Text(
'Category',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
],
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(width: 5.0);
},
)),
);
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => 150.0;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
The only thing I didn't do was to animate the SliverPersistentHeader into view, hopefully, you can achieve this yourself. I'm sure there are other ways to achieve this, but this solution should work for you.
For anyone that is still looking for a solution. You can try to implement this using a NestedScrollView and SliverOverlapAbsorber.
The following code will demonstrate this
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _HomeState();
}
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, isScrolled) => [
SliverAppBar(
pinned: true,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text('App Title'),
),
),
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverPersistentHeader(
floating: true,
delegate: CustomSliver(
expandedHeight: 150.0,
),
),
),
],
body: ListView.builder(
itemCount: 100,
itemBuilder: (_, index) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: Container(
height: 50.0,
color: Colors.amber,
),
),
),
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab1'),
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Tab2'),
),
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Tab3'))
],
currentIndex: 0,
),
);
}
}
class CustomSliver extends SliverPersistentHeaderDelegate {
final double expandedHeight;
CustomSliver({required this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Scrollbar(
child: Container(
color: Theme.of(context).canvasColor,
padding: EdgeInsets.fromLTRB(10.0, 15.0, 0, 5.0),
child: ListView.separated(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.only(right: 10.0, top: 10.0, bottom: 10.0),
child: Container(
width: 100,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(20.0)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.16),
offset: Offset(0, 3.0),
blurRadius: 6.0),
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.navigation),
Text(
'Category',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
],
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(width: 5.0);
},
)),
);
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => 150.0;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
Notice the floatHeaderSlivers: true. On the NestedScrollView widget.
Notice the SliverOverlapAbsorber
Some resources to help
https://github.com/flutter/flutter/issues/62194#issuecomment-664625589
https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html
https://api.flutter.dev/flutter/widgets/SliverOverlapAbsorber-class.html