Skip to content

Instantly share code, notes, and snippets.

@ltvu93
Last active April 12, 2021 10:57
Show Gist options
  • Save ltvu93/65ecabaee8091f34d34bb73125aa1761 to your computer and use it in GitHub Desktop.
Save ltvu93/65ecabaee8091f34d34bb73125aa1761 to your computer and use it in GitHub Desktop.
Tab bar with section scroll
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class VisibleInfo {
final int index;
final double visiblePercent;
VisibleInfo({this.index, this.visiblePercent});
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
TabController _tabController;
ScrollController _scrollController;
final scrollViewKey = GlobalKey<ScrollableState>();
final section1Key = GlobalKey();
final section2Key = GlobalKey();
final section3Key = GlobalKey();
final section4Key = GlobalKey();
List<GlobalKey> sectionKeys;
@override
void initState() {
super.initState();
sectionKeys = [section1Key, section2Key, section3Key, section4Key];
_tabController = TabController(initialIndex: 0, length: 4, vsync: this);
_scrollController = ScrollController();
_scrollController.addListener(() {
_onScroll();
});
}
@override
void dispose() {
_tabController.dispose();
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
final visibleInfos = <VisibleInfo>[];
for (int index = 0; index < sectionKeys.length; index++) {
final sectionKey = sectionKeys[index];
final sectionRenderObject = sectionKey.currentContext.findRenderObject();
final viewport = RenderAbstractViewport.of(sectionRenderObject);
final double viewportHeight = viewport.paintBounds.height;
final sectionOffset =
viewport.getOffsetToReveal(sectionRenderObject, 0.0);
final sectionSize = sectionRenderObject?.semanticBounds?.size;
final viewPortTop = _scrollController.position.pixels;
final viewPortBottom = viewPortTop + viewportHeight;
final sectionTop = sectionOffset.offset;
final sectionBottom = sectionTop + sectionSize.height;
bool isInViewport;
double visiblePercent;
if (sectionTop < viewPortBottom && sectionBottom > viewPortTop) {
isInViewport = true;
final intersectTop = max(sectionTop, viewPortTop);
final intersectBottom = min(sectionBottom, viewPortBottom);
final intersectHeight = intersectBottom - intersectTop;
visiblePercent = intersectHeight / sectionSize.height;
visibleInfos.add(
VisibleInfo(
index: index,
visiblePercent: visiblePercent,
),
);
} else {
isInViewport = false;
visiblePercent = 0.0;
}
print(
'$index --> offset: ${sectionOffset.offset} -- VP?: $isInViewport visiblePercent: $visiblePercent');
}
if (visibleInfos.isNotEmpty) {
visibleInfos.sort(
(visibleInfo1, visibleInfo2) =>
visibleInfo2.visiblePercent.compareTo(visibleInfo1.visiblePercent),
);
_tabController.index = visibleInfos.first.index;
}
}
// https://gist.github.com/slightfoot/9ab7a861468780b9c155d941bf71c8f7
Future<void> scrollToAnchor(
GlobalKey sectionKey, {
duration = const Duration(milliseconds: 350),
curve = Curves.easeOutExpo,
}) {
final anchorBox = sectionKey.currentContext.findRenderObject() as RenderBox;
final scrollableBox = scrollViewKey.currentContext.findRenderObject();
final anchorOffset = anchorBox.localToGlobal(
Offset.zero,
ancestor: scrollableBox,
);
return _scrollController.position.animateTo(
_scrollController.position.pixels + anchorOffset.dy,
duration: duration,
curve: curve,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
TabBar(
controller: _tabController,
onTap: (index) {
scrollToAnchor(sectionKeys[index]);
},
tabs: [
Tab(
child: Text(
'Section 1',
style: TextStyle(
color: Colors.black,
),
),
),
Tab(
child: Text(
'Section 2',
style: TextStyle(
color: Colors.black,
),
),
),
Tab(
child: Text(
'Section 3',
style: TextStyle(
color: Colors.black,
),
),
),
Tab(
child: Text(
'Section 4',
style: TextStyle(
color: Colors.black,
),
),
),
],
),
SizedBox(height: 8.0),
Expanded(
child: SingleChildScrollView(
key: scrollViewKey,
controller: _scrollController,
child: Column(
children: [
Container(
key: section1Key,
width: double.infinity,
height: 500,
color: Colors.red,
child: Text('Section 1'),
),
SizedBox(height: 100),
Container(
key: section2Key,
width: double.infinity,
height: 500,
color: Colors.yellow,
child: Text('Section 2'),
),
SizedBox(height: 100),
Container(
key: section3Key,
width: double.infinity,
height: 500,
color: Colors.blue,
child: Text('Section 3'),
),
SizedBox(height: 100),
Container(
key: section4Key,
width: double.infinity,
height: 500,
color: Colors.orange,
child: Text('Section 4'),
),
SizedBox(height: 100),
],
),
),
),
],
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment