Flutter中一切都是Widget构成,Widget是不可变的,每个Widget的状态都代表了一帧。
Flutter中一切都是Widget构成,Widget是不可变的,每个Widget的状态都代表了一帧。
Flutter中一切都是Widget构成,Widget是不可变的,每个Widget的状态都代表了一帧。
由于Widge不可变的特性,所以Widget必须是轻量级,不可能是真正的绘制对象。那UI是如何绘制到屏幕之上的呢?
Element
比如要显示一行字符串到屏幕上
@override
Widget build(BuildContext context) {
return Text("Hello");
}
当程序运行起来之后,首先会根据Widget创建对应的Element,然后Element通过Widget的状态信息(比如大小、位置、文本等),最终转化为RenderObject对象绘制。
所以Widget的定位更像是描述文件,他并不负责绘制等相关内容。而RenderObject只负责绘制,是真正意义上的View。Element负责管理,比如视图的加载、更新操作都由他处理。
Element除了负责做管理者以外,还具有存储属性,比如StatefulElement中的State,就是在StatefulElement中初始化的时候被创建并保存,从而实现了跨Widget的状态恢复功能。
下面代码代码删除了一些判断相关和不影响阅读的部分
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
_state._element = this;
_state._widget = widget;
}
/// 可以看到这里调用的是_state.build
@override
Widget build() => _state.build(this);
State<StatefulWidget> get state => _state;
State<StatefulWidget> _state;
@override
void reassemble() {
state.reassemble();
super.reassemble();
}
/// 第一次build
@override
void _firstBuild() {
try {
final dynamic debugCheckForReturnedFuture = _state.initState() as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
_state.didChangeDependencies();
super._firstBuild();
}
/// 重新构建
@override
void performRebuild() {
if (_didChangeDependencies) {
_state.didChangeDependencies();
_didChangeDependencies = false;
}
super.performRebuild();
}
/// 更新
@override
void update(StatefulWidget newWidget) {
super.update(newWidget);
final StatefulWidget oldWidget = _state._widget;
_dirty = true;
_state._widget = widget as StatefulWidget;
try {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
rebuild();
}
@override
void activate() {
super.activate();
markNeedsBuild();
}
@override
void deactivate() {
_state.deactivate();
super.deactivate();
}
@override
void unmount() {
super.unmount();
_state.dispose();
_state._element = null;
_state = null;
}
@Deprecated(
'Use dependOnInheritedElement instead. '
'This feature was deprecated after v1.12.1.'
)
@override
InheritedWidget inheritFromElement(Element ancestor, { Object aspect }) {
return dependOnInheritedElement(ancestor, aspect: aspect);
}
@override
InheritedWidget dependOnInheritedElement(Element ancestor, { Object aspect }) {
return super.dependOnInheritedElement(ancestor as InheritedElement, aspect: aspect);
}
bool _didChangeDependencies = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_didChangeDependencies = true;
}
@override
DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) {
return _ElementDiagnosticableTreeNode(
name: name,
value: this,
style: style,
stateful: true,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<State<StatefulWidget>>('state', state, defaultValue: null));
}
}
可能你会觉得你没有用过Element,但其实你应该已经用过很多次啦,Element是BuildContext的实现类,所以我们才可以在context中获取到一些存储的信息。
在Flutter中并不是所有的Element都具备RenderObject,仅当Element的子类是RenderObjectElement是才具备RenderObject,如果子类是ComponentElement时则不再RenderObject。
一般如:Padding、Flex、Text等Widget的Element属于RenderObjectElement;而我们常用的StatelessWidget和StatefulElement他们属于ComponentElement,并不具备RenderObject。
那你可能就有疑惑了,ComponentElement是怎么刷新的呢?答案是通过:Widget.build()
Element总结
Widget作为配置文件描述如何渲染界面,多个Widget在一起够成Widget Tree(小部件树);而Element表示Widget Tree中的特定位置的实例,多个Element在mount之后,会构成Element Tree;Element在mount之后才算是激活,激活之后如果Element存在RenderObject,Element就会通过Widget的createRenderObject方法创建对应的RenderObject,并与Element一一绑定。
RenderObject
RenderObject是真正的绘制对象,我们的UI如何绘制就是由他控制,我们可以根据Widget对应的RenderObject查看某个Widget的绘制对象。
但是由于RenderObject只实现了最基本的layout和paint等相关功能,而绘制到屏幕上面还需要坐标体系和布局协议。所以我们在多数情况会用它的子类,RenderBox或RenderSliver。两者的区别就是Sliver用于可滑动的的控件内,例如:ListView、GridView,其他都基本都属于RenderBox。
RenderBox
abstract class RenderBox extends RenderObject {
/// 把ParentData转化为BoxParentData
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! BoxParentData)
child.parentData = BoxParentData();
}
/// 计算最小的宽度
@protected
double computeMinIntrinsicWidth(double height) {
return 0.0;
}
/// 计算最大的宽度
@protected
double computeMaxIntrinsicWidth(double height) {
return 0.0;
}
/// 计算最小的高度
@protected
double computeMinIntrinsicHeight(double width) {
return 0.0;
}
/// 计算最大的高度
@protected
double computeMaxIntrinsicHeight(double width) {
return 0.0;
}
/// 从父类接受的布局约束,一般控件在嵌套的时候是需要根据parent的布局来动态调整自身大小的
@override
BoxConstraints get constraints => super.constraints as BoxConstraints;
/// 计算基线,得到y轴的偏移量
@protected
double? computeDistanceToActualBaseline(TextBaseline baseline) { }
/// 执行布局(开始布局)
@override
void performLayout() {}
}
computeMinIntrinsicWidth、computeMaxIntrinsicWidth、computeMinIntrinsicHeight、computeMaxIntrinsicHeight这个几个方法值会根据子类对象决定的。同时他们也不是主动调用了,而是通过各自的get方法去获取在调用,然后缓存结果。
我们通过RenderPadding实现细节可以了解
class RenderPadding extends RenderShiftedBox {
/// Creates a render object that insets its child.
///
/// The [padding] argument must not be null and must have non-negative insets.
RenderPadding({
required EdgeInsetsGeometry padding,
TextDirection? textDirection,
RenderBox? child,
}) : assert(padding != null),
assert(padding.isNonNegative),
_textDirection = textDirection,
_padding = padding,
super(child);
EdgeInsets? _resolvedPadding;
void _resolve() {
if (_resolvedPadding != null)
return;
_resolvedPadding = padding.resolve(textDirection);
assert(_resolvedPadding!.isNonNegative);
}
@override
double computeMinIntrinsicWidth(double height) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
/// 这里如果child不为空,通过子类的getMinIntrinsicWidth方法获取
if (child != null) // next line relies on double.infinity absorption
return child!.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
return totalHorizontalPadding;
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
_resolve();
assert(_resolvedPadding != null);
/// 如果没有child,那么通过自己就可以得出size
if (child == null) {
size = constraints.constrain(Size(
_resolvedPadding!.left + _resolvedPadding!.right,
_resolvedPadding!.top + _resolvedPadding!.bottom,
));
return;
}
/// 有child的情况,减去padding
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!);
/// 通过child的layout方法,去计算size
child!.layout(innerConstraints, parentUsesSize: true);
/// 得到child计算完的数据
final BoxParentData childParentData = child!.parentData as BoxParentData;
/// 计算偏移量
childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top);
/// 得到size
size = constraints.constrain(Size(
_resolvedPadding!.left + child!.size.width + _resolvedPadding!.right,
_resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom,
));
}
}

通过上图就可以知道Widget是如何通过约束去获取大小。
RenderPadding并没有实现paint方法,因为其继承的是RenderShiftedBox,在RenderShiftedBox内部实现了paint方法,paint方法何时调用并不会给到用户去处理,需要更新绘制的时候,必须通过markNeedsPaint触发界面执行paint绘制。
渲染图层Layer
当调用markNeedsPaint()触发界面重绘是,markNeedsPaint会通过requestVisualUpdate方法触发引擎更新绘制页面,最终通过RenderBinding的drawFrame开始执行RenderObject的paint方法。
下面代码删除了断言部分的实现
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
/// 根据isRepaintBoundary属性去判断要更新哪些区域,如果为YES 就判断owner是否为空,不为空就把自身加入到绘制区域中,然后开始向下绘制
if (isRepaintBoundary) {
assert(_layer is OffsetLayer);
if (owner != null) {
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
}
} else if (parent is RenderObject) { /// 如果父类是RenderObject的实例对象,就往上查找,看是否需要绘制
final RenderObject parent = this.parent as RenderObject;
parent.markNeedsPaint();
} else {
/// 直接向下绘制
if (owner != null)
owner!.requestVisualUpdate();
}
}
通过源码可以发现isRepaintBoundary是一个get字段,如果一个renderObject需要频繁绘制,那么就可以直接设置为YES,优化性能。
当绘制区域确定时候就会调用pushLayer的方法,其内部会调用createChildContext得到PaintingContext,然后根据childContext和offset去进行绘制图层。所以可以得知PaintingContext和Layer是有关联的,每个Layer上绘制的都是独立的图层。
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) {
assert(painter != null);
// If a layer is being reused, it may already contain children. We remove
// them so that `painter` can add children that are relevant for this frame.
if (childLayer.hasChildren) {
childLayer.removeAllChildren();
}
stopRecordingIfNeeded();
appendLayer(childLayer);
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset);
childContext.stopRecordingIfNeeded();
}
@protected
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
return PaintingContext(childLayer, bounds);
}
为什么push之后的页面不会影响之前的页面?
因为我们在调用Navigator.push(context, MaterialPageRoute)打开的页面,MaterialPageRoute的父类使用了RepaintBoundary嵌套显示,而RepaintBoundary的RenderObject是RenderRepaintBoundary,RenderPEpaintBoundary的isRepaintBoundary正好是true,所以才可以实现路由堆栈内的页面互不干扰,因为他的PaintingContext和Layer不同。
isRepaintBoundary和alawaysNeedsComposition的区别是什么?
两者都会影响Layer的存在,不同的是alwaysNeedsComposition是用于图层混合的,他混合的条件是child != null、alpha != 0、alpha != 255。
/// child不为空 并且 透明度不等于0 不等于 255
@override
bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255);
@override
void paint(PaintingContext context, Offset offset) {
/// 这里进行了优化,如果没有child那么,就不需要进行绘制了
if (child != null) {
/// 不需要绘制
if (_alpha == 0) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
return;
}
/// 不要透明度
if (_alpha == 255) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
context.paintChild(child!, offset);
return;
}
assert(needsCompositing);
/// 得到带透明度的layer
layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
}
}
OpacityLayer pushOpacity(Offset offset, int alpha, PaintingContextCallback painter, { OpacityLayer? oldLayer }) {
final OpacityLayer layer = oldLayer ?? OpacityLayer();
layer
..alpha = alpha
..offset = offset;
pushLayer(layer, painter, Offset.zero);
return layer;
}
Widget、Element、RenderObject、Layer之间的关系?
Widget和Element之间是一对多;
在Element有RenderObject的情况下,Element和RenderObject之间是一对一;
RenderObject和Layer之间是多对一,但不是所有的RenderObject都有Layer;
