前言
首先呢,為什麼會有PageView巢狀PageView這個需求……
我們來看下抖音的互動:
從圖上不難分析出,首頁裡面放了2個tab,右邊的選單欄則是獨立存在的一個頁面
再加上巢狀滑動,所以實現方式就是,PageView裡面再巢狀一個佈局,首頁那塊無法就是在這個巢狀佈局中加入一個TabBarView就好了嘛,選單欄用狀態管理來更新選單內容,so easy~
然而事實證明我還是太年輕了……TabBarView其實就是PageView的擴充實現,然鵝,PageView是不支援巢狀滑動的……
解決方案
手勢監聽,給所有View加上動畫,當劃出選單的時候,用動畫方式移動底部的標籤欄
NestedScrollerView 加上PageView 的physics?
實現一個能支援巢狀滑動的PageView
當然,如果我採用第一種方案也就沒這篇文章了,其中方案調研的血淚史不提也罷。
首先需要了解的知識(課前準備)
Flutter中,提到巢狀滑動,自然第一想到的就是NestedScrollView,所以如果想給PageView加上巢狀滑動機制,學習下NestedScrollView及其核心原理能給我們很大的幫助。
Scrollable.dart 中的ScrollerController,ScrollerPosition等,瞭解其用途、基本含義和使用方法(當然,這篇文章需要的沒那麼多,要想理解,只需要看下ScrollerController、ScrollerPosition即可,那些beginActivity、ScrollActivity什麼的不明白也罷,這篇文章用不到,用ScrollPositionWithSingleContext複製黏貼過來的就行)。
萬里長征第一步,讓它巢狀滑動起來
做好課前準備之後,我們最簡單的描述一下 NestedScrollView 的巢狀滑動步驟:
建立_NestedScrollCoordinator ,同時建立2個ScrollController,用於管理整體的CunstomScrollerView和內部primary的可滑動佈局
通過 自定義的ScrollController ,返回自定義ScrollPosition,順便將Delegate 具體實現交給 _NestedScrollCoordinator來實現。
delegate 通過具體的 applyUserOffset 方法來控制整個列表的內容,順序是往上或者往左滑先滑外部,往下或往右滑先處理內部。
如果實現PageView的巢狀滑動,也可以採取這個思路。
一些小小坑
PageView本身是不支援primary的,所以如果想像NestedScrollerView那樣不需要給child傳入特定controller,直接用即可的話,就需要實現一個支援Primary的PageView;
PageView是不會保活的,所以如果拉到主PageView的第二頁,包含子PageView的第一頁就會dispose,因此丟失滑動狀態,再拉回來的時候自然展示的是第一頁,而不是巢狀滑動之後的最大頁。所以這塊需要保活處理一下下
核心程式碼
1class _ChildPagePosition extends ScrollPosition
2 implements PageMetrics, ScrollActivityDelegate {
3 _ChildPagePosition({
4 this.parentController,
5 ScrollPhysics physics,
6 ScrollContext context,
7 this.initialPage = 0,
8 bool keepPage = true,
9 double viewportFraction = 1.0,
10 double initialPixels = 0.0,
11 ScrollPosition oldPosition,
12 })
13 : assert(initialPage != null),
14 assert(keepPage != null),
15 assert(viewportFraction != null),
16 assert(viewportFraction > 0.0),
17 _viewportFraction = viewportFraction,
18 _pageToUseOnStartup = initialPage.toDouble(),
19 super(
20 physics: physics,
21 context: context,
22 keepScrollOffset: keepPage,
23 oldPosition: oldPosition,
24 ) {
25 // If oldPosition is not null, the superclass will first call absorb(),
26 // which may set _pixels and _activity.
27 if (pixels == null && initialPixels != null)
28 correctPixels(initialPixels);
29 if (activity == null)
30 goIdle();
31 assert(activity != null);
32 }
33
34 /// 中間一大堆無關方法略過
35
36 @override
37 void applyUserOffset(double delta) {
38 updateUserScrollDirection(
39 delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
40 final double newPixels = pixels -
41 physics.applyPhysicsToUserOffset(this, delta);
42 final double overScroll = physics.applyBoundaryConditions(this, newPixels);
43 if (overScroll == 0) {
44 setPixels(newPixels);
45 } else {
46 if(parentController!=null){
47 if(parentController.position is _PagePosition){
48 (parentController.position as _PagePosition).applyClampedDragUpdate(-overScroll);
49 }
50 }
51 print("觸發上級滑動");
52 }
53 }
54}
複製程式碼
Look Look 效果
後記
當然,支援巢狀滑動僅僅只是開始……
如圖所示,目前僅僅巢狀滑動了而已,鬆開手之後的physics效果,拉到一半再拉回去等操作也沒特殊處理,嘛,不過這是以後的事了……