如何创建一个与颤振中的小部件滚动同步的标签栏?
How to create a tabbar that sync with a widget scroll in flutter?
基本上我想要在这里完成的是有一个水平的类别列表(可以是 TabBar)和一个垂直的类别列表,每个类别中都有项目列表。
当你点击一个类别时,它应该滚动到类别垂直列表中的一个位置。
并且当你滚动垂直列表时,顶部的活动类别也应该更新当前活动类别。
我在 FoodPanda 应用程序中看到过此功能,但我无法复制它。
关于如何实现此功能有什么建议吗?
这是 UI 的屏幕截图。
您可以复制粘贴运行下面的完整代码
您可以使用包 https://pub.dev/packages/scrollable_list_tabview
代码片段
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(),
工作演示
完整代码
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')),
),
)),
],
),
);
}
}
我尝试了几种方法,但这似乎最适合我。
包:
scroll_to_index: ^2.0.0
rect_getter: ^1.0.0
rect_getter
的用法灵感来自:https://gist.github.com/debuggerx01/49f108d68ed903458e9478b4f0c186f4
代码(食物熊猫克隆):
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),
),
);
}
}
演示:
https://github.com/theacheng/food_panda_sticky_header/pull/4
基本上我想要在这里完成的是有一个水平的类别列表(可以是 TabBar)和一个垂直的类别列表,每个类别中都有项目列表。
当你点击一个类别时,它应该滚动到类别垂直列表中的一个位置。 并且当你滚动垂直列表时,顶部的活动类别也应该更新当前活动类别。
我在 FoodPanda 应用程序中看到过此功能,但我无法复制它。
关于如何实现此功能有什么建议吗?
这是 UI 的屏幕截图。
您可以复制粘贴运行下面的完整代码
您可以使用包 https://pub.dev/packages/scrollable_list_tabview
代码片段
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(),
工作演示
完整代码
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')),
),
)),
],
),
);
}
}
我尝试了几种方法,但这似乎最适合我。
包:
scroll_to_index: ^2.0.0
rect_getter: ^1.0.0
rect_getter
的用法灵感来自:https://gist.github.com/debuggerx01/49f108d68ed903458e9478b4f0c186f4
代码(食物熊猫克隆):
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),
),
);
}
}
演示: https://github.com/theacheng/food_panda_sticky_header/pull/4