Reputation: 487
Basically what I want to accomplish here is to have a horizontal list of categories(could be TabBar) and a vertical list of the categories with items list in each category.
And when you click on a category it should scroll to a position in the vertical list of categories. And when you scroll the vertical list, the active category at the top should also update the current active category.
I've seen this function in the FoodPanda app but I am not able to replicate it.
Any suggestions on how can I accomplish this functionality?
Here's the screenshot of the UI.
Upvotes: 5
Views: 6303
Reputation: 775
I have tried several methods but this seems to be best for me.
Packages:
scroll_to_index: ^2.0.0
rect_getter: ^1.0.0
Usage of rect_getter
is inspired by: https://gist.github.com/debuggerx01/49f108d68ed903458e9478b4f0c186f4
Code (Food Panda Clone):
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:food_panda_sticky_header/colors.dart';
import 'package:food_panda_sticky_header/example_data.dart';
import 'package:food_panda_sticky_header/widgets/widgets.dart';
import 'package:rect_getter/rect_getter.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
bool isCollapsed = false;
late AutoScrollController scrollController;
late TabController tabController;
final double expandedHeight = 500.0;
final PageData data = ExampleData.data;
final double collapsedHeight = kToolbarHeight;
final listViewKey = RectGetter.createGlobalKey();
Map<int, dynamic> itemKeys = {};
// prevent animate when press on tab bar
bool pauseRectGetterIndex = false;
@override
void initState() {
tabController = TabController(length: data.categories.length, vsync: this);
scrollController = AutoScrollController();
super.initState();
}
@override
void dispose() {
scrollController.dispose();
tabController.dispose();
super.dispose();
}
List<int> getVisibleItemsIndex() {
Rect? rect = RectGetter.getRectFromKey(listViewKey);
List<int> items = [];
if (rect == null) return items;
itemKeys.forEach((index, key) {
Rect? itemRect = RectGetter.getRectFromKey(key);
if (itemRect == null) return;
if (itemRect.top > rect.bottom) return;
if (itemRect.bottom < rect.top) return;
items.add(index);
});
return items;
}
void onCollapsed(bool value) {
if (this.isCollapsed == value) return;
setState(() => this.isCollapsed = value);
}
bool onScrollNotification(ScrollNotification notification) {
if (pauseRectGetterIndex) return false;
int lastTabIndex = tabController.length - 1;
List<int> visibleItems = getVisibleItemsIndex();
bool reachLastTabIndex = visibleItems.length <= 2 && visibleItems.last == lastTabIndex;
if (reachLastTabIndex) {
tabController.animateTo(lastTabIndex);
} else {
int sumIndex = visibleItems.reduce((value, element) => value + element);
int middleIndex = sumIndex ~/ visibleItems.length;
if (tabController.index != middleIndex) tabController.animateTo(middleIndex);
}
return false;
}
void animateAndScrollTo(int index) {
pauseRectGetterIndex = true;
tabController.animateTo(index);
scrollController
.scrollToIndex(index, preferPosition: AutoScrollPosition.begin)
.then((value) => pauseRectGetterIndex = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: scheme.background,
body: RectGetter(
key: listViewKey,
child: NotificationListener<ScrollNotification>(
child: buildSliverScrollView(),
onNotification: onScrollNotification,
),
),
);
}
Widget buildSliverScrollView() {
return CustomScrollView(
physics: const ClampingScrollPhysics(),
controller: scrollController,
slivers: [
buildAppBar(),
buildBody(),
],
);
}
SliverAppBar buildAppBar() {
return FAppBar(
data: data,
context: context,
scrollController: scrollController,
expandedHeight: expandedHeight,
collapsedHeight: collapsedHeight,
isCollapsed: isCollapsed,
onCollapsed: onCollapsed,
tabController: tabController,
onTap: (index) => animateAndScrollTo(index),
);
}
SliverList buildBody() {
return SliverList(
delegate: SliverChildListDelegate(
List.generate(data.categories.length, (index) {
itemKeys[index] = RectGetter.createGlobalKey();
return buildCategoryItem(index);
}),
),
);
}
Widget buildCategoryItem(int index) {
Category category = data.categories[index];
return RectGetter(
key: itemKeys[index],
child: AutoScrollTag(
key: ValueKey(index),
index: index,
controller: scrollController,
child: CategorySection(category: category),
),
);
}
}
Demo: https://github.com/theacheng/food_panda_sticky_header/pull/4
Upvotes: 3
Reputation: 54367
You can copy paste run full code below
You can use package https://pub.dev/packages/scrollable_list_tabview
code snippet
body: ScrollableListTabView(
tabHeight: 48,
bodyAnimationDuration: const Duration(milliseconds: 150),
tabAnimationCurve: Curves.easeOut,
tabAnimationDuration: const Duration(milliseconds: 200),
tabs: [
ScrollableListTab(
tab: ListTab(
label: Text('Vegetables'),
icon: Icon(Icons.group),
showIconOnList: false),
body: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (_, index) => ListTile(
leading: Container(
height: 40,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.grey),
alignment: Alignment.center,
child: Text(index.toString()),
),
title: Text('Vegetables element $index'),
),
)),
ScrollableListTab(
tab: ListTab(label: Text('Fruits'), icon: Icon(Icons.add)),
body: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
working demo
full code
import 'package:flutter/material.dart';
import 'package:scrollable_list_tabview/scrollable_list_tabview.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter ScrollableListTabView Example'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ScrollableListTabView(
tabHeight: 48,
bodyAnimationDuration: const Duration(milliseconds: 150),
tabAnimationCurve: Curves.easeOut,
tabAnimationDuration: const Duration(milliseconds: 200),
tabs: [
ScrollableListTab(
tab: ListTab(
label: Text('Vegetables'),
icon: Icon(Icons.group),
showIconOnList: false),
body: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (_, index) => ListTile(
leading: Container(
height: 40,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.grey),
alignment: Alignment.center,
child: Text(index.toString()),
),
title: Text('Vegetables element $index'),
),
)),
ScrollableListTab(
tab: ListTab(label: Text('Fruits'), icon: Icon(Icons.add)),
body: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (_, index) => ListTile(
leading: Container(
height: 40,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.grey),
alignment: Alignment.center,
child: Text(index.toString()),
),
title: Text('Fruits element $index'),
),
)),
ScrollableListTab(
tab: ListTab(label: Text('Meat'), icon: Icon(Icons.group)),
body: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (_, index) => ListTile(
leading: Container(
height: 40,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.grey),
alignment: Alignment.center,
child: Text(index.toString()),
),
title: Text('Meat element $index'),
),
)),
ScrollableListTab(
tab: ListTab(
label: Text('Herbs&Spices'), icon: Icon(Icons.subject)),
body: GridView.builder(
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
physics: NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (_, index) => Card(
child: Center(child: Text('Herbs&Spices element $index')),
),
)),
ScrollableListTab(
tab: ListTab(
label: Text('Egg'),
icon: Icon(Icons.subject),
showIconOnList: true),
body: GridView.builder(
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
physics: NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (_, index) => Card(
child: Center(child: Text('Egg element $index')),
),
)),
],
),
);
}
}
Upvotes: 6