本文主要介紹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中的例子,效果如下
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的佈局程式碼有些長,在此分段進行講解。
-
- 如果不包含子節點,則尺寸儘可能大。
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,
),
),
),
],
),
)
複製程式碼
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,歡迎大家關注。