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),
)
],
)
