I'm getting a problem with my TabBar I want it to be scrollable (swipe left and right by gesture).
But it's not working, I have to click on the sub category name to view it but I want it to scroll right to go to the next sub category.
I set isScrollable: true, in the TabBar widget but still the same problem.
If there is any suggestion I'm here thank you :)
1st screen
2nd screen
class CategoryScreen extends StatefulWidget {
final CategoryModel categoryModel;
CategoryScreen({#required this.categoryModel});
#override
_CategoryScreenState createState() => _CategoryScreenState();
}
class _CategoryScreenState extends State<CategoryScreen>
with TickerProviderStateMixin {
int _tabIndex = 0;
#override
void initState() {
super.initState();
Provider.of<CategoryProvider>(context, listen: false)
.getSubCategoryList(context, widget.categoryModel.id.toString());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: ResponsiveHelper.isDesktop(context)
? PreferredSize(
child: MainAppBar(), preferredSize: Size.fromHeight(80))
: null,
body: Consumer<CategoryProvider>(
builder: (context, category, child) {
return category.subCategoryList != null
? Center(
child: Scrollbar(
child: Container(
width: 1170,
child: CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: [
SliverAppBar(
expandedHeight: 200,
toolbarHeight:
50 + MediaQuery.of(context).padding.top,
pinned: true,
floating: false,
backgroundColor: Theme.of(context).primaryColor,
leading: IconButton(
icon: Icon(Icons.chevron_left,
color: ColorResources.COLOR_WHITE),
onPressed: () => Navigator.pop(context)),
flexibleSpace: FlexibleSpaceBar(
title: Text(widget.categoryModel.name,
style: rubikMedium.copyWith(
fontSize: Dimensions.FONT_SIZE_LARGE,
color: Colors.white)),
titlePadding: EdgeInsets.only(
bottom: 54 +
(MediaQuery.of(context).padding.top / 2),
left: 50,
right: 50,
),
background: Container(
margin: EdgeInsets.only(bottom: 50),
child: FadeInImage.assetNetwork(
placeholder: Images.placeholder_rectangle,
image:
'${Provider.of<SplashProvider>(context, listen: false).baseUrls.categoryImageUrl}/${widget.categoryModel.image}',
fit: BoxFit.cover,
),
),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(30.0),
child: Container(
width: MediaQuery.of(context).size.width,
color: Theme.of(context).accentColor,
**child: TabBar(
controller: TabController(
initialIndex: _tabIndex,
length:
category.subCategoryList.length + 1,
vsync: this),
isScrollable: true,
unselectedLabelColor:
ColorResources.getGreyColor(context),
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.label,
indicatorColor:
Theme.of(context).primaryColor,
labelColor: Theme.of(context)
.textTheme
.bodyText1
.color,
tabs: _tabs(category),
onTap: (int index) {
_tabIndex = index;
if (index == 0) {
category.getCategoryProductList(context,
widget.categoryModel.id.toString());
} else {
category.getCategoryProductList(
context,
category.subCategoryList[index - 1].id
.toString());
}
},
),
),
),
),**
SliverToBoxAdapter(
child: category.categoryProductList != null
? category.categoryProductList.length > 0
? GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 5,
mainAxisSpacing: 5,
childAspectRatio: 4,
crossAxisCount: ResponsiveHelper
.isDesktop(context)
? 3
: ResponsiveHelper.isTab(
context)
? 2
: 1),
itemCount:
category.categoryProductList.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(
Dimensions.PADDING_SIZE_SMALL),
itemBuilder: (context, index) {
return ProductWidget(
product: category
.categoryProductList[index]);
},
)
: NoDataScreen()
: GridView.builder(
shrinkWrap: true,
itemCount: 10,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(
Dimensions.PADDING_SIZE_SMALL),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 5,
mainAxisSpacing: 5,
childAspectRatio: 4,
crossAxisCount:
ResponsiveHelper.isDesktop(context)
? 3
: ResponsiveHelper.isTab(context)
? 2
: 1,
),
itemBuilder: (context, index) {
return ProductShimmer(
isEnabled:
category.categoryProductList ==
null);
},
),
),
],
),
),
),
)
: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor)));
},
),
);
}
List<Tab> _tabs(CategoryProvider category) {
List<Tab> tabList = [];
tabList.add(Tab(text: 'Tout'));
category.subCategoryList
.forEach((subCategory) => tabList.add(Tab(text: subCategory.name)));
return tabList;
}
}
You need to use a NestedScrollView to use a Sliver with a TabBar
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: SliverTabExample(),
),
);
}
class SliverTabExample extends StatefulWidget {
const SliverTabExample({Key? key}) : super(key: key);
#override
_SliverTabExampleState createState() => _SliverTabExampleState();
}
class _SliverTabExampleState extends State<SliverTabExample>
with SingleTickerProviderStateMixin {
final _tabs = List.generate(10, (index) => 'Tab#${index + 1}');
late final TabController _tabCont;
#override
void initState() {
_tabCont = TabController(length: 10, vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (_, __) {
return [
SliverAppBar(
expandedHeight: 200,
pinned: true,
floating: false,
title: Text('TabBar Example'),
centerTitle: true,
bottom: TabBar(
controller: _tabCont,
isScrollable: true,
tabs: [
..._tabs.map(
(label) => Tab(
child: Text(label),
),
),
],
),
),
];
},
body: TabBarView(
controller: _tabCont,
children: [
..._tabs.map(
(label) => SamplePage(
label: label,
),
),
],
),
),
);
}
}
class SamplePage extends StatelessWidget {
const SamplePage({Key? key, required this.label}) : super(key: key);
final String label;
#override
Widget build(BuildContext context) {
return Container(
child: Center(child: Text('Page of $label')),
);
}
}
Related
I am using AutoRoute with AutoTabsRouter for navigation in my web app. I have a custom Drawer used for navigation. I want my current tab icon and text to be the primary color when on the selected index.
For mobile and tablet view, the drawer pops with every selection and when I open it again the current tab has the primary color as it should. On desktop view this works if I open the app in desktop view and do not resize the screen at all. If I do resize the screen at all, even if I'm still in desktop view after resizing, the selected does not work as expected and the current tab does not have the primary color.
Here is my code for the Drawer :
class CustomDrawer extends StatefulWidget {
final double width;
const CustomDrawer({
required this.width,
Key? key,
}) : super(key: key);
#override
State<CustomDrawer> createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
ScrollController scrollController = ScrollController();
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
#override
Widget build(context) {
final tabsRouter = context.tabsRouter;
return Drawer(
elevation: 0,
backgroundColor: lightThemeCardColor,
child: SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
DrawerHeader(
child: Column(
children: const [
SizedBox(
height: defaultMargin,
),
Icon(
Icons.architecture,
color: lightPrimaryColor,
size: 50,
),
Text(
'Drawer Header',
style: TextStyle(
fontSize: mobileTitleTextSize,
fontWeight: FontWeight.bold,
),
),
],
),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('H O M E'),
selected: tabsRouter.activeIndex == 0,
selectedColor: lightPrimaryColor,
onTap: () {
// Push to screen
tabsRouter.setActiveIndex(0);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
ListTile(
leading: const Icon(Icons.edit),
title: const Text('S E C O N D'),
selected: tabsRouter.activeIndex == 1,
selectedColor: lightPrimaryColor,
onTap: () {
// Push to screen
tabsRouter.setActiveIndex(1);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
ListTile(
leading: const Icon(Icons.check),
title: const Text('T H I R D'),
selected: tabsRouter.activeIndex == 2,
selectedColor: lightPrimaryColor,
onTap: () {
// Push to screen
tabsRouter.setActiveIndex(2);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('F O U R T H'),
selected: tabsRouter.activeIndex == 3,
selectedColor: lightPrimaryColor,
onTap: () {
//Push to screen
tabsRouter.setActiveIndex(3);
if (widget.width < 1100) {
Navigator.pop(context);
}
},
),
],
),
),
);
}
}
Here is my code for the desktop home page where CustomDrawer is the first item in body row:
class DesktopHomePage extends StatelessWidget {
const DesktopHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: lightThemeScaffoldColor,
body: Row(
children: [
// Always open drawer
CustomDrawer(width: width),
// Body contents
Expanded(
child: Container(
margin: const EdgeInsets.all(largeMargin),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const MobileTabletTextHeadline(
text: 'Some widget',
),
SizedBox(
height: 300,
child: AspectRatio(
aspectRatio: 4,
child: SizedBox(
width: double.infinity,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 4,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.all(defaultPadding),
child: Container(color: lightThemeCardColor),
);
}),
),
),
),
),
// Some other widget
const MobileTabletTextHeadline(
text: 'Other Widget',
),
SizedBox(
height: 300,
child: AspectRatio(
aspectRatio: 4,
child: SizedBox(
width: double.infinity,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 4,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.all(defaultPadding),
child: Container(color: lightThemeCardColor),
);
}),
),
),
),
),
],
),
),
),
),
],
),
);
}
}
(All desktop views have the same layout)
And here is my code where I set up AutoTabsRouter:
class NavPage extends StatelessWidget {
const NavPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return AutoTabsRouter(
routes: const [
HomeRouter(),
SecondRouter(),
ThirdRouter(),
FourthRouter(),
],
builder: (context, child, animation) {
return Scaffold(
appBar:
MediaQuery.of(context).size.width < 1100 ? mobileAppBar : null,
backgroundColor: lightThemeScaffoldColor,
body: child,
drawer: CustomDrawer(width: width),
);
},
);
}
}
I've tried using using setState after setting the current index but that does not help. What can I change the Drawer in a way that reflects the changes in desktop view after window resize?
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?
I have a tab bar with a PageView inside each TabBarView. Each PageView has a ListView and when I scroll it, how can I scroll from the Scaffold's SingleChildScrollView rather than just scrolling the ListView inside the TabBarView?
Currently, the tab bar stays on the same position when I scroll the TabBarView, which looks terrible. How can I scroll individual TabBarView from SingleChildScrollView?
I tried tweaking with physics, but didn't turn out the way I wanted it to.
IMAGE :
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
int currIndex = 0;
TabController _tabController;
#override
void initState() {
super.initState();
_tabController =
TabController(vsync: this, length: 3, initialIndex: currIndex);
_tabController.addListener(() {
_handleTabSelection();
});
}
void _handleTabSelection() {
setState(() {
currIndex = _tabController.index == null ? 0 : _tabController.index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: DefaultTabController(
length: 3,
initialIndex: 0,
child: SingleChildScrollView(
child: Container(
height: MediaQuery.of(context).size.height,
child: Column(
children: [
Container(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Container(
child: Text(
"HOME",
style: TextStyle(fontSize: 25),
)),
),
_buildTabBar(context),
],
),
),
Expanded(
child: _buildTabBarView(
context,
),
)
],
),
),
),
),
),
);
}
TabBarView _buildTabBarView(BuildContext context) {
return TabBarView(
controller: _tabController,
children: List.generate(
3,
(index) => Container(
color: Colors.red,
child: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 60,
color: Colors.blue,
child: Center(child: Text("$index"))),
);
},
),
)));
}
Widget _buildTabBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8),
child: Container(
height: 45,
width: double.infinity,
decoration: buildTabBarStyle(),
child: TabBar(
controller: _tabController,
isScrollable: false,
tabs: List.generate(
3,
(index) => Center(
child: Text(
"$index",
style: TextStyle(color: Colors.black),
),
)),
),
),
);
}
BoxDecoration buildTabBarStyle() {
return BoxDecoration(
color: Color.fromARGB(255, 230, 248, 255),
border: Border.all(
width: 1,
color: Colors.black,
),
borderRadius: BorderRadius.all(Radius.circular(10)),
);
}
}
Here is a different approach to do it simply.
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
late final TabController controller;
#override
void initState() {
super.initState();
controller = TabController(length: 3, vsync: this);
}
BoxDecoration buildTabBarStyle() {
return BoxDecoration(
color: Color.fromARGB(255, 230, 248, 255),
border: Border.all(
width: 1,
color: Colors.black,
),
borderRadius: BorderRadius.all(Radius.circular(10)),
);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
snap: true,
floating: true,
pinned: false,
toolbarHeight: 80,
title: Text("Home"),
// title: Search(),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size(0.0, 48.0),
child: Container(
decoration: buildTabBarStyle(),
alignment: Alignment.center,
width: double.infinity,
child: TabBar(
controller: controller,
isScrollable: true,
labelColor: Colors.green,
unselectedLabelColor: Colors.grey,
labelStyle:
TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
tabs: [
Tab(text: "1"),
Tab(text: "2"),
Tab(text: "3"),
],
),
),
),
),
],
body: TabBarView(
controller: controller,
children: [
...List.generate(
3,
(t) => Container(
color: Colors.red,
child: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 60,
color: Colors.blue,
child: Center(
child: Text("tab $t index $index"),
),
),
);
},
),
),
).toList(),
],
),
),
),
);
}
}
I need to display categories in a scroll with 3X3 in grid view and it was working fine and slide also working fine but i cant able to achieve the dots for the scrolling .. I need like carousal . Is it possible to add dots with list view
SingleChildScrollView(
child: GridView.count(
physics: ScrollPhysics(),
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
primary: false,
childAspectRatio: 1.1,
shrinkWrap: true,
crossAxisSpacing: 0,
mainAxisSpacing: 0,
crossAxisCount: 4,
// mainAxisCount:2,
//scrollDirection: Axis.horizontal,
children: List.generate(categoryData.length, (index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductCategoryPage(
categoryId: categoryData[index].id,
categoryName:
categoryData[index].name)));
},
child: Column(children: [
buildCacheNetworkImage(
width: 40,
height: 40,
url: categoryData[index].image,
plColor: Colors.transparent),
Flexible(
child: Container(
margin: EdgeInsets.fromLTRB(0, 10, 0, 0),
child: Text(
categoryData[index].name,
style: TextStyle(
color: CHARCOAL,
fontWeight: FontWeight.normal,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
)
]));
}),
),
)
I need to get the scrolling dots in the list view so how to achieve that
You should implement below way
Code :
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> list = [];
int perPageItem = 16;
int pageCount;
int selectedIndex = 0;
int lastPageItemLength;
PageController pageController;
#override
void initState() {
pageController = PageController(initialPage: 0);
for (int i = 1; i <= 45; i++) {
list.add('$i');
}
var num = (list.length / perPageItem);
pageCount = num.isInt ? num.toInt() : num.toInt() + 1;
var reminder = list.length.remainder(perPageItem);
lastPageItemLength = reminder == 0 ? perPageItem : reminder;
super.initState();
}
#override
void dispose() {
pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
SizedBox(
height: 390,
child: PageView.builder(
controller: pageController,
itemCount: pageCount,
onPageChanged: (index) {
setState(() {
selectedIndex = index;
});
},
itemBuilder: (_, pageIndex) {
return GridView.count(
physics: NeverScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
primary: false,
childAspectRatio: 1.1,
shrinkWrap: true,
crossAxisSpacing: 0,
mainAxisSpacing: 0,
crossAxisCount: 4,
children: List.generate(
(pageCount - 1) != pageIndex
? perPageItem
: lastPageItemLength, (index) {
return GestureDetector(
onTap: () {},
child: Container(
width: 50,
height: 50,
margin: const EdgeInsets.all(5),
color: Colors.amber,
alignment: Alignment.center,
child: Text(
list[index + (pageIndex * perPageItem)],
style: TextStyle(color: Colors.black, fontSize: 20),
),
),
);
}),
);
}),
),
SizedBox(
height: 15,
child: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: pageCount,
itemBuilder: (_, index) {
return GestureDetector(
onTap: () {
pageController.animateToPage(index, duration: Duration(milliseconds: 500), curve: Curves.easeInOut);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 100),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)),
color: Colors.red
.withOpacity(selectedIndex == index ? 1 : 0.5)),
margin: EdgeInsets.all(5),
width: 10,
height: 10,
),
);
},
),
),
],
),
);
}
}
extension NumExtensions on num {
bool get isInt => (this % 1) == 0;
}
Output :
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