【Flutter布局控件】Flex详细讲解

对于刚接触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类中的主要函数实现一共六个,分别为:

  1. insertRenderObjectChild(RenderObject child, IndexedSlot<Element> slot)
  2. moveRenderObjectChild(RenderObject child, IndexedSlot<Element> oldSlot, IndexedSlot<Element> newSlot)
  3. removeRenderObjectChild(RenderObject child, dynamic slot)
  4. forgetChild(Element child)
  5. mount(Element parent, dynamic newSlot)
  6. update(MultiChildRenderObjectWidget newWidget)

其含义分别为:

  1. 插入子渲染对象; 调用时机:当子元素安装时。
  2. 移动子渲染对象到新的位置;调用时机:当子元素位置改变时,相当于子组件位置改变。
  3. 移除子渲染对象;调用时机:当子元素被移除时,相当于子组件被移除。
  4. 将子元素置为不可用状态;调用时机:当子元素以前的父元素(即当前父元素)和它解除关系时调用(意味着子节点已经存在并且正在被复用,即将插入到一个新的父元素的节点下)。
  5. 安装当前元素;调用时机:当父元素正在更新子元素时(即当前元素第一次被创建时),也可以说当父元素的“updateChild(Element child, Widget newWidget, dynamic newSlot)“函数被调用时。注意当前元素MultiChildRenderObjectElement的子元素的安装是直接走“inflateWidget(Widget newWidget, dynamic newSlot)“函数的。其实这两个函数都会间接调用到元素的mount函数。
  6. 更新当前元素的配置;调用时机:当前元素的父元素重新构建时,如果当前元素的旧配置和新配置对象不同,而新旧配置的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 = newWidgets[newChildrenTop];
  if (haveOldChildren) {
	final Key key = newWidg
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值