flutter滾動(dòng)簡(jiǎn)探:當(dāng)你滾動(dòng)ListView的時(shí)候flutter做了什么(1)

引:之前項(xiàng)目遇到一個(gè)需求,需要兩個(gè)嵌套的TabBarView在內(nèi)部滑動(dòng)到底之后可以滑外部,抄了點(diǎn)代碼然后修修補(bǔ)補(bǔ)姑且算是完成了,有的地方也只是看著函數(shù)和類的命名半猜著改的,也不知道具體是哪里調(diào)了這個(gè)函數(shù),總之挺不滿意的。所以就想著去看一下flutter內(nèi)部滾動(dòng)的實(shí)現(xiàn),結(jié)果一看發(fā)現(xiàn)滾動(dòng)相關(guān)的東西是真的非常龐大……不過好消息是,比較龐雜的部分,大概率開發(fā)者也不會(huì)去接觸,剩下的一些,倒是可以一看,有所用處的。

class ListView extends BoxScrollView {
  // 省略一萬個(gè)構(gòu)造函數(shù)
  @override
  Widget buildChildLayout(BuildContext context) {
    if (itemExtent != null) {
      return SliverFixedExtentList(
        delegate: childrenDelegate,
        itemExtent: itemExtent,
      );
    }
    return SliverList(delegate: childrenDelegate);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('itemExtent', itemExtent, defaultValue: null));
  // ...
 }

ListView里唯一做了override的函數(shù)是buildChildLayout,康康哪里用了這個(gè)函數(shù)

abstract class BoxScrollView extends ScrollView {
  // ...
 @override
  List<Widget> buildSlivers(BuildContext context) {
    Widget sliver = buildChildLayout(context);
    // ...
  }
  // ...
}

只有這一個(gè)地方,直接看buildSlivers,發(fā)現(xiàn)在ScrollView里面,ScrollView是一個(gè)普通的StateLessWidget,buildSlivers用在了build函數(shù)里

abstract class ScrollView extends StatelessWidget {
  // 這里是傳入 buildViewport 的 Widget
  @protected
  Widget buildViewport(
    BuildContext context,
    ViewportOffset offset,
    AxisDirection axisDirection,
    List<Widget> slivers,
  ) {
    if (shrinkWrap) {
      return ShrinkWrappingViewport(
        axisDirection: axisDirection,
        offset: offset,
        slivers: slivers,
      );
    }
    return Viewport(
      axisDirection: axisDirection,
      offset: offset,
      slivers: slivers,
      cacheExtent: cacheExtent,
      center: center,
      anchor: anchor,
    );
  }

  @override
  Widget build(BuildContext context) {
    final List<Widget> slivers = buildSlivers(context);
    final AxisDirection axisDirection = getDirection(context);

    final ScrollController scrollController =
        primary ? PrimaryScrollController.of(context) : controller;
    final Scrollable scrollable = Scrollable(
      dragStartBehavior: dragStartBehavior,
      axisDirection: axisDirection,
      controller: scrollController,
      physics: physics,
      semanticChildCount: semanticChildCount,
      // viewportBuilder 在內(nèi)層直接通過 widget.viewportBuilder(context, position) 作為child傳入了
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
        // 這里調(diào)了通過 buildSlivers 得到的 slivers
        return buildViewport(context, offset, axisDirection, slivers);
      },
    );
    // ...
  }
  // ...
}

在ScrollView這一層可以看到,最終build出的內(nèi)容是一個(gè)Viewport 或者 ShrinkWrappingViewport,兩者都是一個(gè) MultiChildRenderObjectWidget,內(nèi)部的布局邏輯大部分也是比較類似的。同時(shí)我們發(fā)現(xiàn),平時(shí)常用的 'slivers' 就是用在了ViewPort里面。關(guān)于 ViewPort 內(nèi)部具體做了什么可以參考這篇文章:
https://segmentfault.com/a/1190000015086603

簡(jiǎn)單概括來說,Sliver類似一個(gè)允許用戶選擇“哪一部分可以被看到”的Flex組件,顯然,這個(gè)特性可以用來做滾動(dòng)頁面。同時(shí)可以發(fā)現(xiàn)flutter的滾動(dòng)區(qū)也是一個(gè)一個(gè)renderBox畫出來的,讓人有一種自己可以控制flutter所有行為的錯(cuò)覺(?)。

滾動(dòng)布局方面的東西,實(shí)際上都由Viewport處理了,剩下的無非就是兩件事:獲取用戶的手勢(shì)和處理手勢(shì)與布局之間的關(guān)系。繼續(xù)讀代碼,可以發(fā)現(xiàn)手勢(shì)部分也是被封成了一塊穩(wěn)定且基本不需要改動(dòng)的模塊。從ScrollView中可以發(fā)現(xiàn)viewportBuilder是在Scrollable里調(diào)的,而Scrollable內(nèi)部則是這樣:

  @override
  Widget build(BuildContext context) {
    Widget result = _ScrollableScope(
      scrollable: this,
      position: position,
      // TODO(ianh): Having all these global keys is sad.
      child: Listener(
        onPointerSignal: _receivedPointerSignal,
        child: RawGestureDetector(
          key: _gestureDetectorKey,
          gestures: _gestureRecognizers,
          behavior: HitTestBehavior.opaque,
          excludeFromSemantics: widget.excludeFromSemantics,
          child: Semantics(
            explicitChildNodes: !widget.excludeFromSemantics,
            child: IgnorePointer(
              key: _ignorePointerKey,
              ignoring: _shouldIgnorePointer,
              ignoringSemantics: false,
              child: widget.viewportBuilder(context, position),
            ),
          ),
        ),
      ),
    );

Listener組件看起來很陌生,但是常用的GestureDetector組件其實(shí)就是Listener的簡(jiǎn)單包裝,Listener組件內(nèi)部對(duì)手勢(shì)做了很詳盡的處理,不太需要修改,(并且由于我不是很想讀這一塊),所以這里就不多關(guān)心。實(shí)際上我們更多關(guān)心的還是:手勢(shì)如何對(duì)Viewport的布局造成影響。于是我們先回到Viewport

  @override
  RenderViewport createRenderObject(BuildContext context) {
    return RenderViewport(
      axisDirection: axisDirection,
      crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
      anchor: anchor,
      offset: offset,
      cacheExtent: cacheExtent,
      cacheExtentStyle: cacheExtentStyle,
    );
  }

Viewport的createRenderObject如圖,可以看到這里能影響顯示區(qū)域的只有一個(gè)offset,offset是一個(gè)叫ViewportOffset的玩意,而繼續(xù)讀代碼可以發(fā)現(xiàn),有時(shí)候會(huì)見到的ScrollPosition就是ViewportOffset的子類(察覺)。

在ScrollPosition看它改寫了哪些方法,很容易發(fā)現(xiàn)最關(guān)鍵的地方:

@override
double get pixels => _pixels;
double _pixels;

其余的改寫都是一些最大滾動(dòng)區(qū),jumpTo之類的方法,并不是特別關(guān)鍵。所以要知道手勢(shì)何時(shí)影響了Viewport,最重要就是要知道手勢(shì)如何影響ScrollPosition中的pixels。

可以在Scrollable中看到,Listener給出的事件是在_gestureRecognizers中監(jiān)聽的,而_gestureRecognizers是在setCanDrag方法中注冊(cè)的

          _gestureRecognizers = <Type, GestureRecognizerFactory>{
            VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
              () => VerticalDragGestureRecognizer(),
              (VerticalDragGestureRecognizer instance) {
                instance
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel
                  ..minFlingDistance = _physics?.minFlingDistance
                  ..minFlingVelocity = _physics?.minFlingVelocity
                  ..maxFlingVelocity = _physics?.maxFlingVelocity
                  ..dragStartBehavior = widget.dragStartBehavior;
              },
            ),
          };

尋找setCanDrag方法,發(fā)現(xiàn)追根溯源一大串,最終是在Viewport的layout函數(shù)里面間接調(diào)用了。好,喜大普奔,不是我們需要關(guān)心的事情了。來關(guān)心一下幾個(gè)handleDrag:

  void _handleDragDown(DragDownDetails details) {
    assert(_drag == null);
    assert(_hold == null);
    _hold = position.hold(_disposeHold);
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    // _drag might be null if the drag activity ended and called _disposeDrag.
    assert(_hold == null || _drag == null);
    _drag?.update(details);
  }

  void _handleDragEnd(DragEndDetails details) {
    // _drag might be null if the drag activity ended and called _disposeDrag.
    assert(_hold == null || _drag == null);
    _drag?.end(details);
    assert(_drag == null);
  }

  void _handleDragCancel() {
    // _hold might be null if the drag started.
    // _drag might be null if the drag activity ended and called _disposeDrag.
    assert(_hold == null || _drag == null);
    _hold?.cancel();
    _drag?.cancel();
    assert(_hold == null);
    assert(_drag == null);
  }

于是我們發(fā)現(xiàn),實(shí)際上就是ScrollPosition在中間協(xié)調(diào)手勢(shì)和viewPort位置的關(guān)系?;氐轿恼麻_頭的問題,要自己控制滾動(dòng)行為,實(shí)際上就是需要有一個(gè)滿足需求的ScrollPosition來處理Drag和Hold方法。

當(dāng)然,之后還會(huì)有Physics給出的goBallistic等方法來處理用戶手勢(shì)結(jié)束后的滾動(dòng)動(dòng)作,這些就在之后的文章再談吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,165評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,595評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,435評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,943評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,175評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,713評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,599評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,788評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,034評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,408評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,747評(píng)論 2 370