scroll_sync_tabs 0.1.0
scroll_sync_tabs: ^0.1.0 copied to clipboard
Keep a TabBar and a scrollable list of sections in sync both ways - scrolling selects the tab, tapping a tab scrolls to the section.
import 'package:flutter/material.dart';
import 'package:scroll_sync_tabs/scroll_sync_tabs.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'scroll_sync_tabs',
theme: ThemeData(useMaterial3: true),
home: const MenuPage(),
);
}
class MenuSection {
const MenuSection(this.title, this.dishes);
final String title;
final List<String> dishes;
}
class MenuPage extends StatefulWidget {
const MenuPage({super.key});
@override
State<MenuPage> createState() => _MenuPageState();
}
class _MenuPageState extends State<MenuPage> with TickerProviderStateMixin {
static const double _tabStripHeight = 48;
static const double _pinnedHeaderExtent = kToolbarHeight + _tabStripHeight;
late final ScrollTabController<MenuSection> _controller;
@override
void initState() {
super.initState();
_controller = ScrollTabController<MenuSection>(
items: const [],
vsync: this,
pinnedHeaderExtent: _pinnedHeaderExtent,
);
_load();
}
Future<void> _load() async {
await Future<void>.delayed(const Duration(milliseconds: 600));
if (!mounted) {
return;
}
_controller.updateItems(const [
MenuSection('Popular', [
'Chè thái',
'Chè bưởi',
'Chè khúc bạch',
'Chè đậu xanh',
'Chè bà ba',
]),
MenuSection('Desserts', [
'Sticky rice',
'Flan',
'Mango sago',
'Banana cake',
'Coconut pudding',
]),
MenuSection('Drinks', [
'Iced tea',
'Coconut coffee',
'Soda chanh',
'Sinh tố xoài',
'Nước mía',
]),
MenuSection('Toppings', [
'Pearls',
'Jelly',
'Coconut milk',
'Red beans',
'Grass jelly',
]),
MenuSection('Combos', ['Family set', 'Duo set', 'Party platter']),
]);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
if (_controller.items.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return CustomScrollView(
controller: _controller.scrollController,
slivers: [
const SliverAppBar(
expandedHeight: 180,
pinned: true,
flexibleSpace: FlexibleSpaceBar(title: Text("Chè Chang's")),
),
ScrollTabBar.sliver<MenuSection>(
height: _tabStripHeight,
controller: _controller,
isScrollable: true,
tabAlignment: TabAlignment.start,
labelBuilder: (context, item, index) => Text(item.title),
),
SliverList.builder(
itemCount: _controller.items.length,
itemBuilder: (context, i) {
final section = _controller.items[i];
return ScrollTabAnchor(
controller: _controller,
index: i,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
section.title,
style: Theme.of(context).textTheme.titleLarge,
),
),
...section.dishes.map(
(d) => ListTile(
leading: const CircleAvatar(
child: Icon(Icons.restaurant),
),
title: Text(d),
),
),
const SizedBox(height: 24),
],
),
);
},
),
],
);
},
),
);
}
}