flutter源碼系列 PageView源碼分析以及監(jiān)聽(tīng)事件

最近一個(gè)項(xiàng)目要實(shí)現(xiàn)可以無(wú)限循環(huán)的PageView,主要思路是在初始化pageview的list的時(shí)候在開(kāi)始和結(jié)尾多加一個(gè)結(jié)尾和開(kāi)頭的widget,當(dāng)滑動(dòng)到開(kāi)頭和結(jié)尾的時(shí)候手動(dòng)進(jìn)行頁(yè)面的切換,詳細(xì)可以搜索pageview無(wú)限輪播。
這種方法有一個(gè)要點(diǎn)就是要維護(hù)兩個(gè)索引,一個(gè)是內(nèi)部list的索引,一個(gè)是外部顯示的索引,由于list的容量是比顯示的數(shù)量多2的,所以如果要在外部進(jìn)行一些比如指示器或者計(jì)時(shí)器功能要進(jìn)行和頁(yè)面同步顯示或者切換頁(yè)面操作時(shí),需要將顯示的索引轉(zhuǎn)換成list的索引。
不過(guò)網(wǎng)上說(shuō)的都是一些比較簡(jiǎn)單的實(shí)現(xiàn),看到比較多的就是當(dāng)滑動(dòng)到要手動(dòng)切換的時(shí)候進(jìn)行一個(gè)時(shí)延,這樣可以避免直接切換頁(yè)面造成的卡頓和跳動(dòng)現(xiàn)象。但是存在一個(gè)問(wèn)題,如果要同時(shí)實(shí)現(xiàn)一個(gè)跟隨頁(yè)面切換的指示器,就會(huì)出現(xiàn)當(dāng)頁(yè)面切換過(guò)去之后指示器才會(huì)跟著過(guò)去,因?yàn)轫?yè)面切換的時(shí)候執(zhí)行了時(shí)延,而時(shí)延之后才會(huì)真正改變索引,此時(shí)才會(huì)setstate,之后指示器才能響應(yīng)到索引的切換,但是如果在時(shí)延之前就切換的話又會(huì)出現(xiàn)指示器先行的情況。因此這種方法其實(shí)是存在一些問(wèn)題的。
所以解決這個(gè)問(wèn)題的關(guān)鍵在于如何進(jìn)行頁(yè)面切換的判斷。這里可以有兩種思路實(shí)現(xiàn),第一種是實(shí)現(xiàn)viewpage的onpagechanged方法,在里面進(jìn)行邏輯的判斷,然后用controller來(lái)進(jìn)行頁(yè)面跳轉(zhuǎn),不過(guò)這種方法存在當(dāng)controller跳轉(zhuǎn)的時(shí)候又會(huì)回調(diào)onpagechanged,所以就會(huì)出現(xiàn)多次對(duì)索引不必要操作,而且如果有比如計(jì)時(shí)器等額外的功能的話可能不方便將頁(yè)面邏輯分開(kāi),而且依舊無(wú)法解決指示器延遲問(wèn)題,同時(shí)也很難進(jìn)行細(xì)粒度的操作。
第二種方法我們就要去看pageview的源碼了,從源碼的角度來(lái)解決問(wèn)題才是正確的方法。首先我們點(diǎn)進(jìn)去pageview的源碼

//pageview的一些參數(shù)就不用過(guò)多解析了,這里我們直接看build方法,我寫(xiě)上了一些解析
 @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來(lái)進(jìn)行監(jiān)聽(tīng),
//這其實(shí)也是我們解決的思路,后面詳解。
    return NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification notification) {
//這里有兩個(gè)點(diǎn)要注意,一個(gè)是ScrollUpdateNotification,代表頁(yè)面滑動(dòng)已經(jīng)完成了
//還有一個(gè)是這里是一直返回false的,也就是pageview不會(huì)攔截事件,會(huì)往父類傳遞
        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的邏輯調(diào)用
//在這里,每次當(dāng)listener監(jiān)聽(tīng)到頁(yè)面變化是就會(huì)調(diào)用,所以不方便進(jìn)行額外的操作和功能。
            widget.onPageChanged(currentPage);
          }
        }
        return false;
      },
//pageview可以理解為就是外部有一個(gè)監(jiān)聽(tīng)事件,里面通過(guò)Scrollable來(lái)進(jìn)行頁(yè)面切換邏輯的實(shí)現(xiàn)
//Scrollable內(nèi)部其實(shí)就是通過(guò)RawGestureDetector來(lái)實(shí)現(xiàn)手勢(shì)的監(jiān)聽(tīng),這里不多展開(kāi),大家可以自行看源碼。
      child: Scrollable(
        dragStartBehavior: widget.dragStartBehavior,
        axisDirection: axisDirection,
        controller: widget.controller,
        physics: physics,
        restorationId: widget.restorationId,
        viewportBuilder: (BuildContext context, ViewportOffset position) 

看到這里其實(shí)已經(jīng)有一些思路了,我們之前難點(diǎn)在于重寫(xiě)了onpagechanged方法導(dǎo)致問(wèn)題無(wú)法很好的解決,現(xiàn)在我們找到了onpagechanged調(diào)用的地方,只要找辦法避免掉就可以實(shí)現(xiàn)了。
當(dāng)然這里我們要說(shuō)到NotificationListener,以及flutter對(duì)應(yīng)的冒泡事件傳輸機(jī)制,這里大家可以去看看這篇 文章。
我來(lái)總結(jié)一下,其實(shí)就是flutter對(duì)于notification這個(gè)組件,有一中事件規(guī)則叫冒泡傳遞,底層的notification如果在它的 onNotification寫(xiě)的邏輯中返回是false以及它不是根結(jié)點(diǎn),就會(huì)去向上遍歷尋找它的祖先notification組件,知道遇到root節(jié)點(diǎn)或者某一個(gè)返回true,則事件傳遞結(jié)束。
而且在onNotification中可以對(duì)多種事件進(jìn)行監(jiān)聽(tīng)和處理,所以我們可以把對(duì)viewpage頁(yè)面跳轉(zhuǎn)對(duì)索引處理的邏輯寫(xiě)在這里,而且我們可以分別處理比如滑動(dòng)開(kāi)始的start事件和結(jié)束的end事件,分別進(jìn)行細(xì)粒度的邏輯的處理,這樣就可以在外部進(jìn)行操作和別的功能實(shí)現(xiàn)了。

//這里可以寫(xiě)成這樣,在end事件進(jìn)行頁(yè)面索引的判斷和切換同時(shí)在update事件來(lái)進(jìn)行我們的索引變換
          if (notification is ScrollStartNotification &&
              widget.onPageStartChanged != null) {
            widget.onPageStartChanged(_currentReportedPage);
          } else if (notification is ScrollEndNotification &&
              widget.onPageEndChanged != null) {
            widget.onPageEndChanged(_currentReportedPage);

因此不僅無(wú)限輪播事件可以通過(guò)這種方法來(lái)解決,如果有其他的操作也可以這樣進(jìn)行處理,而且因?yàn)槲覀儧](méi)有傳入onpagechanged方法,所以不存在多次調(diào)用的問(wèn)題,pageview那里判斷onpagechanged是null方法就不會(huì)進(jìn)去了,會(huì)直接我們寫(xiě)在pageview外面的notification的邏輯。
最后的結(jié)構(gòu)大概這樣

NotificationListener<ScrollNotification>(
      child: ScrollConfiguration(
        behavior: OverScrollBehavior(),
        child: PageView(
          controller: _controller,
          children: _children,
          physics: const ClampingScrollPhysics(),
          allowImplicitScrolling: widget.allowImplicitScrolling,
        ),
      ),
      onNotification: (notification) {
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容