diff --git a/example/lib/features/custom_scrollview/custom_scrollview_demo/multi_sliver_demo_page.dart b/example/lib/features/custom_scrollview/custom_scrollview_demo/multi_sliver_demo_page.dart index 5078133..9dc87a4 100644 --- a/example/lib/features/custom_scrollview/custom_scrollview_demo/multi_sliver_demo_page.dart +++ b/example/lib/features/custom_scrollview/custom_scrollview_demo/multi_sliver_demo_page.dart @@ -5,6 +5,7 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/random.dart'; @@ -32,15 +33,23 @@ class _MultiSliverDemoPageState extends State { final appBarKey = GlobalKey(); final scrollController = ScrollController(); - late final SliverObserverController sliverObserverController; - Map contextList = {}; + late final SliverObserverController sliverItemObserverController; + // late final SliverObserverController sliverObserverController; + Map itemSliverIndexCtxMap = {}; + Map sliverIndexCtxMap = {}; + + ValueNotifier tabCurrentSelectedIndex = ValueNotifier(0); + bool isIgnoreCalcTabBarIndex = false; @override void initState() { super.initState(); - sliverObserverController = SliverObserverController( + sliverItemObserverController = SliverObserverController( controller: scrollController, ); + // sliverObserverController = SliverObserverController( + // controller: scrollController, + // ); for (var i = 0; i < 4; i++) { final tag = 'Section ${i + 1}'; @@ -54,26 +63,80 @@ class _MultiSliverDemoPageState extends State { @override Widget build(BuildContext context) { + Widget resultWidget = _buildScrollView(); + resultWidget = _buildSliverItemObserver(child: resultWidget); + resultWidget = _buildSliverObserver(child: resultWidget); return Scaffold( - body: SliverViewObserver( - controller: sliverObserverController, - sliverContexts: () => contextList.values.toList(), - child: CustomScrollView( - controller: scrollController, - physics: const ClampingScrollPhysics(), - slivers: [ - SliverAppBar( + body: resultWidget, + bottomNavigationBar: buildBottomNavigationBar(context), + ); + } + + /// To observe sliver items and handle scrollTo. + Widget _buildSliverItemObserver({ + required Widget child, + }) { + return SliverViewObserver( + controller: sliverItemObserverController, + sliverContexts: () => itemSliverIndexCtxMap.values.toList(), + child: child, + ); + } + + /// To observe which sliver is currently the first. + Widget _buildSliverObserver({ + required Widget child, + }) { + return SliverViewObserver( + // controller: sliverObserverController, + child: child, + sliverContexts: () => sliverIndexCtxMap.values.toList(), + triggerOnObserveType: ObserverTriggerOnObserveType.directly, + dynamicLeadingOffset: () { + // Accumulate the height of all PersistentHeader. + return ObserverUtils.calcPersistentHeaderExtent( key: appBarKey, - pinned: true, - title: const Text('Multi Sliver'), - ), - ...List.generate(modelList.length, (mainIndex) { - return _buildSectionListView(mainIndex); - }), - ], + offset: scrollController.offset, + ) + + 1; // To avoid tabBar index rebound. + }, + onObserveViewport: (result) { + if (isIgnoreCalcTabBarIndex) return; + int? currentTabIndex; + final currentFirstSliverCtx = result.firstChild.sliverContext; + for (var sectionIndex in sliverIndexCtxMap.keys) { + final ctx = sliverIndexCtxMap[sectionIndex]; + if (ctx == null) continue; + // If they are not the same sliver, continue. + if (currentFirstSliverCtx != ctx) continue; + // If the sliver is not visible, continue. + final visible = + (ctx.findRenderObject() as RenderSliver).geometry?.visible ?? + false; + if (!visible) continue; + currentTabIndex = sectionIndex; + break; + } + if (currentTabIndex == null) return; + updateTabBarIndex(currentTabIndex); + }, + ); + } + + Widget _buildScrollView() { + return CustomScrollView( + controller: scrollController, + physics: const ClampingScrollPhysics(), + slivers: [ + SliverAppBar( + key: appBarKey, + pinned: true, + title: const Text('Multi Sliver'), ), - ), - bottomNavigationBar: buildBottomNavigationBar(context), + ...List.generate(modelList.length, (mainIndex) { + return _buildSectionListView(mainIndex); + }), + ], ); } @@ -85,9 +148,11 @@ class _MultiSliverDemoPageState extends State { children: List.generate(modelList.length, (index) { return Expanded( child: InkWell( - onTap: () { - // sliverObserverController.jumpTo( - // sliverContext: contextList[index], + onTap: () async { + updateTabBarIndex(index); + isIgnoreCalcTabBarIndex = true; + // await sliverItemObserverController.jumpTo( + // sliverContext: itemSliverIndexCtxMap[index], // index: 0, // isFixedHeight: true, // offset: (offset) { @@ -97,11 +162,11 @@ class _MultiSliverDemoPageState extends State { // ); // }, // ); - sliverObserverController.animateTo( - sliverContext: contextList[index], + await sliverItemObserverController.animateTo( + sliverContext: itemSliverIndexCtxMap[index], index: 0, isFixedHeight: true, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: (offset) { return ObserverUtils.calcPersistentHeaderExtent( @@ -110,28 +175,35 @@ class _MultiSliverDemoPageState extends State { ); }, ); + isIgnoreCalcTabBarIndex = false; }, - child: Container( - alignment: Alignment.center, - height: 40, - decoration: BoxDecoration( - border: Border.all(width: 0.5), - ), - child: Text( - modelList[index].tag, - ), + child: ValueListenableBuilder( + valueListenable: tabCurrentSelectedIndex, + builder: (BuildContext context, int value, Widget? child) { + return Container( + alignment: Alignment.center, + height: 40, + decoration: BoxDecoration( + border: Border.all(width: 0.5), + color: value == index ? Colors.amber : Colors.white, + ), + child: Text( + modelList[index].tag, + ), + ); + }, ), ), ); }), ), - SizedBox(height: MediaQuery.of(context).padding.bottom), + SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ); } Widget _buildSectionListView(int mainIndex) { - return SliverStickyHeader( + Widget resultWidget = SliverStickyHeader( header: Container( height: 40, color: Colors.white, @@ -145,7 +217,8 @@ class _MultiSliverDemoPageState extends State { itemExtent: 120, delegate: SliverChildBuilderDelegate( (context, index) { - contextList[mainIndex] = context; + // Save the context of SliverList. + itemSliverIndexCtxMap[mainIndex] = context; return Container( padding: const EdgeInsets.only(left: 12), color: RandomTool.color(), @@ -159,5 +232,18 @@ class _MultiSliverDemoPageState extends State { ), ), ); + resultWidget = SliverObserveContext( + child: resultWidget, + onObserve: (context) { + // Save the context of the outermost sliver. + sliverIndexCtxMap[mainIndex] = context; + }, + ); + return resultWidget; + } + + updateTabBarIndex(int index) { + if (index == tabCurrentSelectedIndex.value) return; + tabCurrentSelectedIndex.value = index; } } diff --git a/example/lib/features/home/home_page.dart b/example/lib/features/home/home_page.dart index 1ace420..cf60354 100644 --- a/example/lib/features/home/home_page.dart +++ b/example/lib/features/home/home_page.dart @@ -23,6 +23,7 @@ import 'package:scrollview_observer_example/features/listview/listview_dynamic_o import 'package:scrollview_observer_example/features/listview/listview_fixed_height_demo/listview_fixed_height_demo_page.dart'; import 'package:scrollview_observer_example/features/listview/sliver_list_demo/sliver_list_demo_page.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_demo/nested_scrollview_demo_page.dart'; +import 'package:scrollview_observer_example/features/pageview/pageview_demo/pageview_demo_page.dart'; import 'package:scrollview_observer_example/features/scene/anchor_demo/anchor_page.dart'; import 'package:scrollview_observer_example/features/scene/anchor_demo/anchor_waterfall_page.dart'; import 'package:scrollview_observer_example/features/scene/azlist_demo/azlist_page.dart'; @@ -189,6 +190,12 @@ class HomePage extends StatelessWidget { return const NestedScrollViewDemoPage(); }, ), + Tuple2( + "PageView", + () { + return const PageViewDemoPage(); + }, + ), Tuple2( "VideoList AutoPlay", () { diff --git a/example/lib/features/pageview/pageview_demo/pageview_demo_page.dart b/example/lib/features/pageview/pageview_demo/pageview_demo_page.dart new file mode 100644 index 0000000..2b8f530 --- /dev/null +++ b/example/lib/features/pageview/pageview_demo/pageview_demo_page.dart @@ -0,0 +1,152 @@ +/* + * @Author: LinXunFeng linxunfeng@yeah.net + * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer + * @Date: 2024-08-03 14:09:53 + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:scrollview_observer/scrollview_observer.dart'; + +class PageViewDemoPage extends StatefulWidget { + const PageViewDemoPage({Key? key}) : super(key: key); + + @override + State createState() => _PageViewDemoPageState(); +} + +class _PageViewDemoPageState extends State { + double offsetYDelta = 50; + + late PageController pageController; + + int pageItemCount = 10; + + List> pageItemOffsetYList = []; + + final observerController = ListObserverController(); + + @override + void initState() { + super.initState(); + pageController = PageController( + initialPage: 4, + viewportFraction: 0.9, + ); + pageItemOffsetYList = List.generate( + pageItemCount, + (index) { + return ValueNotifier(0); + }, + ); + + Future.delayed(const Duration(milliseconds: 100)).then((_) { + observerController.dispatchOnceObserve(); + }); + } + + @override + void dispose() { + pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("PageView"), + ), + body: Stack( + children: [ + _buildMap(), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: _buildPageView(), + ), + ], + ), + ); + } + + Widget _buildMap() { + Widget resultWidget = SizedBox( + width: 500, + height: 900, + child: Image.network( + 'https://img2.baidu.com/it/u=675935710,2689018786&fm=253&fmt=auto&app=138&f=JPG?w=1061&h=500', + fit: BoxFit.fitHeight, + ), + ); + resultWidget = Stack( + children: [ + resultWidget, + Positioned( + left: 0, + right: 0, + top: 0, + bottom: 0, + child: Container( + color: Colors.black38, + ), + ), + ], + ); + return resultWidget; + } + + Widget _buildPageView() { + Widget resultWidget = PageView.builder( + controller: pageController, + itemBuilder: (context, index) { + Widget itemWidget = Container( + decoration: BoxDecoration( + color: Colors.blue[100], + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Text("Page $index"), + ); + Widget resultWidget = ValueListenableBuilder( + valueListenable: pageItemOffsetYList[index], + builder: (BuildContext context, double offsetY, Widget? child) { + return Transform.translate( + offset: Offset(0, offsetY), + child: itemWidget, + ); + }, + ); + resultWidget = Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: resultWidget, + ); + return resultWidget; + }, + itemCount: pageItemCount, + ); + resultWidget = ListViewObserver( + controller: observerController, + child: resultWidget, + triggerOnObserveType: ObserverTriggerOnObserveType.directly, + onObserve: (resultModel) { + final displayingChildModelList = resultModel.displayingChildModelList; + for (var itemModel in displayingChildModelList) { + final itemIndex = itemModel.index; + final itemDisplayPercentage = itemModel.displayPercentage; + final offsetY = (1 - itemDisplayPercentage) * offsetYDelta; + pageItemOffsetYList[itemIndex].value = offsetY; + } + }, + customTargetRenderSliverType: (renderObj) { + return renderObj is RenderSliverFillViewport; + }, + ); + resultWidget = SizedBox( + height: 300, + child: resultWidget, + ); + return resultWidget; + } +}