Flutter Sliver一生之敵 (ScrollView)

image

前言

入坑Flutter一年了,接觸到Flutter也只是冰山一角聂宾,很多東西可能知道是怎么用的果善,但是不是很明白其中的原理,俗話說唯有深入系谐,方能淺出巾陕。本系列將對Sliver相關(guān)源碼一一進行分析,希望能夠舉一反三纪他,不再懼怕Sliver鄙煤。


image

看完Flutter Sliver一生之敵 你將不會害怕使用Sliver,Sliver將成為你的一生之愛茶袒。歡迎加入Flutter Candies <a target="_blank" ><img border="0" src="https://user-gold-cdn.xitu.io/2019/10/27/16e0ca3f1a736f0e?w=90&h=22&f=png&s=1827" alt="flutter-candies" title="flutter-candies"></a> QQ群: 181398081馆类。

下面是全部滾動的組件,以及他們的關(guān)系

Widget Build Viewport
SingleChildScrollView Scrollable _SingleChildViewport
ScrollView Scrollable ShrinkWrappingViewport/Viewport

Sliver系列繼承于ScrollView

Widget Extends
CustomScrollView ScrollView
NestedScrollView CustomScrollView
ListView/GridView BoxScrollView => ScrollView

簡單講滾動組件由Scrollable獲取用戶手勢反饋弹谁,將滾動反饋和Slivers傳遞給Viewport計算出Sliver的位置曹动。注意Sliver可以是單孩子(SliverPadding/SliverPersistentHeader/SliverToBoxAdapter等等)也可以是多孩子(SliverList/SliverGrid)萄金。下面我們通過分析源碼,探究其中奧秘眼五。

ScrollView

下面為build方法中的關(guān)鍵代碼咳胃,這里是我們上面說的Scrollable植康,主要負責用戶手勢監(jiān)聽反饋。

    final Scrollable scrollable = Scrollable(
      dragStartBehavior: dragStartBehavior,
      axisDirection: axisDirection,
      controller: scrollController,
      physics: physics,
      semanticChildCount: semanticChildCount,
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
        return buildViewport(context, offset, axisDirection, slivers);
      },
    );

我們再看看buildViewport方法

  @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,
    );
  }

根據(jù)shrinkWrap的不同展懈,分成了2種Viewport

Scrollable

用于監(jiān)聽各種用戶手勢并實現(xiàn)滾動销睁,下面為build方法中的關(guān)鍵代碼。

    //InheritedWidget組件存崖,為了共享position數(shù)據(jù)
    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,
              //通過Listener監(jiān)聽手勢冻记,將滾動position通過viewportBuilder回調(diào)。
              child: widget.viewportBuilder(context, position),
            ),
          ),
        ),
      ),
    );
    
   //這里可以看到為什么安卓和ios上面對于滾動越界(overscrolls)時候的操作不一樣    
   return _configuration.buildViewportChrome(context, result, widget.axisDirection);

安卓和fuchsia上面使用GlowingOverscrollIndicator來顯示滾動不了之后的水波紋效果来惧。

  /// Wraps the given widget, which scrolls in the given [AxisDirection].
  ///
  /// For example, on Android, this method wraps the given widget with a
  /// [GlowingOverscrollIndicator] to provide visual feedback when the user
  /// overscrolls.
  Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
    // When modifying this function, consider modifying the implementation in
    // _MaterialScrollBehavior as well.
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
        return child;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return GlowingOverscrollIndicator(
          child: child,
          axisDirection: axisDirection,
          color: _kDefaultGlowColor,
        );
    }
    return null;
  }

Viewport

通過只顯示(計算繪制)滾動視圖中的一部分內(nèi)容來實現(xiàn)滾動可視化設(shè)計冗栗,大大降低內(nèi)存消耗。比如ListView可視區(qū)域為666像素供搀,但其列表元素的總高度遠遠超過666像素隅居,但實際上我們只是關(guān)心這個666像素中的元素(當然如果設(shè)置了CacheExtent,還要算上這個距離)

在Scrollview中將Scrollable滾動反饋以及Slivers傳遞給了Viewport葛虐。Viewport 是一個MultiChildRenderObjectWidget胎源,lei了lei了,這是一個自繪多孩子的組件屿脐。直接找到createRenderObject方法涕蚤,看到返回一個RenderViewport

RenderViewport

重頭戲來了宪卿,我們看看構(gòu)造參數(shù)有哪些。

  RenderViewport({
    //主軸方向赞季,默認向下
    AxisDirection axisDirection = AxisDirection.down,
    //縱軸方向愧捕,跟主軸方向以及有關(guān)系
    @required AxisDirection crossAxisDirection,
    //Scrollable中回調(diào)的用戶反饋
    @required ViewportOffset offset,
    //當scrollOffset = 0,第一個child在viewport的位置(0 <= anchor <= 1.0)申钩,0.0在leading次绘,1.0在trailing,0.5在中間
    double anchor = 0.0,
    //sliver孩子們
    List<RenderSliver> children,
    //The first child in the [GrowthDirection.forward] growth direction.
    //計算時候的基準撒遣,默認為第一個娃邮偎,這個參數(shù)估計極少有人使用
    RenderSliver center,
    //緩存區(qū)域大小
    double cacheExtent,
    //決定cacheExtent是實際大小還是根據(jù)viewport的百分比
    CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
  })... {
    addAll(children);
    if (center == null && firstChild != null)
      _center = firstChild;
  }

可以看到構(gòu)造中把全部孩子都加進入了,而且如果外部不傳遞center义黎,center默認為第一個孩子禾进。

劃重點代碼分析

sizedByParent

在Viewport中這個值永遠返回true,

  @override
  bool get sizedByParent => true;

來看看這個屬性的解釋廉涕。即如果這個值為true泻云,那么組件的大小只跟它的parent告訴它的大小constraints有關(guān)系,與它的 child 都無關(guān).

就是說RenderViewport的大小約束是由它的parent告訴它的狐蜕,跟里面的Slivers沒有關(guān)系宠纯。說到這個我們看一個新手經(jīng)常錯誤的代碼。

     Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '測試',
            ),
            ListView.builder(itemBuilder: (context,index){})
          ],
        ),

我們前面知道ListView最終是一個ScrollView,其中的Viewport在Column當中是無法知道自己的有效大小的层释,該代碼的會導致Viewport的高度為無限大婆瓜,將會報錯(當然你這里可以把shrinkWrap設(shè)置為true,但是這樣會導致ListView的全部元素都被計算贡羔,列表將失去滾動廉白,這個我們后面會講)

繼續(xù)看代碼中看到,當sizedByParent為true的時候調(diào)用performResize方法乖寒,指定Size只根據(jù)constraints猴蹂。

    if (sizedByParent) {
      assert(() {
        _debugDoingThisResize = true;
        return true;
      }());
      try {
        performResize();
        assert(() {
          debugAssertDoesMeetConstraints();
          return true;
        }());
      } catch (e, stack) {
        _debugReportException('performResize', e, stack);
      }
      assert(() {
        _debugDoingThisResize = false;
        return true;
      }());
    }

performResize

看看RenderViewport的performResize中做了什么。有一大堆assert楣嘁,就一句話晕讲,我不能無限大。最后將自己的size設(shè)置為constraints.biggest马澈。
(size是自己的大小瓢省,constraints是parent給的限制)

  @override
  void performResize() {
    assert(() {
      if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
        switch (axis) {
          case Axis.vertical:
            if (!constraints.hasBoundedHeight) {
              throw FlutterError.fromParts(<DiagnosticsNode>[
                ErrorSummary('Vertical viewport was given unbounded height.'),
                ErrorDescription(
                  'Viewports expand in the scrolling direction to fill their container. '
                  'In this case, a vertical viewport was given an unlimited amount of '
                  'vertical space in which to expand. This situation typically happens '
                  'when a scrollable widget is nested inside another scrollable widget.'
                ),
                ErrorHint(
                  'If this widget is always nested in a scrollable widget there '
                  'is no need to use a viewport because there will always be enough '
                  'vertical space for the children. In this case, consider using a '
                  'Column instead. Otherwise, consider using the "shrinkWrap" property '
                  '(or a ShrinkWrappingViewport) to size the height of the viewport '
                  'to the sum of the heights of its children.'
                )
              ]);
            }
            if (!constraints.hasBoundedWidth) {
              throw FlutterError(
                'Vertical viewport was given unbounded width.\n'
                'Viewports expand in the cross axis to fill their container and '
                'constrain their children to match their extent in the cross axis. '
                'In this case, a vertical viewport was given an unlimited amount of '
                'horizontal space in which to expand.'
              );
            }
            break;
          case Axis.horizontal:
            if (!constraints.hasBoundedWidth) {
              throw FlutterError.fromParts(<DiagnosticsNode>[
                ErrorSummary('Horizontal viewport was given unbounded width.'),
                ErrorDescription(
                  'Viewports expand in the scrolling direction to fill their container.'
                  'In this case, a horizontal viewport was given an unlimited amount of '
                  'horizontal space in which to expand. This situation typically happens '
                  'when a scrollable widget is nested inside another scrollable widget.'
                ),
                ErrorHint(
                  'If this widget is always nested in a scrollable widget there '
                  'is no need to use a viewport because there will always be enough '
                  'horizontal space for the children. In this case, consider using a '
                  'Row instead. Otherwise, consider using the "shrinkWrap" property '
                  '(or a ShrinkWrappingViewport) to size the width of the viewport '
                  'to the sum of the widths of its children.'
                )
              ]);
            }
            if (!constraints.hasBoundedHeight) {
              throw FlutterError(
                'Horizontal viewport was given unbounded height.\n'
                'Viewports expand in the cross axis to fill their container and '
                'constrain their children to match their extent in the cross axis. '
                'In this case, a horizontal viewport was given an unlimited amount of '
                'vertical space in which to expand.'
              );
            }
            break;
        }
      }
      return true;
    }());
    size = constraints.biggest;
    // We ignore the return value of applyViewportDimension below because we are
    // going to go through performLayout next regardless.
    switch (axis) {
      case Axis.vertical:
        offset.applyViewportDimension(size.height);
        break;
      case Axis.horizontal:
        offset.applyViewportDimension(size.width);
        break;
    }
  }

performLayout

負責布局RenderViewport的Children

    //從size中得到主軸和縱軸的大小
    double mainAxisExtent;
    double crossAxisExtent;
    switch (axis) {
      case Axis.vertical:
        mainAxisExtent = size.height;
        crossAxisExtent = size.width;
        break;
      case Axis.horizontal:
        mainAxisExtent = size.width;
        crossAxisExtent = size.height;
        break;
    }

    //如果單Sliver孩子的viewport高度為100,anchor為0.5痊班,centerOffsetAdjustment設(shè)置為50.0的話勤婚,當scroll offset is 0.0的時候,center會剛好在viewport中間涤伐。
    final double centerOffsetAdjustment = center.centerOffsetAdjustment;

    double correction;
    int count = 0;
    do {
      assert(offset.pixels != null);
      correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
      ///如果不為0.0的話馒胆,是因為child中有需要修正(這個我們將在后面系列中講到缨称,這里我們就簡單認為在layout child過程中出現(xiàn)了問題),我們需要改變scroll offset之后重新layout chilren祝迂。
      if (correction != 0.0) {
        offset.correctBy(correction);
      } else {
        ///告訴Scrollable 最小滾動距離和最大滾動距離
        if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           ))
          break;
      }
      count += 1;
    } while (count < _maxLayoutCycles);

如果超過最大次數(shù)睦尽,children還是layout還是有問題的話,將警告提示型雳。

下面我們看看_attemptLayout方法中做了什么当凡。

  double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
    assert(!mainAxisExtent.isNaN);
    assert(mainAxisExtent >= 0.0);
    assert(crossAxisExtent.isFinite);
    assert(crossAxisExtent >= 0.0);
    assert(correctedOffset.isFinite);
    _minScrollExtent = 0.0;
    _maxScrollExtent = 0.0;
    _hasVisualOverflow = false;

    //centerOffset的數(shù)值將使用anchor和offset.pixels + centerOffsetAdjustment進行修正。前面有講
    final double centerOffset = mainAxisExtent * anchor - correctedOffset;
    //反向RemainingPaintExtent纠俭,就是center之前還有多少距離可以拿來繪制
    final double reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent);
    //正向RemainingPaintExtent沿量,就是center之后還有多少距離可以拿來繪制
    final double forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent);

    switch (cacheExtentStyle) {
      case CacheExtentStyle.pixel:
        _calculatedCacheExtent = cacheExtent;
        break;
      case CacheExtentStyle.viewport:
        _calculatedCacheExtent = mainAxisExtent * cacheExtent;
        break;
    }
    ///總的計算區(qū)域包含前后2個cacheExtent
    final double fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent;
    ///加上cacheExtent的center位置,跟前面的比就是多了cache
    final double centerCacheOffset = centerOffset + _calculatedCacheExtent;
     //反向RemainingPaintExtent冤荆,就是center之前還有多少距離可以拿來繪制朴则,跟前面的比就是多了cache
    final double reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent);
     //正向RemainingPaintExtent,就是center之后還有多少距離可以拿來繪制钓简,跟前面的比就是多了cache
    final double forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent);

    final RenderSliver leadingNegativeChild = childBefore(center);
    ///如果在center之前還有child乌妒,將向前l(fā)ayout child,計算前面布局前面的child
    if (leadingNegativeChild != null) {
      // negative scroll offsets
      final double result = layoutChildSequence(
        child: leadingNegativeChild,
        scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
        overlap: 0.0,
        layoutOffset: forwardDirectionRemainingPaintExtent,
        remainingPaintExtent: reverseDirectionRemainingPaintExtent,
        mainAxisExtent: mainAxisExtent,
        crossAxisExtent: crossAxisExtent,
        growthDirection: GrowthDirection.reverse,
        advance: childBefore,
        remainingCacheExtent: reverseDirectionRemainingCacheExtent,
        cacheOrigin: (mainAxisExtent - centerOffset).clamp(-_calculatedCacheExtent, 0.0),
      );
      if (result != 0.0)
        return -result;
    }

    ///布局center后面的child
    // positive scroll offsets
    return layoutChildSequence(
      child: center,
      scrollOffset: math.max(0.0, -centerOffset),
      overlap: leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
      layoutOffset: centerOffset >= mainAxisExtent ? centerOffset: reverseDirectionRemainingPaintExtent,
      remainingPaintExtent: forwardDirectionRemainingPaintExtent,
      mainAxisExtent: mainAxisExtent,
      crossAxisExtent: crossAxisExtent,
      growthDirection: GrowthDirection.forward,
      advance: childAfter,
      remainingCacheExtent: forwardDirectionRemainingCacheExtent,
      cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent, 0.0),
    );
  }

注意scrollOffset 外邓,在向前和向后layout的時候不一樣撤蚊,
一個是 math.max(mainAxisExtent, centerOffset) - mainAxisExtent
一個是 math.max(0.0, -centerOffset)
我們有說過center其實是scrolloffset為0的基準,viewport里面如果有多個slivers坐榆,我們可以指定其中一個為center(默認第一個為center),那么想前滾centerOffset會變大冗茸,想后滾centerOffset會變成負數(shù)席镀。感覺還是有點抽象,下面給一個栗子夏漱,我給第2個sliver增加了key豪诲,并且把CustomScrollView的center賦值為這個key。小聲逼逼挂绰,Center這個參數(shù)我估計百分之99的人沒有用過屎篱,用過的請留言,我看看有多少人知道這個葵蒂。

CustomScrollView(
        center: key,
        slivers: <Widget>[
        SliverList(),
        SliverGrid(key:key),

運行起來初始centerOffset為0的時候SliverGrid在初始位置交播。


image

向前滾動,可以看到我們得到了逆向的SliverList践付,從我們的參數(shù)中也可以驗證到秦士。而offset.pixels(ScollView的滾動位置)當然也為0.(而不是你們想的SliverList的高度)


image

再看下layoutChildSequence方法,注意到advance方法永高,向前其實調(diào)用的是childBefore隧土,向后是調(diào)用的childAfter

  double layoutChildSequence({
    @required RenderSliver child,
    @required double scrollOffset,
    @required double overlap,
    @required double layoutOffset,
    @required double remainingPaintExtent,
    @required double mainAxisExtent,
    @required double crossAxisExtent,
    @required GrowthDirection growthDirection,
    @required RenderSliver advance(RenderSliver child),
    @required double remainingCacheExtent,
    @required double cacheOrigin,
  }) {
    assert(scrollOffset.isFinite);
    assert(scrollOffset >= 0.0);
    final double initialLayoutOffset = layoutOffset;
    final ScrollDirection adjustedUserScrollDirection =
        applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection);
    assert(adjustedUserScrollDirection != null);
    double maxPaintOffset = layoutOffset + overlap;
    double precedingScrollExtent = 0.0;

    while (child != null) {
      final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
      // If the scrollOffset is too small we adjust the paddedOrigin because it
      // doesn't make sense to ask a sliver for content before its scroll
      // offset.
      final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
      final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;

      assert(sliverScrollOffset >= correctedCacheOrigin.abs());
      assert(correctedCacheOrigin <= 0.0);
      assert(sliverScrollOffset >= 0.0);
      assert(cacheExtentCorrection <= 0.0);
      
      //輸入
      child.layout(SliverConstraints(
        axisDirection: axisDirection,
        growthDirection: growthDirection,
        userScrollDirection: adjustedUserScrollDirection,
        scrollOffset: sliverScrollOffset,
        precedingScrollExtent: precedingScrollExtent,
        overlap: maxPaintOffset - layoutOffset,
        remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
        crossAxisExtent: crossAxisExtent,
        crossAxisDirection: crossAxisDirection,
        viewportMainAxisExtent: mainAxisExtent,
        remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
        cacheOrigin: correctedCacheOrigin,
      ), parentUsesSize: true);
      //輸出
      final SliverGeometry childLayoutGeometry = child.geometry;
      assert(childLayoutGeometry.debugAssertIsValid());

      // If there is a correction to apply, we'll have to start over.
      if (childLayoutGeometry.scrollOffsetCorrection != null)
        return childLayoutGeometry.scrollOffsetCorrection;

      // We use the child's paint origin in our coordinate system as the
      // layoutOffset we store in the child's parent data.
      final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;

      // `effectiveLayoutOffset` becomes meaningless once we moved past the trailing edge
      // because `childLayoutGeometry.layoutExtent` is zero. Using the still increasing
      // 'scrollOffset` to roughly position these invisible slivers in the right order.
      if (childLayoutGeometry.visible || scrollOffset > 0) {
        updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
      } else {
        updateChildLayoutOffset(child, -scrollOffset + initialLayoutOffset, growthDirection);
      }

      //更新最大繪制位置
      maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
      scrollOffset -= childLayoutGeometry.scrollExtent;
      //前一個child的滾動距離
      precedingScrollExtent += childLayoutGeometry.scrollExtent;
      layoutOffset += childLayoutGeometry.layoutExtent;
      if (childLayoutGeometry.cacheExtent != 0.0) {
        remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
        cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
      }
      
      // 更新_maxScrollExtent和_minScrollExtent
      // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/rendering/viewport.dart#L1449
      updateOutOfBandData(growthDirection, childLayoutGeometry);

      // move on to the next child
      // layout下一個child
      child = advance(child);
    }

    // we made it without a correction, whee!
    //完美提针,全部的children都沒有錯誤
    return 0.0;
  }

SliverConstraints為layout child的輸入,SliverGeometry為layout child之后的輸出曹傀,layout之后viewport將更新_maxScrollExtent和_minScrollExtent辐脖,然后layout下一個sliver。至于child.layout方法里面內(nèi)容皆愉,我們將會在下一個章當中講到嗜价。

RenderShrinkWrappingViewport

當我們把shrinkWrap設(shè)置為true的時候,最終的Viewport使用的是RenderShrinkWrappingViewport亥啦。那么我們看看其中的區(qū)別是什么炭剪。
先看看官方對shrinkWrap參數(shù)的解釋。設(shè)置shrinkWrap為true翔脱,viewport的大小將不是由它的父親而決定奴拦,而是由它自己決定。我們經(jīng)常碰到由人使用ListView嵌套ListView的情況届吁, 外面的ListView在layout child的時候需要知道里面ListView的大小错妖,而我們前面知道ListView中的Viewport的大小是由它parent告訴它的。

parent:hi, child,你有多大疚沐,我給你一個無限縱軸大小的限制暂氯。

child: hi, parent,我也不知道啊亮蛔,你不告訴我痴施,我的viewport有多大。那么我只能將我的全部child都layout出來才知道我總的大小了究流。那我得換一個viewport了辣吃,RenderShrinkWrappingViewport才能知道計算出我的總高度。

由于ListView的parent無法告訴它的child ListView的可丈量大小芬探,所以我們必須設(shè)置shrinkWrap為true神得,內(nèi)部使用RenderShrinkWrappingViewport計算。

由于RenderShrinkWrappingViewport的大小不再只由parent決定偷仿,所以不再調(diào)用performResize方法哩簿。那么我們來關(guān)注下performLayout方法。

performLayout

 @override
 void performLayout() {
   if (firstChild == null) {
     switch (axis) {
       case Axis.vertical:
         //如果是豎直酝静,你起碼要告訴我水平最大限制吧节榜?
         assert(constraints.hasBoundedWidth);
         size = Size(constraints.maxWidth, constraints.minHeight);
         break;
          //如果是水平,你起碼要告訴我垂直最大限制吧别智?
       case Axis.horizontal:
         assert(constraints.hasBoundedHeight);
         size = Size(constraints.minWidth, constraints.maxHeight);
         break;
     }
     offset.applyViewportDimension(0.0);
     _maxScrollExtent = 0.0;
     _shrinkWrapExtent = 0.0;
     _hasVisualOverflow = false;
     offset.applyContentDimensions(0.0, 0.0);
     return;
   }

   double mainAxisExtent;
   double crossAxisExtent;
   switch (axis) {
     case Axis.vertical:
      //如果是豎直全跨,你起碼要告訴我水平最大限制吧?說到這個我想起來了Flutter中為啥沒有支持水平和垂直都能滾動的容器了亿遂。
       assert(constraints.hasBoundedWidth);
       mainAxisExtent = constraints.maxHeight;
       crossAxisExtent = constraints.maxWidth;
       break;
     case Axis.horizontal:
       assert(constraints.hasBoundedHeight);
       //如果是水平浓若,你起碼要告訴我垂直最大限制吧渺杉?
       mainAxisExtent = constraints.maxWidth;
       crossAxisExtent = constraints.maxHeight;
       break;
   }

   double correction;
   double effectiveExtent;
   do {
     assert(offset.pixels != null);
     correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels);
     if (correction != 0.0) {
       offset.correctBy(correction);
     } else {
       switch (axis) {
         case Axis.vertical:
           effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
           break;
         case Axis.horizontal:
           effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
           break;
       }
       final bool didAcceptViewportDimension = offset.applyViewportDimension(effectiveExtent);
       final bool didAcceptContentDimension = offset.applyContentDimensions(0.0, math.max(0.0, _maxScrollExtent - effectiveExtent));
       if (didAcceptViewportDimension && didAcceptContentDimension)
         break;
     }
   } while (true);
   switch (axis) {
     case Axis.vertical:
       size = constraints.constrainDimensions(crossAxisExtent, effectiveExtent);
       break;
     case Axis.horizontal:
       size = constraints.constrainDimensions(effectiveExtent, crossAxisExtent);
       break;
   }
 }

_maxScrollExtent和
_shrinkWrapExtent都是關(guān)鍵先生。當mainAxisExtent不為double.Infinity(無限大)的時候挪钓,其實效果跟Viewport里面計算(除掉Center相關(guān))是一樣; 當mainAxisExtent為double.Infinity(無限大)是越,我們將會將全部的child都layout出來獲得總的大小

關(guān)鍵代碼

 @override
 void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
   assert(growthDirection == GrowthDirection.forward);
   _maxScrollExtent += childLayoutGeometry.scrollExtent;
   if (childLayoutGeometry.hasVisualOverflow)
     _hasVisualOverflow = true;
   _shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
 }

這里也就是為啥我們之前說Column里面或者ListView放ListView(子),ListView(子)會全部元素都build碌上,并且失去滾動的原因倚评。

劇透

這一章看起來有些枯燥,都是源碼分析馏予。下一章(Flutter Sliver一生之敵 (ExtendedList))天梧,我們將順著ListView/GridView=> SliverList/SliverGrid => RenderSliverList/RenderSliverGrid的感情線,了解最終Sliver是怎么將children繪制出來的霞丧。下一章將不只是枯燥的源碼分析呢岗,我們將舉一反N,告訴你如何處理圖片列表內(nèi)存爆炸閃退蛹尝,將告訴你列表元素特殊的layout方式等等后豫。

結(jié)語

ExtendedList WaterfallFlowLoadingMoreList 都是可以食用的狀態(tài)。等不及的小伙伴可以提前食用突那,特別是圖片列表內(nèi)存過大而導致閃退的小伙伴可以先看demo,先解決掉一直折磨大家的問題

歡迎加入Flutter Candies挫酿,一起生產(chǎn)可愛的Flutter小糖果( <a target="_blank" ><img border="0" src="https://user-gold-cdn.xitu.io/2019/10/27/16e0ca3f1a736f0e?w=90&h=22&f=png&s=1827" alt="flutter-candies" title="flutter-candies"></a>QQ群:181398081)

最最后放上Flutter Candies全家桶,真香愕难。

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末早龟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子猫缭,更是在濱河造成了極大的恐慌葱弟,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饵骨,死亡現(xiàn)場離奇詭異翘悉,居然都是意外死亡茫打,警方通過查閱死者的電腦和手機居触,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來老赤,“玉大人轮洋,你說我怎么就攤上這事√” “怎么了弊予?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長开财。 經(jīng)常有香客問我汉柒,道長误褪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任碾褂,我火速辦了婚禮兽间,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘正塌。我一直安慰自己嘀略,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布乓诽。 她就那樣靜靜地躺著帜羊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鸠天。 梳的紋絲不亂的頭發(fā)上讼育,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音粮宛,去河邊找鬼窥淆。 笑死,一個胖子當著我的面吹牛巍杈,可吹牛的內(nèi)容都是我干的忧饭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼筷畦,長吁一口氣:“原來是場噩夢啊……” “哼词裤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鳖宾,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吼砂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鼎文,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體渔肩,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年拇惋,在試婚紗的時候發(fā)現(xiàn)自己被綠了周偎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡撑帖,死狀恐怖蓉坎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胡嘿,我是刑警寧澤蛉艾,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響勿侯,放射性物質(zhì)發(fā)生泄漏拓瞪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一助琐、第九天 我趴在偏房一處隱蔽的房頂上張望吴藻。 院中可真熱鬧,春花似錦弓柱、人聲如沸沟堡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽航罗。三九已至,卻和暖如春屁药,著一層夾襖步出監(jiān)牢的瞬間粥血,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工酿箭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留复亏,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓缭嫡,卻偏偏與公主長得像缔御,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妇蛀,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容