Flutter樣式和佈局控件簡析(二)

開始

繼續接着分析Flutter相關的樣式和佈局控件,可是此次內容難度感受比較高,怕有分析不到位的地方,因此此次僅僅當作一個參考,你們最好能夠本身閱讀一下代碼,應該會有更深的體會。web

Sliver佈局

Flutter存在着兩大布局體系(就目前分析),一個是Box佈局,還有另一個就是Sliver佈局;可是Sliver佈局明顯比Box會更加複雜,這真是一個坎,那麼爲啥說Sliver更加複雜尼,請看一下對比:
首先是Box佈局,主要看輸入的BoxConstraints(約束)和輸出Size(尺寸)app

class BoxConstraints extends Constraints {
    const BoxConstraints({
        this.minWidth: 0.0,
        this.maxWidth: double.infinity,
        this.minHeight: 0.0,
        this.maxHeight: double.infinity
      });
  }
class Size extends OffsetBase {
    const Size(double width, double height) : super(width, height);
}

而Sliver佈局,SliverConstraints(約束)和輸出SliverGeometry佈局

class SliverConstraints extends Constraints {
const SliverConstraints({
    @required this.axisDirection,
    @required this.growthDirection,
    @required this.userScrollDirection,
    @required this.scrollOffset,
    @required this.overlap,
    @required this.remainingPaintExtent,
    @required this.crossAxisExtent,
    @required this.crossAxisDirection,
    @required this.viewportMainAxisExtent,
  }) 
}
class SliverGeometry extends Diagnosticable {
    const SliverGeometry({
        this.scrollExtent: 0.0,
        this.paintExtent: 0.0,
        this.paintOrigin: 0.0,
        double layoutExtent,
        this.maxPaintExtent: 0.0,
        this.maxScrollObstructionExtent: 0.0,
        double hitTestExtent,
        bool visible,
        this.hasVisualOverflow: false,
        this.scrollOffsetCorrection,
      })
}

二者一對比,Box佈局明顯參數更少,也更直觀:maxWidth,width,minWidth這些一看就明白其起到的做用;可是Sliver佈局不管輸入輸出都是一大堆參數,這些參數究竟起到什麼做用,爲何須要這些參數,不看代碼真的很難明白。性能

Viewport組件

其實介紹Sliver佈局,必須得先介紹Viewport組件,由於Sliver相關組件須要在Viewport組件下使用,而Viewport組件的主要做用就是提供滾動機制,能夠根據傳入的offset參數來顯示特定的內容;在Flutter中並不像web只需在每一個元素樣式上加上overflow: auto,元素內容就能夠自動滾動,這是由於Flutter主要一個思想就是萬物皆組件,不管樣式仍是佈局或者功能都是以組件形式出現。ui

class Viewport extends MultiChildRenderObjectWidget {
    Viewport({
        Key key,
        this.axisDirection: AxisDirection.down, //主軸方向,默認往下
        this.crossAxisDirection, //縱軸方向
        this.anchor: 0.0, //決定scrollOffset = 0分割線在viewport的位置(0 <= anchor <= 1.0)
        @required this.offset, //viewport偏移位置
        this.center, //標記哪一個做爲center組件
        List<Widget> slivers: const <Widget>[], //sliver組件雙向列表
      })
  }

雖然簡單描述了各個參數的做用,可是仍是不夠直觀。。。仍是畫圖吧:this

clipboard.png
首先上圖整個能夠看到Center參數的做用能夠標出整個列表應該以哪一個組件爲基線來佈局,Center組件始終在scrollOffset = 0.0的初始線上開始佈局,而anchor參數則能夠控制scrollOffset = 0.0這個初始線在Viewport上的位置,這裏設置的是0.3,因此初始線的位置是距離頂端506 * .3 = 151.8這個位置上放置的。spa

雖然這樣好像把參數的做用都搞清楚了,可是仍然沒有知道爲何須要這些參數,繼續深刻RenderViewport,瞭解一下佈局的核心。
直接跳到performLayout方法:debug

void performLayout() {
    ...
     final double centerOffsetAdjustment = center.centerOffsetAdjustment;

    double correction;
    int count = 0;
    do {
      assert(offset.pixels != null);
      correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
      if (correction != 0.0) {
        offset.correctBy(correction);
      } else {
        if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           ))
          break;
      }
      count += 1;
    } while (count < _kMaxLayoutCycles);

這裏能夠注意到performLayout裏面存在一個循環,只要哪一個元素佈局的過程當中須要調整滾動的偏移量,就會更新滾動偏移量以後再從新佈局,可是從新佈局的次數不能超過_kMaxLayoutCycles也就是10次,這裏也是明顯從性能考慮;
另外Center組件還有一個centerOffsetAdjustment屬性,例如centerOffsetAdjustment爲50.0的時候,Center組件就會再原來基礎上往上50.0,可是這裏的處理能夠看到只是等同於改變了滾動偏移量,增長50.0的偏移位置,所作到的效果。code

而後直接把Viewport的寬高和調整後的滾動偏移量傳入_attemptLayout方法:orm

double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
    _minScrollExtent = 0.0;
    _maxScrollExtent = 0.0;
    _hasVisualOverflow = false;
  
    final double centerOffset = mainAxisExtent * anchor - correctedOffset;
    final double clampedForwardCenter = math.max(0.0, math.min(mainAxisExtent, centerOffset));
    final double clampedReverseCenter = math.max(0.0, math.min(mainAxisExtent, mainAxisExtent - centerOffset));

    final RenderSliver leadingNegativeChild = childBefore(center);

    if (leadingNegativeChild != null) {
      // negative scroll offsets
      final double result = layoutChildSequence(
        leadingNegativeChild,
        math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
        0.0,
        clampedReverseCenter,
        clampedForwardCenter,
        mainAxisExtent,
        crossAxisExtent,
        GrowthDirection.reverse,
        childBefore,
      );
      if (result != 0.0)
        return -result;
    }

    // positive scroll offsets
    return layoutChildSequence(
      center,
      math.max(0.0, -centerOffset),
      leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
      clampedForwardCenter,
      clampedReverseCenter,
      mainAxisExtent,
      crossAxisExtent,
      GrowthDirection.forward,
      childAfter,
    );
  }

這裏先提早說一下兩個關鍵屬性layoutOffset和remainingPaintExtent:

clipboard.png

layoutOffset表示組件在Viewport中偏移多少距離纔開始佈局,而remainingPaintExtent表示在Viewport中剩餘繪製區域大小,一旦remainingPaintExtent爲0的時候,控件是不須要繪製的,由於就算繪製了用戶也看不到。

而這幾行代碼:

final double centerOffset = mainAxisExtent * anchor - correctedOffset;
final double clampedForwardCenter = math.max(0.0, math.min(mainAxisExtent, centerOffset));
final double clampedReverseCenter = math.max(0.0, math.min(mainAxisExtent, mainAxisExtent - centerOffset));

就是計算這兩個關鍵屬性過程,能夠假設centerOffset爲0.0的時候,clampedForwardCenter就等於0.0,clampedReverseCenter 等於 mainAxisExtent;因此也就等於layoutOffset等於0.0,remainingPaintExtent等於mainAxisExtent。

接着分析,當Center組件前面還有組件的時候,就會進入剛纔代碼的處理流程:

if (leadingNegativeChild != null) {
      // negative scroll offsets
  final double result = layoutChildSequence(
    leadingNegativeChild,
    math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
    0.0,
    clampedReverseCenter,
    clampedForwardCenter,
    mainAxisExtent,
    crossAxisExtent,
    GrowthDirection.reverse,
    childBefore,
  );
  if (result != 0.0)
    return -result;
}

Center前面的組件會一個接一個佈局,可是對於Center前面的組件,剛纔描述layoutOffset和remainingPaintExtent的圖得要倒着來看,也就是說會變成這樣:

clipboard.png

因此Center組件其實就是一個分割線把內容分紅上下兩部分,一部分順着Viewport主軸方向,另一部分是反主軸的方向發展的,再看看layoutChildSequence方法:

double layoutChildSequence(
    RenderSliver child,
    double scrollOffset,
    double overlap,
    double layoutOffset,
    double remainingPaintExtent,
    double mainAxisExtent,
    double crossAxisExtent,
    GrowthDirection growthDirection,
    RenderSliver advance(RenderSliver child),
  ) {
    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;
    while (child != null) {
      assert(scrollOffset >= 0.0);
      child.layout(new SliverConstraints(
        axisDirection: axisDirection,
        growthDirection: growthDirection,
        userScrollDirection: adjustedUserScrollDirection,
        scrollOffset: scrollOffset,
        overlap: maxPaintOffset - layoutOffset,
        remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
        crossAxisExtent: crossAxisExtent,
        crossAxisDirection: crossAxisDirection,
        viewportMainAxisExtent: mainAxisExtent,
      ), 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;
      updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
      maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
      scrollOffset -= childLayoutGeometry.scrollExtent;
      layoutOffset += childLayoutGeometry.layoutExtent;

      if (scrollOffset <= 0.0)
        scrollOffset = 0.0;

      updateOutOfBandData(growthDirection, childLayoutGeometry);

      // move on to the next child
      child = advance(child);
    }

    // we made it without a correction, whee!
    return 0.0;
  }

這個方法比較長,並且無法精簡了。
scrollOffset屬性表示超出Viewport邊界的距離,這裏能夠看到傳進來的scrollOffset是必須大於等於0,也就是說scrollOffset其實等同於web的scrollTop屬性了,可是若是scrollOffset大於0的時候,layoutOffset必然是等於0,remainingPaintExtent必然等於mainAxisExtent,只要聯想一下剛纔的圖的就能夠推出他們的關係了。

關於SliverConstraints.overlap屬性,指前一個Sliver組件的layoutExtent(佈局區域)和paintExtent(繪製區域)重疊了。

clipboard.png
這裏紅色部分比綠色部分多出地方及時overlap的大小

可是也受SliverGeometry.paintOrigin影響,因此必須計算在內:

clipboard.png

因此這裏計算是這樣:首先layoutOffset + paintOrigin + paintExtent = maxPaintOffset;再layoutOffset += layoutExtent;最後maxPintOffset - layoutOffset = 下個sliver的overlap。

final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;
  maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
  scrollOffset -= childLayoutGeometry.scrollExtent;
  layoutOffset += childLayoutGeometry.layoutExtent;

而layoutOffset不停增長,最終致使remainingPaintExtent變成0.0,也就是告訴Sliver無需繪製了,而remainingPaintExtent爲0.0的Sliver,最終計算的SliverGeometry的paintExtent和layoutExtent通常都是0.0,惟有scrollExtent不能爲0.0,由於這個值須要加起來,決定下次是否可以繼續滾動。

還有SliverGeometry.scrollOffsetCorrection屬性的做用,這個值只要返回不是0.0,就會觸發Viewport根據這個值修正偏移量後從新佈局(這裏存在的一個用途多是滑動翻頁的時候每次都能定位每一頁的開始)

結束?

固然沒有,下次接着寫,Sliver佈局還有挺多能夠挖掘的地方,今天先到這裏。

相關文章
相關標籤/搜索