開始
在Android中我們要實現一個布局需要繼承ViewGroup
, 重寫其中的onLayout
和onMeasure
方法. 其中onLayout負責給子控件設置布局區域, onMeaseure度量子控件大小和自身大小. 今天我們就研究下Flutter是如何實現布局的.
Flutter布局
首先我們挑選一個Flutter控件去看源碼, 我們就選Stack
, 因為它足夠簡單. 從表象上講它只要重疊擺放一組子控件即可. 先看下Stack的源碼:
class Stack extends MultiChildRenderObjectWidget {
Stack({
Key key,
this.alignment: AlignmentDirectional.topStart,
this.textDirection,
this.fit: StackFit.loose,
this.overflow: Overflow.clip,
List<Widget> children: const <Widget>[],
}) : super(key: key, children: children);
final AlignmentGeometry alignment;
final StackFit fit;
final Overflow overflow;
@override
RenderStack createRenderObject(BuildContext context) {
return new RenderStack(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
overflow: overflow,
);
}
@override
void updateRenderObject(BuildContext context, RenderStack renderObject) {
renderObject
..alignment = alignment
..textDirection = textDirection ?? Directionality.of(context)
..fit = fit
..overflow = overflow;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new EnumProperty<StackFit>('fit', fit));
properties.add(new EnumProperty<Overflow>('overflow', overflow));
}
}
Stack
繼承自MultiChildRenderObjectWidget
, 重寫了createRenderObject
其返回了一個RenderStack
對象, 實際的工作者. 而updateRenderObject
則只是修改RenderStack
對象的屬性. debugFillProperties
方法則是填充該類屬性的參數值到DiagnosticPropertiesBuilder
中.
我們看看Flex
, 也是如此, 重寫了createRenderObject
其返回了一個RenderFlex
對象, 實際的工作者. 而updateRenderObject
則只是修改RenderFlex
對象的屬性.
所以我們接下來看看RenderStack
, 精簡代碼如下:
class RenderStack extends RenderBox
with ContainerRenderObjectMixin<RenderBox, StackParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
RenderStack({
List<RenderBox> children,
AlignmentGeometry alignment: AlignmentDirectional.topStart,
TextDirection textDirection,
StackFit fit: StackFit.loose,
Overflow overflow: Overflow.clip,
}) : assert(alignment != null),
assert(fit != null),
assert(overflow != null),
_alignment = alignment,
_textDirection = textDirection,
_fit = fit,
_overflow = overflow {
addAll(children);
}
bool _hasVisualOverflow = false;
@override
void performLayout() {
_resolve();
assert(_resolvedAlignment != null);
_hasVisualOverflow = false;
bool hasNonPositionedChildren = false;
if (childCount == 0) {
size = constraints.biggest;
assert(size.isFinite);
return;
}
double width = constraints.minWidth;
double height = constraints.minHeight;
BoxConstraints nonPositionedConstraints;
assert(fit != null);
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;
}
assert(nonPositionedConstraints != null);
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;
}
if (hasNonPositionedChildren) {
size = new Size(width, height);
assert(size.width == constraints.constrainWidth(width));
assert(size.height == constraints.constrainHeight(height));
} else {
size = constraints.biggest;
}
assert(size.isFinite);
child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData;
if (!childParentData.isPositioned) {
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
} else {
BoxConstraints childConstraints = const BoxConstraints();
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);
}
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
}
@protected
void paintStack(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
void paint(PaintingContext context, Offset offset) {
if (_overflow == Overflow.clip && _hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintStack);
} else {
paintStack(context, offset);
}
}
}
可以看出RenderStack
接收了所有傳遞給Stack
的參數, 畢竟RenderStack
才是實際干活的^^. performLayout
負責了所有布局相關的工作. performLayout
首先分析StackFit
參數, 該參數有3個值:
- StackFit.loose 按最小的來.
- StackFit.expand 按最大的來.
- StackFit.passthrough
Stack
上層為->Expanded
->Row
, 橫向盡量大, 縱向盡量小.
得出BoxConstraints
. 然后遍歷所有子控件, 如果不是Positioned
類型子控件, 則將BoxConstraints
傳給子控件讓它根據父控件大小自己內部布局. 并且記錄下所有子控件結合RenderStack
自生大小得出的最大高度和寬度. 將其設置為當前控件大小.
接著再繼續從頭遍歷子控件, 如果不是Positioned
類型子控件, 根據alignment
參數, 設置子控件在父控件中的偏移量, 比如Stack
設置了居中, 上面計算出寬100, 高200, 而子控件寬30, 高30, 那么子控件需要偏移x=35, y=85. 如果是Positioned
類型的子控件, 先將RenderStack
的size
大小, 減去Positioned
屬性里的大小. 再來計算便宜量.
這個里面有_hasVisualOverflow
變量, 如果內容超出RenderStack
大小, 其值為true
. 也就是我們寫布局時, 內容超過范圍了, 報出來一個色塊提示, 就是如此得出的.
_overflow
屬性則指定了子控件的繪制區域是否能超過父控件, 跟Android中的clipChildren
屬性很像.
另外我們再分析下IndexedStack
, 該控件一次只能顯示一個子控件. 其實際差異在RenderIndexedStack
class RenderIndexedStack extends RenderStack {
...
@override
bool hitTestChildren(HitTestResult result, { @required Offset position }) {
if (firstChild == null || index == null)
return false;
assert(position != null);
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData;
return child.hitTest(result, position: position - childParentData.offset);
}
@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);
}
...
}
重寫了RenderStack
的paintStack
和hitTestChildren
方法, 只繪制選中的子控件, 和接收事件.
總結
實現一個自定義布局, 我們需要先繼承MultiChildRenderObjectWidget
, 然后重寫createRenderObject
和updateRenderObject
方法, 前者返回我們自定義的RenderBox
的對象. 后者更新想要傳遞的屬性. 然后需要我們繼承RenderBox
, 來擴展我們想要的功能特性.