Flutter 详解(八、深入了解布局)
Widget
、Element
、RenderObject
三者之间的关系在已经讲解过,其中我们最为熟知的 Widget
,究竟是通过什么样的方式来实现代码搭积木实现建造房子呢?
单子元素布局--SingleChildRenderObjectWidget
Container
Container
是继承StatelessWidget
,那么build
是构建布局关键函数,在Container
中build中,掺杂了很多其他的部件,Align
、Padding
、ColoredBox
、DecorateBox
、Transform
…等等,每个关于布局或者样式的属性,最后都被转化成其他的box
来呈现。看下官方源码:
@override
Widget build(BuildContext context) {
Widget current = child;
if (child == null && (constraints == null || !constraints.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
/// 封装 Align
if (alignment != null)
current = Align(alignment: alignment, child: current);
final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
/// 封装 Padding
current = Padding(padding: effectivePadding, child: current);
if (color != null)
/// 封装 ColoredBox
current = ColoredBox(color: color, child: current);
/// 封装DecoratedBox
if (decoration != null)
current = DecoratedBox(decoration: decoration, child: current);
/// 封装 DecoratedBox
if (foregroundDecoration != null) {
current = DecoratedBox(
decoration: foregroundDecoration,
position: DecorationPosition.foreground,
child: current,
);
}
/// 封装 ConstrainedBox
if (constraints != null)
current = ConstrainedBox(constraints: constraints, child: current);
/// 封装 Padding
if (margin != null)
current = Padding(padding: margin, child: current);
/// 封装 Transform
if (transform != null)
current = Transform(transform: transform, child: current);
/// 封装 ClipPath
if (clipBehavior != Clip.none) {
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.of(context),
decoration: decoration
),
clipBehavior: clipBehavior,
child: current,
);
}
return current;
}
Padding/Transform/ConstraineBox...
都是继承SingleChildRenderObjectWidget
,通过SingleChildRenderObjectWidget
实现的布局。
Padding
通过创建渲染实例RenderPadding
,在更新实例的时候,更新该实例的属性。Align
通过创建RenderPositionedBox
来实现渲染实例,其他的如下表所示:
部件 | 渲染对象 |
---|---|
Padding | RenderPadding |
Align | RenderPositionedBox |
ColoredBox | _RenderColoredBox |
DecoratedBox | RenderDecoratedBox |
ConstrainedBox | RenderConstrainedBox |
Transform | RenderTransform |
ClipPath | RenderClipPath |
他们有一个共同点都是继承SingleChildRenderObjectWidget
,而createRenderObject
返回了不同的渲染对象RenderBox
,RenderBox
最终实现位置偏移和大小,都是通过RenderBox
来实现的,所以找每个组件的RenderBox
的实现就可以看到他们是怎么布局的。
Padding
是一个继承RenderPadding
,RenderPadding
继承了RenderShiftedBox
,RenderShiftedBox
继承了RenderBox
,那么我们就拿Padding
举例子讲解下。
在RenderShiftedBox
实现了获取组件的宽度和高度,子组件为空,则返回0.0
,这里实现了computeMinIntrinsicWidth
、computeMaxIntrinsicWidth
、computeMinIntrinsicHeight
、computeMaxIntrinsicHeight
和最终绘画函数paint(PaintingContext context, Offset offset)
,把UI
绘画出来。
abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
/// Initializes the [child] property for subclasses.
RenderShiftedBox(RenderBox child) {
this.child = child;
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null)
return child.getMinIntrinsicWidth(height);
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null)
return child.getMaxIntrinsicWidth(height);
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null)
return child.getMinIntrinsicHeight(width);
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null)
return child.getMaxIntrinsicHeight(width);
return 0.0;
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
double result;
if (child != null) {
assert(!debugNeedsLayout);
result = child.getDistanceToActualBaseline(baseline);
final BoxParentData childParentData = child.parentData as BoxParentData;
if (result != null)
result += childParentData.offset.dy;
} else {
result = super.computeDistanceToActualBaseline(baseline);
}
return result;
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child.parentData as BoxParentData;
context.paintChild(child, childParentData.offset + offset);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
if (child != null) {
final BoxParentData childParentData = child.parentData as BoxParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child.hitTest(result, position: transformed);
},
);
}
return false;
}
}
我们通过Padding
来看下源码如何实现布局的:
@override
double computeMinIntrinsicWidth(double height) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
if (child != null) // next line relies on double.infinity absorption
return child.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
return totalHorizontalPadding;
}
这里通过getMinIntrinsicWidth()
来获取最小宽度。
@override
double computeMaxIntrinsicWidth(double height) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding.left + _resolvedPadding.right;
final double totalVerticalPadding = _resolvedPadding.top + _resolvedPadding.bottom;
if (child != null) // next line relies on double.infinity absorption
return child.getMaxIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
return totalHorizontalPadding;
}
通过getMaxIntrinsicWidth
获获得最大宽度,通过computeMaxIntrinsicHeight
获取最大高度,通过computeMinIntrinsicHeight
最小高度,当高度和宽度都计算了之后,然后进行布局。
那么作为开发者可以自己通过RenderBox
来实现一个新的布局组件吗?当然是可以的,只要你觉得有必要的话。否则官方提供的布局组件足够满足我们的使用了。
其实官方已提供一个抽象接口SingleChildLayoutDelegate
,让开发者自己实现一个布局。
abstract class SingleChildLayoutDelegate {
/// 监听
final Listenable _relayout;
/// 获取大小
Size getSize(BoxConstraints constraints) => constraints.biggest;
/// 获取子部件的约束
BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints;
/// 获取子部件的位置
Offset getPositionForChild(Size size, Size childSize) => Offset.zero;
/// 是否需要更新
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate);
}
多子元素 MultiChildRenderObjectWidget
多子元素和单子元素基本一致,Row
、Column
继承了Flex
,Flex
继承了MultiChildRenderObjectWidget
,MultiChildRenderObjectWidget
中的RenderFlex
通过继承RenderBox
来实现的布局样式。
部件 | 渲染对象 |
---|---|
Column、Row、Flex | RenderFlex |
Stack | RenderStack |
Flow | RenderFlow |
Wrap | RenderWrap |
同样的 多子元素也提供了供开发者自己实现布局的抽象接口CustomMultiChildLayout
和MultiChildLayoutDelegate
.
滑动 多子布局
滑动布局也是多子布局的一种,如各种ListView
、GridView
、Customview
他们在实现过程很复杂,从下面的一个流程我们可以大致了解他们的关系。
由上图我们可以知道,最终会产生两个渲染对象RenderObject
:
并且从 RenderViewport
的说明我们了解到,RenderViewport
内部是不能直接放置 RenderBox
,需要通过 RenderSliver
让大家族来完成布局。而从源码可以了解到:RenderViewport
对应的 Widget Viewport
就是一个 MultiChildRenderObjectWidget
。
再稍微说下上图的流程:
ListView、Pageview、GridView
等都是通过 Scrollable
、 ViewPort
、Sliver
大家族实现的效果。这里简单总结下就是:一个“可滑动”的控件,嵌套了一个“视觉窗口”,然后内部通过“碎片”展示 children
。
不同的是 PageView
没有继承 SrollView
,而是直接通过 NotificationListener
和 ScrollNotification
嵌套实现。
官方同样提供的自定义滑动 CustomScrollView
,它继承了 ScrollView
,可通过 slivers
参数实现子控件布局, slivers
是通过 Scrollable
的 buildViewport
添加到 ViewPort
中,如下代码所示:
CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return Text('$index');
}, childCount: 10),
),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
delegate: SliverChildBuilderDelegate((context, index) {
return Text('$index');
}, childCount: 12),
)
],
)