55 */
66
77import 'package:flutter/material.dart' ;
8+ import 'package:flutter/rendering.dart' ;
89import 'package:flutter_sticky_header/flutter_sticky_header.dart' ;
910import 'package:scrollview_observer/scrollview_observer.dart' ;
1011import 'package:scrollview_observer_example/utils/random.dart' ;
@@ -32,15 +33,23 @@ class _MultiSliverDemoPageState extends State<MultiSliverDemoPage> {
3233 final appBarKey = GlobalKey ();
3334
3435 final scrollController = ScrollController ();
35- late final SliverObserverController sliverObserverController;
36- Map <int , BuildContext > contextList = {};
36+ late final SliverObserverController sliverItemObserverController;
37+ // late final SliverObserverController sliverObserverController;
38+ Map <int , BuildContext > itemSliverIndexCtxMap = {};
39+ Map <int , BuildContext > sliverIndexCtxMap = {};
40+
41+ ValueNotifier <int > tabCurrentSelectedIndex = ValueNotifier (0 );
42+ bool isIgnoreCalcTabBarIndex = false ;
3743
3844 @override
3945 void initState () {
4046 super .initState ();
41- sliverObserverController = SliverObserverController (
47+ sliverItemObserverController = SliverObserverController (
4248 controller: scrollController,
4349 );
50+ // sliverObserverController = SliverObserverController(
51+ // controller: scrollController,
52+ // );
4453
4554 for (var i = 0 ; i < 4 ; i++ ) {
4655 final tag = 'Section ${i + 1 }' ;
@@ -54,26 +63,80 @@ class _MultiSliverDemoPageState extends State<MultiSliverDemoPage> {
5463
5564 @override
5665 Widget build (BuildContext context) {
66+ Widget resultWidget = _buildScrollView ();
67+ resultWidget = _buildSliverItemObserver (child: resultWidget);
68+ resultWidget = _buildSliverObserver (child: resultWidget);
5769 return Scaffold (
58- body: SliverViewObserver (
59- controller: sliverObserverController,
60- sliverContexts: () => contextList.values.toList (),
61- child: CustomScrollView (
62- controller: scrollController,
63- physics: const ClampingScrollPhysics (),
64- slivers: [
65- SliverAppBar (
70+ body: resultWidget,
71+ bottomNavigationBar: buildBottomNavigationBar (context),
72+ );
73+ }
74+
75+ /// To observe sliver items and handle scrollTo.
76+ Widget _buildSliverItemObserver ({
77+ required Widget child,
78+ }) {
79+ return SliverViewObserver (
80+ controller: sliverItemObserverController,
81+ sliverContexts: () => itemSliverIndexCtxMap.values.toList (),
82+ child: child,
83+ );
84+ }
85+
86+ /// To observe which sliver is currently the first.
87+ Widget _buildSliverObserver ({
88+ required Widget child,
89+ }) {
90+ return SliverViewObserver (
91+ // controller: sliverObserverController,
92+ child: child,
93+ sliverContexts: () => sliverIndexCtxMap.values.toList (),
94+ triggerOnObserveType: ObserverTriggerOnObserveType .directly,
95+ dynamicLeadingOffset: () {
96+ // Accumulate the height of all PersistentHeader.
97+ return ObserverUtils .calcPersistentHeaderExtent (
6698 key: appBarKey,
67- pinned: true ,
68- title: const Text ('Multi Sliver' ),
69- ),
70- ...List .generate (modelList.length, (mainIndex) {
71- return _buildSectionListView (mainIndex);
72- }),
73- ],
99+ offset: scrollController.offset,
100+ ) +
101+ 1 ; // To avoid tabBar index rebound.
102+ },
103+ onObserveViewport: (result) {
104+ if (isIgnoreCalcTabBarIndex) return ;
105+ int ? currentTabIndex;
106+ final currentFirstSliverCtx = result.firstChild.sliverContext;
107+ for (var sectionIndex in sliverIndexCtxMap.keys) {
108+ final ctx = sliverIndexCtxMap[sectionIndex];
109+ if (ctx == null ) continue ;
110+ // If they are not the same sliver, continue.
111+ if (currentFirstSliverCtx != ctx) continue ;
112+ // If the sliver is not visible, continue.
113+ final visible =
114+ (ctx.findRenderObject () as RenderSliver ).geometry? .visible ??
115+ false ;
116+ if (! visible) continue ;
117+ currentTabIndex = sectionIndex;
118+ break ;
119+ }
120+ if (currentTabIndex == null ) return ;
121+ updateTabBarIndex (currentTabIndex);
122+ },
123+ );
124+ }
125+
126+ Widget _buildScrollView () {
127+ return CustomScrollView (
128+ controller: scrollController,
129+ physics: const ClampingScrollPhysics (),
130+ slivers: [
131+ SliverAppBar (
132+ key: appBarKey,
133+ pinned: true ,
134+ title: const Text ('Multi Sliver' ),
74135 ),
75- ),
76- bottomNavigationBar: buildBottomNavigationBar (context),
136+ ...List .generate (modelList.length, (mainIndex) {
137+ return _buildSectionListView (mainIndex);
138+ }),
139+ ],
77140 );
78141 }
79142
@@ -85,9 +148,11 @@ class _MultiSliverDemoPageState extends State<MultiSliverDemoPage> {
85148 children: List .generate (modelList.length, (index) {
86149 return Expanded (
87150 child: InkWell (
88- onTap: () {
89- // sliverObserverController.jumpTo(
90- // sliverContext: contextList[index],
151+ onTap: () async {
152+ updateTabBarIndex (index);
153+ isIgnoreCalcTabBarIndex = true ;
154+ // await sliverItemObserverController.jumpTo(
155+ // sliverContext: itemSliverIndexCtxMap[index],
91156 // index: 0,
92157 // isFixedHeight: true,
93158 // offset: (offset) {
@@ -97,11 +162,11 @@ class _MultiSliverDemoPageState extends State<MultiSliverDemoPage> {
97162 // );
98163 // },
99164 // );
100- sliverObserverController .animateTo (
101- sliverContext: contextList [index],
165+ await sliverItemObserverController .animateTo (
166+ sliverContext: itemSliverIndexCtxMap [index],
102167 index: 0 ,
103168 isFixedHeight: true ,
104- duration: const Duration (seconds : 1 ),
169+ duration: const Duration (milliseconds : 200 ),
105170 curve: Curves .easeInOut,
106171 offset: (offset) {
107172 return ObserverUtils .calcPersistentHeaderExtent (
@@ -110,28 +175,35 @@ class _MultiSliverDemoPageState extends State<MultiSliverDemoPage> {
110175 );
111176 },
112177 );
178+ isIgnoreCalcTabBarIndex = false ;
113179 },
114- child: Container (
115- alignment: Alignment .center,
116- height: 40 ,
117- decoration: BoxDecoration (
118- border: Border .all (width: 0.5 ),
119- ),
120- child: Text (
121- modelList[index].tag,
122- ),
180+ child: ValueListenableBuilder (
181+ valueListenable: tabCurrentSelectedIndex,
182+ builder: (BuildContext context, int value, Widget ? child) {
183+ return Container (
184+ alignment: Alignment .center,
185+ height: 40 ,
186+ decoration: BoxDecoration (
187+ border: Border .all (width: 0.5 ),
188+ color: value == index ? Colors .amber : Colors .white,
189+ ),
190+ child: Text (
191+ modelList[index].tag,
192+ ),
193+ );
194+ },
123195 ),
124196 ),
125197 );
126198 }),
127199 ),
128- SizedBox (height: MediaQuery .of (context).padding .bottom),
200+ SizedBox (height: MediaQuery .paddingOf (context).bottom),
129201 ],
130202 );
131203 }
132204
133205 Widget _buildSectionListView (int mainIndex) {
134- return SliverStickyHeader (
206+ Widget resultWidget = SliverStickyHeader (
135207 header: Container (
136208 height: 40 ,
137209 color: Colors .white,
@@ -145,7 +217,8 @@ class _MultiSliverDemoPageState extends State<MultiSliverDemoPage> {
145217 itemExtent: 120 ,
146218 delegate: SliverChildBuilderDelegate (
147219 (context, index) {
148- contextList[mainIndex] = context;
220+ // Save the context of SliverList.
221+ itemSliverIndexCtxMap[mainIndex] = context;
149222 return Container (
150223 padding: const EdgeInsets .only (left: 12 ),
151224 color: RandomTool .color (),
@@ -159,5 +232,18 @@ class _MultiSliverDemoPageState extends State<MultiSliverDemoPage> {
159232 ),
160233 ),
161234 );
235+ resultWidget = SliverObserveContext (
236+ child: resultWidget,
237+ onObserve: (context) {
238+ // Save the context of the outermost sliver.
239+ sliverIndexCtxMap[mainIndex] = context;
240+ },
241+ );
242+ return resultWidget;
243+ }
244+
245+ updateTabBarIndex (int index) {
246+ if (index == tabCurrentSelectedIndex.value) return ;
247+ tabCurrentSelectedIndex.value = index;
162248 }
163249}
0 commit comments