Related
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,
),
),
],
);
}
}
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,
),
);
}
}
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,
),
),
],
),
),
);
}
}
I have created the Search bar, I need to add a functionality to it so that whenever the user scrolls down, the search bar turns into an icon in the appbar which can be clicked again to expand.
This is the appbar container
Container(
height: 122,
color: AppColors.kDefaultPink,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
//Location text
SizedBox(height: 10.0,),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.location_on,
color: Colors.white,
),
SizedBox(width: 12.0),
Text("Delhi NCR",style: TextStyle(color: Colors.white, fontSize: 18.0),),
],
),
SizedBox(height: 20.0,),
//SearchBOX
SearchBox(onChanged: (value) {}),
],
),
),
),
This is the code for the search bar that would be present in the appbar
import 'package:flutter/material.dart';
import 'package:zattireuserapp/views/AppColors.dart';
class SearchBox extends StatelessWidget {
final ValueChanged onChanged;
const SearchBox({Key key, this.onChanged}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Container(
width: 390,
height: 45,
margin: EdgeInsets.symmetric(horizontal: 22.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(1),
borderRadius: BorderRadius.circular(12),
),
child: TextField(
onChanged: onChanged,
style: TextStyle(color: AppColors.blackColor),
decoration: InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
prefixIcon: Icon(Icons.search),
hintText: 'Search for anything',
hintStyle: TextStyle(fontSize: 15),
),
),
),
);
}
}
You can use LayoutBuilder to get sliver AppBar biggest height. When biggest.height = 80.0, it actually means sliver appbar is collapsed.
Here is a some example:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(
home: MyApp(),
));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var top = 0.0;
ScrollController _scrollController;
#override
void initState() {
_scrollController = ScrollController(keepScrollOffset: true);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: true,
flexibleSpace: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
top = constraints.biggest.height;
return FlexibleSpaceBar(
centerTitle: true,
title: top >= 80 ? TextField() : IconButton(icon: Icon(Icons.search), onPressed: () => _scrollController.jumpTo(0)) // condition
);
})),
];
},body: ListView.builder(
itemCount: 100,
itemBuilder: (context,index){
return Text("List Item: " + index.toString());
},
),
));
}
}
I'm sorry for messy code but, I hope you get the idea
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