十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
这篇文章给大家分享的是有关Flutter如何构建、布局及绘制三部曲的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
成都创新互联长期为上千多家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为港闸企业提供专业的网站制作、成都网站设计,港闸网站改版等技术服务。拥有十多年丰富建站经验和众多成功案例,为您定制开发。
前言
学习Fullter也有些时间了,写过不少demo,对一些常用的widget使用也比较熟练,但是总觉得对Flutter的框架没有一个大致的了解,碰到有些细节的地方又没有文档可以查询,例如在写UI时总不知道为什么container添加了child就变小了;widget中key的作用,虽然官方有解释但是凭空来讲的话有点难理解。所以觉得深入一点的了解Flutter框架还是很有必要的。
构建
初次构建
flutter的入口main方法直接调用了runApp(Widget app)
方法,app参数就是我们的根视图的Widget,我们直接跟进runApp方法
void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized()//此方法是对flutter的框架做一些必要的初始化 ..attachRootWidget(app) ..scheduleWarmUpFrame(); }
runApp方法先调用了WidgetsFlutterBinding.ensureInitialized()
方法,这个方法是做一些必要的初始化
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { static WidgetsBinding ensureInitialized() { if (WidgetsBinding.instance == null) WidgetsFlutterBinding(); return WidgetsBinding.instance; } }
WidgetsFlutterBinding混入了不少的其他的Binding
BindingBase 那些单一服务的混入类的基类
GestureBinding framework手势子系统的绑定,处理用户输入事件
ServicesBinding 接受平台的消息将他们转换成二进制消息,用于平台与flutter的通信
SchedulerBinding 调度系统,用于调用Transient callbacks(Window.onBeginFrame
的回调)、Persistent callbacks(Window.onDrawFrame
的回调)、Post-frame callbacks(在Frame结束时只会被调用一次,调用后会被系统移除,在Persistent callbacks后Window.onDrawFrame
回调返回之前执行)
PaintingBinding 绘制库的绑定,主要处理图片缓存
SemanticsBinding 语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持
RendererBinding 渲染树与Flutter engine的桥梁
WidgetsBinding Widget层与Flutter engine的桥梁
以上是这些Binding的主要作用,在此不做过多赘述,WidgetsFlutterBinding.ensureInitialized()
返回的是WidgetsBinding对象,然后马上调用了WidgetsBinding的attachRootWidget(app)
方法,将我们的根视图的Widget对象穿进去,我们继续看attachRootWidget方法
void attachRootWidget(Widget rootWidget) { _renderViewElement = RenderObjectToWidgetAdapter( container: renderView, debugShortDescription: '[root]', child: rootWidget ).attachToRenderTree(buildOwner, renderViewElement); }
创建了一个RenderObjectToWidgetAdapter,让后直接调用它的attachToRenderTree方法,BuildOwner是Widget framework的管理类
RenderObjectToWidgetElementattachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement element]) { if (element == null) { owner.lockState(() { element = createElement(); assert(element != null); element.assignOwner(owner); }); owner.buildScope(element, () { element.mount(null, null); }); } else { element._newWidget = this; element.markNeedsBuild(); } return element; }
element为空,owner先锁定状态,然后调用了RenderObjectToWidgetAdapter的createElement()
返回了RenderObjectToWidgetElement对象,让后将owner赋值给element(assignOwner方法),让后就是owner调用buildScope方法
void buildScope(Element context, [VoidCallback callback]) { if (callback == null && _dirtyElements.isEmpty) return; Timeline.startSync('Build', arguments: timelineWhitelistArguments); try { _scheduledFlushDirtyElements = true; if (callback != null) { _dirtyElementsNeedsResorting = false; try { callback(); } finally {} } ... }
省略了部分以及后续代码,可以看到buildScope方法首先就调用了callback(就是element.mount(null, null)
方法),回到RenderObjectToWidgetElement的mount方法
@override void mount(Element parent, dynamic newSlot) { assert(parent == null); super.mount(parent, newSlot); _rebuild(); }
首先super.mount(parent, newSlot)
调用了RootRenderObjectElement的mount方法(只是判定parent和newSlot都为null),让后又继续向上调用了RenderObjectElement中的mount方法
@override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); _renderObject = widget.createRenderObject(this); attachRenderObject(newSlot); _dirty = false; }
RenderObjectElement中的mount方法又调用了Element的mount方法
@mustCallSuper void mount(Element parent, dynamic newSlot) { _parent = parent; _slot = newSlot; _depth = _parent != null ? _parent.depth + 1 : 1; _active = true; if (parent != null) // Only assign ownership if the parent is non-null _owner = parent.owner; if (widget.key is GlobalKey) { final GlobalKey key = widget.key; key._register(this); } _updateInheritance(); }
Element的mount方法其实就是进行了一些赋值,以确认当前Element在整个树种的位置,让后回到RenderObjectElement中的mount方法,调用了widget.createRenderObject(this)
方法,widget是RenderObjectToWidgetAdapter的实例,它返回的是RenderObjectWithChildMixin对象,让后调用attachRenderObject方法
@override void attachRenderObject(dynamic newSlot) { assert(_ancestorRenderObjectElement == null); _slot = newSlot; _ancestorRenderObjectElement = _findAncestorRenderObjectElement();//获取此RenderObjectElement最近的RenderObjectElement对象 _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);//将renderObject插入RenderObjectElement中 final ParentDataElementparentDataElement = _findAncestorParentDataElement(); if (parentDataElement != null) _updateParentData(parentDataElement.widget); } ///RenderObjectToWidgetElement中的insertChildRenderObject方法,简单将子RenderObject赋值给父RenderObject的child字段 @override void insertChildRenderObject(RenderObject child, dynamic slot) { assert(slot == _rootChildSlot); assert(renderObject.debugValidateChild(child)); renderObject.child = child; }
Element的mount方法确定当前Element在整个树种的位置并插入,RenderObjectElement中的mount方法来创建RenderObject对象并将其插入到渲染树中,让后再回到RenderObjectToWidgetElement方法,mount之后调用_rebuild()方法, _rebuild()
方法中主要是调用了Element的updateChild方法
@protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { if (newWidget == null) {//当子Widget没有的时候,直接将child deactivate掉 if (child != null) deactivateChild(child); return null; } if (child != null) {//有子Element的时候 if (child.widget == newWidget) {//Widget没有改变 if (child.slot != newSlot)//再判断slot有没有改变,没有则不更新slot updateSlotForChild(child, newSlot);//更新child的slot return child;//返回child } if (Widget.canUpdate(child.widget, newWidget)) {//Widget没有改变,再判断Widget能否update,如果能还是重复上面的步骤 if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); return child; } deactivateChild(child);//如果不能更新的话,直接将child deactivate掉,然后在inflateWidget(newWidget, newSlot)创建新的Element } return inflateWidget(newWidget, newSlot);//根据Widget对象以及slot创建新的Element }
由于我们是第一次构建,child是null,所以就直接走到inflateWidget方法创建新的Element对象,跟进inflateWidget方法
@protected Element inflatinflateWidgeteWidget(Widget newWidget, dynamic newSlot) { final Key key = newWidget.key; if (key is GlobalKey) {//newWidget的key是GlobalKey final Element newChild = _retakeInactiveElement(key, newWidget);//复用Inactive状态的Element if (newChild != null) { newChild._activateWithParent(this, newSlot);//activate 此Element(将newChild出入到Element树) final Element updatedChild = updateChild(newChild, newWidget, newSlot);//直接将newChild更新 return updatedChild;//返回更新后的Element } } final Element newChild = newWidget.createElement();//调用createElement()进行创建 newChild.mount(this, newSlot);//继续调用newChild Element的mount方法(如此就行一直递归下去,当递归完成,整个构建过程也就结束了) return newChild;//返回子Element }
inflateWidget中其实就是通过Widget得到Element对象,让后继续调用子Element的mount的方将进行递归。
不同的Element,mount的实现会有所不同,我们看一下比较常用的StatelessElement、StatefulElement,他们的mount方法实现在ComponentElement中
@override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); _firstBuild(); } void _firstBuild() { rebuild();//调用了Element的rebuild()方法 } //Element的rebuild方法,通常被三处地方调用 //1.当BuildOwner.scheduleBuildFor被调用标记此Element为dirty时 //2.当Element第一次构建由mount方法去调用 //3.当Widget改变时,被update方法调用 void rebuild() { if (!_active || !_dirty) return; performRebuild();//调用performRebuild方法(抽象方法) } //ComponentElement的performRebuild实现 @override void performRebuild() { Widget built; try { built = build();//构建Widget(StatelessElement直接调用build方法,StatefulElement直接调用state.build方法) } catch (e, stack) { built = ErrorWidget.builder(_debugReportException('building $this', e, stack));//有错误的化就创建一个ErrorWidget } finally { _dirty = false; } try { _child = updateChild(_child, built, slot);//让后还是根据Wdiget来更新子Element } catch (e, stack) { built = ErrorWidget.builder(_debugReportException('building $this', e, stack)); _child = updateChild(null, built, slot); } }
再看一看MultiChildRenderObjectElement的mount方法
@override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); _children = List(widget.children.length); Element previousChild; for (int i = 0; i < _children.length; i += 1) { final Element newChild = inflateWidget(widget.children[i], previousChild);//遍历children直接inflate根据Widget创建新的Element _children[i] = newChild; previousChild = newChild; } }
可以看到不同的Element构建方式会有些不同,Element(第一层Element)的mount方法主要是确定当前Element在整个树种的位置并插入;ComponentElement(第二层)的mount方法先构建Widget树,让后再递归更新(包括重用,更新,直接创建inflate)其Element树;RenderObjectElement(第二层)中的mount方法来创建RenderObject对象并将其插入到渲染树中。MultiChildRenderObjectElement(RenderObjectElement的子类)在RenderObjectElement还要继续创建children Element。
总结:首先是由WidgetBinding创建RenderObjectToWidgetAdapter然后调用它的attachToRenderTree方法,创建了RenderObjectToWidgetElement对象,让后将它mount(调用mount方法),mount方法中调用的_rebuild,继而调用updateChild方法,updateChild会进行递归的更新Element树,若child没有则需要重新创建新的Element,让后将其mount进Element树中(如果是RenderobjectElement的化,mount的过程中会去创建RenderObject对象,并插入到RenderTree)。
通过setState触发构建
通常我们在应用中要更新状态都是通过State中的setState方法来触发界面重绘,setState方法就是先调用了callback让后调用该State的Element对象的markNeedsBuild方法,markNeedsBuild中将Element标记为dirty并通过BuildOwner将其添加到dirty列表中并调用onBuildScheduled回调(在WidgetsBinding初始化时设置的,它回去调用window.scheduleFrame
方法),让后window的onBeginFrame,onDrawFrame回调(在SchedulerBinding初始化时设置的,这两个回调会执行一些callback)会被调用,SchedulerBinding通过persisterCallbacks来调用到BuildOwner中buildScope方法。上面我们只看了buildScope的一部分,当通过setState方法来触发界面重绘时,buildScope的callBack为null
void buildScope(Element context, [VoidCallback callback]) { if (callback == null && _dirtyElements.isEmpty) return; Timeline.startSync('Build', arguments: timelineWhitelistArguments); try { _scheduledFlushDirtyElements = true; if (callback != null) { Element debugPreviousBuildTarget; _dirtyElementsNeedsResorting = false; try { callback();//调用callback } finally {} } _dirtyElements.sort(Element._sort); _dirtyElementsNeedsResorting = false; int dirtyCount = _dirtyElements.length; int index = 0; while (index < dirtyCount) { try { _dirtyElements[index].rebuild();//遍历dirtyElements并执行他们的rebuild方法来使这些Element进行rebuild } catch (e, stack) {} index += 1; if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) { _dirtyElements.sort(Element._sort); _dirtyElementsNeedsResorting = false; dirtyCount = _dirtyElements.length; while (index > 0 && _dirtyElements[index - 1].dirty) { index -= 1; } } } } finally { for (Element element in _dirtyElements) {//最后解除Element的dirty标记,以及清空dirtyElements assert(element._inDirtyList); element._inDirtyList = false; } _dirtyElements.clear(); _scheduledFlushDirtyElements = false; _dirtyElementsNeedsResorting = null; Timeline.finishSync(); } }
很明显就是对dirtyElements中的元素进行遍历并且对他们进行rebuild。
布局
window通过scheduleFrame方法会让SchedulerBinding来执行handleBeginFrame方法(执行transientCallbacks)和handleDrawFrame方法(执行persistentCallbacks,postFrameCallbacks),在RendererBinding初始化时添加了_handlePersistentFrameCallback,它调用了核心的绘制方法drawFrame。
@protected void drawFrame() { assert(renderView != null); pipelineOwner.flushLayout();//布局 pipelineOwner.flushCompositingBits();//刷新dirty的renderobject的数据 pipelineOwner.flushPaint();//绘制 renderView.compositeFrame(); // 将二进制数据发送给GPU pipelineOwner.flushSemantics(); // 将语义发送给系统 }
flushLayout触发布局,将RenderObject树的dirty节点通过调用performLayout方法进行逐一布局,我们先看一下RenderPadding中的实现
@override void performLayout() { _resolve();//解析padding参数 if (child == null) {//如果没有child,直接将constraints与padding综合计算得出自己的size size = constraints.constrain(Size( _resolvedPadding.left + _resolvedPadding.right, _resolvedPadding.top + _resolvedPadding.bottom )); return; } final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);//将padding减去,生成新的约束innerConstraints child.layout(innerConstraints, parentUsesSize: true);//用新的约束去布局child final BoxParentData childParentData = child.parentData; childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);//设置childParentData的offset值(这个值是相对于parent的绘制偏移值,在paint的时候传入这个偏移值) size = constraints.constrain(Size(//将constraints与padding以及child的sieze综合计算得出自己的size _resolvedPadding.left + child.size.width + _resolvedPadding.right, _resolvedPadding.top + child.size.height + _resolvedPadding.bottom )); }
可以看到RenderPadding中的布局分两种情况。如果没有child,那么就直接拿parent传过来的约束以及padding来确定自己的大小;否则就先去布局child,让后再拿parent传过来的约束和padding以及child的size来确定自己的大小。
RenderPadding是典型的单child的RenderBox,我们看一下多个child的RenderBox。例如RenderFlow
@override void performLayout() { size = _getSize(constraints);//直接先确定自己的size int i = 0; _randomAccessChildren.clear(); RenderBox child = firstChild; while (child != null) {//遍历孩子 _randomAccessChildren.add(child); final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);//获取child的约束,此方法为抽象 child.layout(innerConstraints, parentUsesSize: true);//布局孩子 final FlowParentData childParentData = child.parentData; childParentData.offset = Offset.zero; child = childParentData.nextSibling; i += 1; } }
可以看到RenderFlow的size直接就根据约束来确定了,并没去有先布局孩子,所以RenderFlow的size不依赖与孩子,后面依旧是对每一个child依次进行布局。
还有一种比较典型的树尖类型的RenderBox,LeafRenderObjectWidget子类创建的RenderObject对象都是,他们没有孩子,他们才是最终需要渲染的对象,例如
@override void performLayout() { size = _sizeForConstraints(constraints); }
非常简单就通过约束确定自己的大小就结束了。所以performLayout过程就是两点,确定自己的大小以及布局孩子。我们上面提到的都是RenderBox的子类,这些RenderObject约束都是通过BoxConstraints来完成,但是RenderSliver的子类的约束是通过SliverConstraints来完成,虽然他们对child的约束方式不同,但他们在布局过程需要执行的操作都是一致的。
绘制
布局完成了,PipelineOwner就通过flushPaint来进行绘制
void flushPaint() { try { final ListdirtyNodes = _nodesNeedingPaint; _nodesNeedingPaint = []; // 对dirty nodes列表进行排序,最深的在第一位 for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) { assert(node._layer != null); if (node._needsPaint && node.owner == this) { if (node._layer.attached) { PaintingContext.repaintCompositedChild(node); } else { node._skippedPaintingOnLayer(); } } } } finally {} }
PaintingContext.repaintCompositedChild(node)
会调用到child._paintWithContext(childContext, Offset.zero)
方法,进而调用到child的paint方法,我们来看一下第一次绘制的情况,dirty的node就应该是RenderView,跟进RenderView的paint方法
@override void paint(PaintingContext context, Offset offset) { if (child != null) context.paintChild(child, offset);//直接绘制child }
自己没有什么绘制的内容,直接绘制child,再看一下RenderShiftedBox
@override void paint(PaintingContext context, Offset offset) { if (child != null) { final BoxParentData childParentData = child.parentData; context.paintChild(child, childParentData.offset + offset);//直接绘制child } }
好像没有绘制内容就直接递归的进行绘制child,那找一个有绘制内容的吧,我们看看RenderDecoratedBox
@override void paint(PaintingContext context, Offset offset) {//Offset由parent去paintChild的时候传入,该值存放在child的parentdata字段中,该字段是BoxParentData或以下实例 _painter ??= _decoration.createBoxPainter(markNeedsPaint);//获取painter画笔 final ImageConfiguration filledConfiguration = configuration.copyWith(size: size); if (position == DecorationPosition.background) {//画背景 _painter.paint(context.canvas, offset, filledConfiguration);//绘制过程,具体细节再painter中 if (decoration.isComplex) context.setIsComplexHint(); } super.paint(context, offset);//画child,里面直接调用了paintChild if (position == DecorationPosition.foreground) {//画前景 _painter.paint(context.canvas, offset, filledConfiguration); if (decoration.isComplex) context.setIsComplexHint(); } }
如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,看一下RenderImage
@override void paint(PaintingContext context, Offset offset) { if (_image == null) return; _resolve(); paintImage(//直接绘制Image,具体细节再此方法中 canvas: context.canvas, rect: offset & size, image: _image, scale: _scale, colorFilter: _colorFilter, fit: _fit, alignment: _resolvedAlignment, centerSlice: _centerSlice, repeat: _repeat, flipHorizontally: _flipHorizontally, invertColors: invertColors, filterQuality: _filterQuality ); }
所以基本上绘制需要完成的流程就是,如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,流程比较简单。
感谢各位的阅读!关于“Flutter如何构建、布局及绘制三部曲”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!