Flutter - Widget
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,
));
}
}
PerformLayout
通过上图就可以知道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
;