I need to implement a sticky tab bar. The same tab bar can be of variable length, as different merchants can have different number of categories.
My current code for merchant page is as:
class _MerchantPageState extends State<MerchantPage>
with TickerProviderStateMixin {
ScrollController _scrollController = ScrollController();
double _scrollPosition = 0;
late TabController _tabController;
_scrollListener() {
setState(() {
_scrollPosition = _scrollController.position.pixels;
});
}
GridContainerData? _products;
GridContainerData _getProductData() {
return productGridContainerData;
}
@override
void initState() {
super.initState();
_products = _getProductData();
_scrollController.addListener(_scrollListener);
_tabController = TabController(length: 2, vsync: this);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
controller: _scrollController,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
MerchantSliverAppbar(
merchant: widget.merchant!,
tabController: _tabController,
),
];
},
body: Column(
children: [
TabBar(
controller: _tabController,
unselectedLabelColor: Colors.redAccent,
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.redAccent, Colors.orangeAccent],
),
borderRadius: BorderRadius.circular(50),
color: Colors.redAccent,
),
isScrollable: true,
tabs: [
Tab(
child: Align(
alignment: Alignment.center,
child: Text("Category 1"),
),
),
Tab(
child: Align(
alignment: Alignment.center,
child: Text("Category 2"),
),
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildProductFeed(),
_buildProductFeed(),
],
),
),
],
),
),
),
floatingActionButton: ChatButton(),
),
);
}
Widget _buildProductFeed() {
return Container(
padding: EdgeInsets.all(10),
child: GridContainer(
data: _products,
isScrollable: true,
),
);
}
}
I cannot implement the Tab bar in my MerchantSliverAppbar
because it has a background image an implementing a Tabbar in the bottom:
of the MerchantSliverAppbar
puts the tab bar on top of the image.
Here is the full code for MerchantSliverAppbar.
My one solution is:
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
controller: _tabController,
unselectedLabelColor: Colors.redAccent,
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.redAccent, Colors.orangeAccent],
),
borderRadius: BorderRadius.circular(50),
color: Colors.redAccent,
),
physics: NeverScrollableScrollPhysics(),
isScrollable: true,
tabs: [
Tab(
child: Align(
alignment: Alignment.center,
child: Text("Category 1"),
),
),
Tab(
child: Align(
alignment: Alignment.center,
child: Text("Category 2"),
),
),
],
),
),
pinned: true,
),
And in the same file:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar);
final TabBar _tabBar;
@override
double get minExtent => _tabBar.preferredSize.height;
@override
double get maxExtent => _tabBar.preferredSize.height;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
child: _tabBar,
);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
The issue with this implementation is:
- I cannot use the same app bar in different pages. I need to make it modular.
How can I solve the issue?
CodePudding user response:
As it turns out I didn't need any of those. Since I am using a CustomSliverAppbar called MerchantSliverAppbar
I wrapped it with a SliverOverlapAbsorber in the following way:
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: CustomColors.white,
body: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
sliver: MerchantSliverAppbar(merchant: widget.merchant!),
),
)
];
},
body: FeedSliderContainer(),
),
),
);
}
Notice the:
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
sliver: MerchantSliverAppbar(merchant: widget.merchant!),
),
)
According to the docs:
A sliver that wraps another, forcing its layout extent to be treated as overlap.
The rest was pretty similar to building a normal TabBar, just create a new stateful widget and put in the following:
List<String> categories = ['All', 'Women', 'Men', 'Kids', 'Gadgets', 'Shoes'];
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: categories.length);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
child: Builder(
builder: (BuildContext context) {
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
int index = _tabController.index;
setState(() {});
}
});
return Column(
children: <Widget>[
getTabBar(context, _tabController),
Expanded(
child: Container(
child: TabBarView(
controller: _tabController,
children: [
... data you wanna show
],
),
),
),
],
);
},
),
);
}
List<Widget> getTabs(TabController tabController) {
return categories.map((category) {
return Tab(
..customize the tabs
);
}).toList();
}
Widget getTabBar(BuildContext context, TabController tabController) {
return TabBar(
tabs: getTabs(tabController),
labelColor: CustomColors.primary,
unselectedLabelColor: CustomColors.secondary.withOpacity(0.5),
indicatorColor: Colors.transparent,
isScrollable: true,
padding: const EdgeInsets.all(3),
controller: tabController,
);
}
}