对于刚接触flutter框架的童鞋们,布局控件写的最多的想必是Column和Row两种基本容器组件。这两种组件都有共同的特性,都是容器并且都继承自Flex组件类。唯一的区别就是布局的方向不同而已,Column意为列,表示垂直布局,Row意为行,表示纵向排列。因为Flex是继承自MultiChildRenderObjectWidget,在介绍Flex之前,就先来掌握一下基本组件MultiChildRenderObjectWidget的使用。
1.Flutter的布局控件MultiChildRenderObjectWidget
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
MultiChildRenderObjectWidget({ Key key, this.children = const <Widget>[] })
: super(key: key);
final List<Widget> children;
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}
MultiChildRenderObjectWidget类代码简单明了,其继承自RenderObjectWidget目的是让MultiChildRenderObjectWidget具备创建渲染对象的特性。属性"children"让其具备一个容器组件的特性,管理着一个有顺序的子组件列表。而其对应的元素类MultiChildRenderObjectElement理应同样也需要维护一个和组件列表“children”对应的元素列表,以下来具体分析一下其源码:
class MultiChildRenderObjectElement extends RenderObjectElement {
MultiChildRenderObjectElement(MultiChildRenderObjectWidget widget)
: super(widget);
@override
MultiChildRenderObjectWidget get widget => super.widget as MultiChildRenderObjectWidget;
@protected
@visibleForTesting
Iterable<Element> get children => _children.where((Element child) => !_forgottenChildren.contains(child));
//维护的子元素列表
List<Element> _children;
final Set<Element> _forgottenChildren = HashSet<Element>();
@override
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element> slot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
renderObject.insert(child, after: slot?.value?.renderObject);
}
@override
void moveRenderObjectChild(RenderObject child, IndexedSlot<Element> oldSlot, IndexedSlot<Element> newSlot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
renderObject.move(child, after: newSlot?.value?.renderObject);
}
@override
void removeRenderObjectChild(RenderObject child, dynamic slot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
renderObject.remove(child);
}
@override
void visitChildren(ElementVisitor visitor) {
for (final Element child in _children) {
if (!_forgottenChildren.contains(child))
visitor(child);
}
}
@override
void forgetChild(Element child) {
_forgottenChildren.add(child);
super.forgetChild(child);
}
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_children = List<Element>(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element>(i, previousChild));
_children[i] = newChild;
previousChild = newChild;
}
}
@override
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
_children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
_forgottenChildren.clear();
}
}
从源码分析来看,MultiChildRenderObjectElement中维护了一个子元素列表“_children”,注意该属性中不仅包括可使用的子元素也包括已遗失的子元素,目的在于在“updateChildren“函数中做子元素的复用,后面再做分析。代码:
Iterable<Element> get children => _children.where((Element child) => !_forgottenChildren.contains(child));
仅仅获取可使用的元素列表,这里是需要特别注意的。MultiChildRenderObjectElement类中的主要函数实现一共六个,分别为:
- insertRenderObjectChild(RenderObject child, IndexedSlot<Element> slot)
- moveRenderObjectChild(RenderObject child, IndexedSlot<Element> oldSlot, IndexedSlot<Element> newSlot)
- removeRenderObjectChild(RenderObject child, dynamic slot)
- forgetChild(Element child)
- mount(Element parent, dynamic newSlot)
- update(MultiChildRenderObjectWidget newWidget)
其含义分别为:
- 插入子渲染对象; 调用时机:当子元素安装时。
- 移动子渲染对象到新的位置;调用时机:当子元素位置改变时,相当于子组件位置改变。
- 移除子渲染对象;调用时机:当子元素被移除时,相当于子组件被移除。
- 将子元素置为不可用状态;调用时机:当子元素以前的父元素(即当前父元素)和它解除关系时调用(意味着子节点已经存在并且正在被复用,即将插入到一个新的父元素的节点下)。
- 安装当前元素;调用时机:当父元素正在更新子元素时(即当前元素第一次被创建时),也可以说当父元素的“updateChild(Element child, Widget newWidget, dynamic newSlot)“函数被调用时。注意当前元素MultiChildRenderObjectElement的子元素的安装是直接走“inflateWidget(Widget newWidget, dynamic newSlot)“函数的。其实这两个函数都会间接调用到元素的mount函数。
- 更新当前元素的配置;调用时机:当前元素的父元素重新构建时,如果当前元素的旧配置和新配置对象不同,而新旧配置的widget的类型和key保持一致,就会调用到该函数。注意当前元素的子元素的更新是在“updateChildren“函数中实现的,可能会间接调用到子元素的“updateChild(Element child, Widget newWidget, dynamic newSlot)”函数。
其实MultiChildRenderObjectElement类和SingleChildRenderObjectElement类对比,需要实现的函数是相同的。那么如果我们需要自定义一个符合需求的容器组件,其元素类也可以类比MultiChildRenderObjectElement类实现相关函数即可,但是一般继承MultiChildRenderObjectElement类即可满足需求。MultiChildRenderObjectElement类的代码十分简单,这里就不详细讲解,我们主要看“updateChildren”函数的实现。该函数的实现存在于RenderObjectElement类中,由于该函数比较复杂,在下一节中来单独介绍。
2.Flutter的布局控件之RenderObjectElement.updateChildren函数分析
这一节单独讲解“updateChildren”函数,该函数逻辑十分的庞大,但是可以自上而下慢慢将其拆分理解,首先看以下代码:
@override
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
_children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
_forgottenChildren.clear();
}
我们从上一节得知,该函数是在容器组件被更新时调用,也就是在update(MultiChildRenderObjectWidget newWidget)函数被调用时。update函数中,第一句代码为:super.update(newWidget);会间接调用到类Element的update函数,该函数会中语句“_widget=newWidget”会将当前配置更新为新配置,而语句“ _children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren)”,即调用到“updateChildren”函数,第一个入参为旧元素集,第二个入参为子组件集的新配置。注意看第三个入参,这里上一节提到的“_forgottenChildren”属性就派上用场了,用完之后便将_forgottenChildren清除。
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
Element replaceWithNullIfForgotten(Element child) {
return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;
}
当调用到"updateChildren"函数时,从“replaceWithNullIfForgotten“函数的实现可以知道,该函数判断入参元素“child”是否可以复用,如果可以被重复利用,就返回它自身,否则就会返回null。猜测:当返回null时,肯定会通过“newWidget”去创建一个新的元素对象。
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>(newWidgets.length);
Element previousChild;
// Update the top of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
根据这段代码,需要理解相关变量的含义:
1. newChildrenTop 为新配置列表的头偏移下标,从0开始。
2. oldChildrenTop 为旧配置列表的头偏移下标,从0开始。
3. newChildrenBottom 为新配置列表的尾下标,从length - 1开始,即从新列表最后一个配置开始。
4. oldChildrenBottom 为旧配置列表的尾下标,从length - 1开始,即从旧列表最后一个配置开始。
代码:
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>(newWidgets.length);
表示更新配置后的元素集,该集合会作为最后的返回值。将oldChildren赋值给newChildren只是纯粹为了复用对象而已,集合中的值还是会在后面进行更新。
下面的一段循环上面有一段注释说明,意为从头往尾部开始扫描更新列表的头部符合配置的旧元素,这一段的循环是从oldChildrenTop位置开始进行扫描,什么时候会存在符合配置的旧元素呢,代码:
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
表示当oldChild不为空同时新旧配置类型和key保持一致时,代表旧元素可以使用新配置进行更新,这里即在复用旧元素对象。
但当oldChild为空,旧元素是通过“replaceWithNullIfForgotten”函数获取的,即当该旧元素没有被任何其他父元素使用时,则该元素对象是可以被复用的,还需要保证旧元素的配置和新配置保持一致。否则,就需要通过新配置重新生成一个元素对象,只要提前找到一个不满足的元素,循环就会马上跳出。
当满足条件时,语句:
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
就会被执行,注意这里的oldChild一定不为空,并且oldChild.widget和newWidget一定是保持一致的。再来看一下"updateChild"函数中的关键代码:
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (Widget.canUpdate(child.widget, newWidget)) {
//此时一定会进去到该if逻辑中调用child.update(newWidget)函数
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
上图代码中一定会进入到注释的if判断中调用子元素的更新函数"update(newWidget)"。
注意子元素插槽对象IndexedSlot<Element>(newChildrenTop, previousChild)里面保存的是当前子元素的位置和其前一个子元素。具体这个插槽值有什么作用,大家可以转到“MultiChildRenderObjectElement”源码再看看,当增删子渲染对象时,会将子渲染对象插入到子渲染对象列表的对应位置,其实内部是一个链表结构而已,有兴趣的同学可以去看看我的《小白的flutter之路(二)》文章对应的介绍。
当从头往尾部扫描完毕后,元素集合中开头符合配置的旧元素即被更新了,即旧元素的“update”函数被调用了。此时newChildrenTop和oldChildrenTop偏移下标会指向新的位置,该位置应该是不符合新配置的旧元素。再看从尾部往头部扫描更新旧元素的代码片段:
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}
这段代码和上面代码看似相似,实则不然,其没有更新旧元素的任何代码,只是简单地做了尾部变量的偏移而已。这里不作更新主要是为了保证子元素更新的顺序:更新头元素->更新中间元素->更新尾元素。
当循环结束时尾部偏移值oldChildrenBottom和newChildrenBottom会指向不符合新配置的旧元素。
接着再看后面的代码:
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element> oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = <Key, Element>{};
while (oldChildrenTop <= oldChildrenBottom) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null) {
if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key] = oldChild;
else
deactivateChild(oldChild);
}
oldChildrenTop += 1;
}
}
这段代码意在找到头部oldChildTop到尾部oldChildrenBottom之间不符合新配置的旧元素,此时需要做的工作就是将中间不符合新配置的旧元素进行销毁。语句:
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
表示中间是否存在不符合新配置的旧元素,注意如果oldChildrenTop==oldChildrenBottom,同样存在一个元素。除非oldChildrenTop > oldChildrenBottom。
如果存在不符合新配置的旧元素,就会进入循环找到中间的所有元素。“replaceWithNullIfForgotten”函数返回的旧元素如果为空,代表该元素正在被其他父元素使用,该元素不能被销毁。如果非空,再进一步判断旧元素的旧配置是否配置过key,如果配置过,就将旧元素保存在oldKeyedChildren的map中,这里的目的就是为了通过key来复用旧元素对象(因为这些旧元素对象未被被其他任何父元素使用,还有利用的价值,不用着急销毁),否则就调用deactivateChild(child)函数直接销毁掉该元素(其实并不是真正的销毁,而是元素会进入非活动元素列表等待下次被复用)。循环结束后头部偏移变量oldChildrenTop也就大于oldChildrenBottom值了,其实此时的oldChildrenTop为oldChildrenBottom + 1,即该下标为符合新配置的尾部列表开始位置。
// Update the middle of the list.
while (newChildrenTop <= newChildrenBottom) {
Element oldChild;
final Widget newWidget