[Flutter]從零開始實現一個巢狀滑動的PageView(一)

lwlizhe發表於2020-01-15

前言

首先呢,為什麼會有PageView巢狀PageView這個需求……

我們來看下抖音的互動:

抖音互動
抖音互動

從圖上不難分析出,首頁裡面放了2個tab,右邊的選單欄則是獨立存在的一個頁面

再加上巢狀滑動,所以實現方式就是,PageView裡面再巢狀一個佈局,首頁那塊無法就是在這個巢狀佈局中加入一個TabBarView就好了嘛,選單欄用狀態管理來更新選單內容,so easy~

然而事實證明我還是太年輕了……TabBarView其實就是PageView的擴充實現,然鵝,PageView是不支援巢狀滑動的……

解決方案

  1. 手勢監聽,給所有View加上動畫,當劃出選單的時候,用動畫方式移動底部的標籤欄

  2. NestedScrollerView 加上PageView 的physics?

  3. 實現一個能支援巢狀滑動的PageView

當然,如果我採用第一種方案也就沒這篇文章了,其中方案調研的血淚史不提也罷。

首先需要了解的知識(課前準備)

  1. Flutter中,提到巢狀滑動,自然第一想到的就是NestedScrollView,所以如果想給PageView加上巢狀滑動機制,學習下NestedScrollView及其核心原理能給我們很大的幫助。

  2. Scrollable.dart 中的ScrollerController,ScrollerPosition等,瞭解其用途、基本含義和使用方法(當然,這篇文章需要的沒那麼多,要想理解,只需要看下ScrollerController、ScrollerPosition即可,那些beginActivity、ScrollActivity什麼的不明白也罷,這篇文章用不到,用ScrollPositionWithSingleContext複製黏貼過來的就行)。

萬里長征第一步,讓它巢狀滑動起來

做好課前準備之後,我們最簡單的描述一下 NestedScrollView 的巢狀滑動步驟:

  1. 建立_NestedScrollCoordinator ,同時建立2個ScrollController,用於管理整體的CunstomScrollerView和內部primary的可滑動佈局

  2. 通過 自定義的ScrollController ,返回自定義ScrollPosition,順便將Delegate 具體實現交給 _NestedScrollCoordinator來實現。

  3. delegate 通過具體的 applyUserOffset 方法來控制整個列表的內容,順序是往上或者往左滑先滑外部,往下或往右滑先處理內部。

如果實現PageView的巢狀滑動,也可以採取這個思路。

一些小小坑

  1. PageView本身是不支援primary的,所以如果想像NestedScrollerView那樣不需要給child傳入特定controller,直接用即可的話,就需要實現一個支援Primary的PageView;

  2. PageView是不會保活的,所以如果拉到主PageView的第二頁,包含子PageView的第一頁就會dispose,因此丟失滑動狀態,再拉回來的時候自然展示的是第一頁,而不是巢狀滑動之後的最大頁。所以這塊需要保活處理一下下

核心程式碼

 1class _ChildPagePosition extends ScrollPosition
2    implements PageMetricsScrollActivityDelegate 
{
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 效果

NestedPageView 1.0
NestedPageView 1.0

後記

當然,支援巢狀滑動僅僅只是開始……

如圖所示,目前僅僅巢狀滑動了而已,鬆開手之後的physics效果,拉到一半再拉回去等操作也沒特殊處理,嘛,不過這是以後的事了……

相關文章