Flutter 布局(八)- Stack、IndexedStack、GridView詳解


本文主要介紹Flutter布局中的Stack、IndexedStack、GridView控件,詳細介紹了其布局行為以及使用場景,並對源碼進行了分析。

1. Stack

A widget that positions its children relative to the edges of its box.

1.1 簡介

Stack可以類比web中的absolute,絕對布局。絕對布局一般在移動端開發中用的較少,但是在某些場景下,還是有其作用。當然,能用Stack絕對布局完成的,用其他控件組合也都能實現。

1.2 布局行為

Stack的布局行為,根據child是positioned還是non-positioned來區分。

  • 對於positioned的子節點,它們的位置會根據所設置的top、bottom、right以及left屬性來確定,這幾個值都是相對於Stack的左上角;
  • 對於non-positioned的子節點,它們會根據Stack的aligment來設置位置。

對於繪制child的順序,則是第一個child被繪制在最底端,后面的依次在前一個child的上面,類似於web中的z-index。如果想調整顯示的順序,則可以通過擺放child的順序來進行。

1.3 繼承關系

Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Stack

1.4 示例代碼

Stack(
  alignment: const Alignment(0.6, 0.6),
  children: [
    CircleAvatar(
      backgroundImage: AssetImage('images/pic.jpg'),
      radius: 100.0,
    ),
    Container(
      decoration: BoxDecoration(
        color: Colors.black45,
      ),
      child: Text(
        'Mia B',
        style: TextStyle(
          fontSize: 20.0,
          fontWeight: FontWeight.bold,
          color: Colors.white,
        ),
      ),
    ),
  ],
);

示例代碼我就直接用的Building Layouts in Flutter中的例子,效果如下

Stack例子

1.5 源碼解析

構造函數如下:

Stack({
  Key key,
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})

1.5.1 屬性解析

alignment:對齊方式,默認是左上角(topStart)。

textDirection:文本的方向,絕大部分不需要處理。

fit:定義如何設置non-positioned節點尺寸,默認為loose。

其中StackFit有如下幾種:

  • loose:子節點寬松的取值,可以從min到max的尺寸;
  • expand:子節點盡可能的占用空間,取max尺寸;
  • passthrough:不改變子節點的約束條件。

overflow:超過的部分是否裁剪掉(clipped)。

1.5.2 源碼

Stack的布局代碼有些長,在此分段進行講解。

    1. 如果不包含子節點,則尺寸盡可能大。
if (childCount == 0) {
  size = constraints.biggest;
  return;
}
  • 2.根據fit屬性,設置non-positioned子節點約束條件。
switch (fit) {
  case StackFit.loose:
    nonPositionedConstraints = constraints.loosen();
    break;
  case StackFit.expand:
    nonPositionedConstraints = new BoxConstraints.tight(constraints.biggest);
    break;
  case StackFit.passthrough:
    nonPositionedConstraints = constraints;
    break;
}
  • 3.對non-positioned子節點進行布局。
RenderBox child = firstChild;
while (child != null) {
  final StackParentData childParentData = child.parentData;
  if (!childParentData.isPositioned) {
    hasNonPositionedChildren = true;
    child.layout(nonPositionedConstraints, parentUsesSize: true);
    final Size childSize = child.size;
    width = math.max(width, childSize.width);
    height = math.max(height, childSize.height);
  }
  child = childParentData.nextSibling;
}
  • 4.根據是否包含positioned子節點,對stack進行尺寸調整。
if (hasNonPositionedChildren) {
  size = new Size(width, height);
} else {
  size = constraints.biggest;
}
  • 5.最后對子節點位置的調整,這個調整過程中,則根據alignment、positioned節點的絕對位置等信息,對子節點進行布局。

第一步是根據positioned的絕對位置,計算出約束條件后進行布局。

if (childParentData.left != null && childParentData.right != null)
  childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
else if (childParentData.width != null)
  childConstraints = childConstraints.tighten(width: childParentData.width);

if (childParentData.top != null && childParentData.bottom != null)
  childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
else if (childParentData.height != null)
  childConstraints = childConstraints.tighten(height: childParentData.height);

child.layout(childConstraints, parentUsesSize: true);

第二步則是位置的調整,其中坐標的計算如下:

double x;
if (childParentData.left != null) {
  x = childParentData.left;
} else if (childParentData.right != null) {
  x = size.width - childParentData.right - child.size.width;
} else {
  x = _resolvedAlignment.alongOffset(size - child.size).dx;
}

if (x < 0.0 || x + child.size.width > size.width)
  _hasVisualOverflow = true;

double y;
if (childParentData.top != null) {
  y = childParentData.top;
} else if (childParentData.bottom != null) {
  y = size.height - childParentData.bottom - child.size.height;
} else {
  y = _resolvedAlignment.alongOffset(size - child.size).dy;
}

if (y < 0.0 || y + child.size.height > size.height)
  _hasVisualOverflow = true;

childParentData.offset = new Offset(x, y);

1.6 使用場景

Stack的場景還是比較多的,對於需要疊加顯示的布局,一般都可以使用Stack。有些場景下,也可以被其他控件替代,我們應該選擇開銷較小的控件去實現。

2. IndexedStack

A Stack that shows a single child from a list of children.

2.1 簡介

IndexedStack繼承自Stack,它的作用是顯示第index個child,其他child都是不可見的。所以IndexedStack的尺寸永遠是跟最大的子節點尺寸一致。

2.2 例子

在此還是將Stack的例子稍加改造,將index設置為1,也就是顯示含文本的Container的節點。

Container(
  color: Colors.yellow,
  child: IndexedStack(
    index: 1,
    alignment: const Alignment(0.6, 0.6),
    children: [
      CircleAvatar(
        backgroundImage: AssetImage('images/pic.jpg'),
        radius: 100.0,
      ),
      Container(
        decoration: BoxDecoration(
          color: Colors.black45,
        ),
        child: Text(
          'Mia B',
          style: TextStyle(
            fontSize: 20.0,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
      ),
    ],
  ),
)

IndexedStack例子

2.3 源碼解析

其繪制代碼很簡單,因為繼承自Stack,布局方面表現基本一致,不同之處在於其繪制的時候,只是將第Index個child進行了繪制。

@override
void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null || index == null)
  return;
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);
}

2.4 使用場景

如果需要展示一堆控件中的一個,可以使用IndexedStack。有一定的使用場景,但是也有控件可以實現其功能,只不過操作起來可能會復雜一些。

3. GridView

A scrollable, 2D array of widgets.

3.1 簡介

GridView在移動端上非常的常見,就是一個滾動的多列列表,實際的使用場景也非常的多。

3.2 布局行為

GridView的布局行為不復雜,本身是盡量占滿空間區域,布局行為上完全繼承自ScrollView。

3.3 繼承關系

Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView

從繼承關系看,GridView是在ScrollView的基礎上封裝而來的,這跟移動端的類似。

3.4 示例代碼

GridView.count(
  crossAxisCount: 2,
  children: List.generate(
    100,
    (index) {
      return Center(
        child: Text(
          'Item $index',
          style: Theme.of(context).textTheme.headline,
        ),
      );
    },
  ),
);

示例代碼直接用了Creating a Grid List中的例子,創建了一個2列總共100個子節點的列表。

3.5 源碼解析

默認構造函數如下:

GridView({
  Key key,
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  @required this.gridDelegate,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
})

同時也提供了如下額外的四種構造方法,方便開發者使用。

GridView.builder
GridView.custom
GridView.count
GridView.extent

3.5.1 屬性解析

scrollDirection:滾動的方向,有垂直和水平兩種,默認為垂直方向(Axis.vertical)。

reverse:默認是從上或者左向下或者右滾動的,這個屬性控制是否反向,默認值為false,不反向滾動。

controller:控制child滾動時候的位置。

primary:是否是與父節點的PrimaryScrollController所關聯的主滾動視圖。

physics:滾動的視圖如何響應用戶的輸入。

shrinkWrap:滾動方向的滾動視圖內容是否應該由正在查看的內容所決定。

padding:四周的空白區域。

gridDelegate:控制GridView中子節點布局的delegate。

cacheExtent:緩存區域。

3.5.2 源碼

@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 = new Scrollable(
    axisDirection: axisDirection,
    controller: scrollController,
    physics: physics,
    viewportBuilder: (BuildContext context, ViewportOffset offset) {
      return buildViewport(context, offset, axisDirection, slivers);
    },
  );
  return primary && scrollController != null
    ? new PrimaryScrollController.none(child: scrollable)
    : scrollable;
}

上面這段代碼是ScrollView的build方法,GridView就是一個特殊的ScrollView。GridView本身代碼沒有什么,基本上都是ScrollView上的東西,主要會涉及到Scrollable、Sliver、Viewport等內容,這些內容比較多,因此源碼就先略了,后面單獨出一篇文章對ScrollView進行分析吧。

3.6 使用場景

使用場景很多,非常常見的控件。也有控件可以實現其功能,例如官方說的,GridView實際上是一個silvers只包含一個SilverGrid的CustomScrollView。

4. 后話

筆者建了一個Flutter學習相關的項目,Github地址,里面包含了筆者寫的關於Flutter學習相關的一些文章,會定期更新,也會上傳一些學習Demo,歡迎大家關注。

5. 參考

  1. Stack class
  2. IndexedStack class
  3. GridView class
  4. ScrollView class


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM