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),
)
],
)
文章匯總
Flutter 詳解(一、深入了解狀態管理--ScopeModel