flutter原始碼系列 PageView原始碼分析以及監聽事件

wzqlldy發表於2021-03-08

最近一個專案要實現可以無限迴圈的PageView,主要思路是在初始化pageview的list的時候在開始和結尾多加一個結尾和開頭的widget,當滑動到開頭和結尾的時候手動進行頁面的切換,詳細可以搜尋pageview無限輪播。 這種方法有一個要點就是要維護兩個索引,一個是內部list的索引,一個是外部顯示的索引,由於list的容量是比顯示的數量多2的,所以如果要在外部進行一些比如指示器或者計時器功能要進行和頁面同步顯示或者切換頁面操作時,需要將顯示的索引轉換成list的索引。 不過網上說的都是一些比較簡單的實現,看到比較多的就是當滑動到要手動切換的時候進行一個時延,這樣可以避免直接切換頁面造成的卡頓和跳動現象。但是存在一個問題,如果要同時實現一個跟隨頁面切換的指示器,就會出現當頁面切換過去之後指示器才會跟著過去,因為頁面切換的時候執行了時延,而時延之後才會真正改變索引,此時才會setstate,之後指示器才能響應到索引的切換,但是如果在時延之前就切換的話又會出現指示器先行的情況。因此這種方法其實是存在一些問題的。 所以解決這個問題的關鍵在於如何進行頁面切換的判斷。這裡可以有兩種思路實現,第一種是實現viewpage的onpagechanged方法,在裡面進行邏輯的判斷,然後用controller來進行頁面跳轉,不過這種方法存在當controller跳轉的時候又會回撥onpagechanged,所以就會出現多次對索引不必要操作,而且如果有比如計時器等額外的功能的話可能不方便將頁面邏輯分開,而且依舊無法解決指示器延遲問題,同時也很難進行細粒度的操作。 第二種方法我們就要去看pageview的原始碼了,從原始碼的角度來解決問題才是正確的方法。首先我們點進去pageview的原始碼

//pageview的一些引數就不用過多解析了,這裡我們直接看build方法,我寫上了一些解析
 @override
  Widget build(BuildContext context) {
    final AxisDirection axisDirection = _getDirection(context);
    final ScrollPhysics physics = _ForceImplicitScrollPhysics(
      allowImplicitScrolling: widget.allowImplicitScrolling,
    ).applyTo(widget.pageSnapping
        ? _kPagePhysics.applyTo(widget.physics)
        : widget.physics);
//此處可知pageview最主要的判斷方法是利用NotificationListener來進行監聽,
//這其實也是我們解決的思路,後面詳解。
    return NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification notification) {
//這裡有兩個點要注意,一個是ScrollUpdateNotification,代表頁面滑動已經完成了
//還有一個是這裡是一直返回false的,也就是pageview不會攔截事件,會往父類傳遞
        if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
          final PageMetrics metrics = notification.metrics as PageMetrics;
          final int currentPage = metrics.page.round();
          if (currentPage != _lastReportedPage) {
            _lastReportedPage = currentPage;
//可以看到onPageChanged如果是按照第一種方法,onpagechanged的邏輯呼叫
//在這裡,每次當listener監聽到頁面變化是就會呼叫,所以不方便進行額外的操作和功能。
            widget.onPageChanged(currentPage);
          }
        }
        return false;
      },
//pageview可以理解為就是外部有一個監聽事件,裡面通過Scrollable來進行頁面切換邏輯的實現
//Scrollable內部其實就是通過RawGestureDetector來實現手勢的監聽,這裡不多展開,大家可以自行看原始碼。
      child: Scrollable(
        dragStartBehavior: widget.dragStartBehavior,
        axisDirection: axisDirection,
        controller: widget.controller,
        physics: physics,
        restorationId: widget.restorationId,
        viewportBuilder: (BuildContext context, ViewportOffset position) 
複製程式碼

看到這裡其實已經有一些思路了,我們之前難點在於重寫了onpagechanged方法導致問題無法很好的解決,現在我們找到了onpagechanged呼叫的地方,只要找辦法避免掉就可以實現了。 當然這裡我們要說到NotificationListener,以及flutter對應的冒泡事件傳輸機制,這裡大家可以去看看這篇 文章。 我來總結一下,其實就是flutter對於notification這個元件,有一中事件規則叫冒泡傳遞,底層的notification如果在它的 onNotification寫的邏輯中返回是false以及它不是根結點,就會去向上遍歷尋找它的祖先notification元件,知道遇到root節點或者某一個返回true,則事件傳遞結束。 而且在onNotification中可以對多種事件進行監聽和處理,所以我們可以把對viewpage頁面跳轉對索引處理的邏輯寫在這裡,而且我們可以分別處理比如滑動開始的start事件和結束的end事件,分別進行細粒度的邏輯的處理,這樣就可以在外部進行操作和別的功能實現了。

//這裡可以寫成這樣,在end事件進行頁面索引的判斷和切換同時在update事件來進行我們的索引變換
          if (notification is ScrollStartNotification &&
              widget.onPageStartChanged != null) {
            widget.onPageStartChanged(_currentReportedPage);
          } else if (notification is ScrollEndNotification &&
              widget.onPageEndChanged != null) {
            widget.onPageEndChanged(_currentReportedPage);
複製程式碼

因此不僅無限輪播事件可以通過這種方法來解決,如果有其他的操作也可以這樣進行處理,而且因為我們沒有傳入onpagechanged方法,所以不存在多次呼叫的問題,pageview那裡判斷onpagechanged是null方法就不會進去了,會直接我們寫在pageview外面的notification的邏輯。 最後的結構大概這樣

NotificationListener<ScrollNotification>(
      child: ScrollConfiguration(
        behavior: OverScrollBehavior(),
        child: PageView(
          controller: _controller,
          children: _children,
          physics: const ClampingScrollPhysics(),
          allowImplicitScrolling: widget.allowImplicitScrolling,
        ),
      ),
      onNotification: (notification) {
複製程式碼

相關文章