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

本文深入探讨了Flutter中的Flex布局控件,包括其作为容器组件的基础特性,如Column和Row的使用,以及Flex组件的原理。通过分析MultiChildRenderObjectWidget和RenderObjectElement.updateChildren函数,解释了Flex布局的更新过程。此外,还详细讲解了Flex的属性,如direction、mainAxisAlignment和mainAxisSize等,以及它们如何影响子组件的布局。最后,介绍了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 = newWidget.key;
	if (key != null) {
	  oldChild = oldKeyedChildren[key];
	  if (oldChild != null) {
		if (Widget.canUpdate(oldChild.widget, newWidget)) {
		  // we found a match!
		  // remove it from oldKeyedChildren so we don't unsync it later
		  oldKeyedChildren.remove(key);
		} else {
		  // Not a match, let's pretend we didn't see it for now.
		  oldChild = null;
		}
	  }
	}
  }
  final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
  newChildren[newChildrenTop] = newChild;
  previousChild = newChild;
  newChildrenTop += 1;
}

通过注释知道这段逻辑在更新列表的中间部分,如果haveOldChildren为true,代表存在不符合规则的旧元素,为了尽量复用这些未使用的旧元素对象,如果新元素配置过key,就通过新元素的key来在可利用的旧元素列表中查找,如果找到可再次利用的旧元素对象,再检查旧元素的配置和新配置是否一致。如果一致就从可利用的旧元素列表中移除掉,因为该旧元素对象即将被用在新的元素列表中;如果不一致,就将oldChild引用置空,下面再调用"updateChild"函数时,不用在函数中再做多余的判断了。下面的代码十分简单,无非就是通过新配置更新出一个元素对象,加入到新的元素列表中。更新时,要么oldChild为空,会重新创建一个新的元素对象(但也不一定,如果非活动元素列表中能通过newWidget配置的key找到一个非活动元素,并且key中的配置和当前新配置一致,也可复用非活动元素列表中的元素);要么oldChild不为空,也就是会重新调用旧元素的update函数来更新新配置数据。

当循环跳出后,newChildrenTop头偏移位置也就大于newChildrenBottom尾部偏移位置了,其实此时的newChildrenTop为newChildrenBottom+ 1,即该下标为符合新配置的尾部列表开始位置。接下来最后来分析下余下的代码逻辑:

newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;

// Update the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
  final Element oldChild = oldChildren[oldChildrenTop];
  final Widget newWidget = newWidgets[newChildrenTop];
  final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
  newChildren[newChildrenTop] = newChild;
  previousChild = newChild;
  newChildrenTop += 1;
  oldChildrenTop += 1;
}

// Clean up any of the remaining middle nodes from the old list.
if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
  for (final Element oldChild in oldKeyedChildren.values) {
	if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
	  deactivateChild(oldChild);
  }
}

return newChildren;

代码开头重新将oldChildrenBottom和newChildrenBottom偏移位置赋值到相应的列表末尾位置。此时oldChildrenBottom - oldChildrenTop一定是等于newChildrenBottom - newChildrenTop的。下面的循环为更新尾部列表,而所有的尾部旧元素列表全部是符合新配置的,在循环中直接使用“updateChild”函数更新即可,内部会间接调用到旧元素oldChild的"update"函数。

循环跳出后,此时变量“newChildren”中的所有元素都是具备新配置的子元素列表了。

接下来还有一步,就是销毁可利用元素列表中未被利用的的旧元素(利用过的元素都会从oldKeyedChildren中移除掉,可看前面的代码)。最后的代码:

if (forgottenChildren == null || !forgottenChildren.contains(oldChild))

这个判断是必要的,如果该子元素正在被其他父元素使用的话,就不能销毁它了。

 

3.Flutter的布局控件Flex

好了,前面做了那么多铺垫,到此为止就即将引出Flex容器控件了。从Flex本身含义分析意为弹性布局,猜测应该是具备弹性值的。我们知道Flex是一个容器组件,既然是容器组件,那么大概率上是继承了MultiChildRenderObjectWidget类,通过前面几小节的分析得知,继承MultiChildRenderObjectWidget类的好处是什么?就是子元素列表和子渲染对象已经被很好地管理了,包括他们状态的安装和更新,子对象的插入和删除等等操作。

class Flex extends MultiChildRenderObjectWidget {
  
  Flex({
    Key key,
    @required this.direction,
    this.mainAxisAlignment = MainAxisAlignment.start,
    this.mainAxisSize = MainAxisSize.max,
    this.crossAxisAlignment = CrossAxisAlignment.center,
    this.textDirection,
    this.verticalDirection = VerticalDirection.down,
    this.textBaseline = TextBaseline.alphabetic,
    this.clipBehavior = Clip.hardEdge,
    List<Widget> children = const <Widget>[],
  }) : super(key: key, children: children);

  final Axis direction;

  final MainAxisAlignment mainAxisAlignment;

  final MainAxisSize mainAxisSize;

  final CrossAxisAlignment crossAxisAlignment;

  final TextDirection textDirection;

  final VerticalDirection verticalDirection;

  final TextBaseline textBaseline;

  final Clip clipBehavior;

  bool get _needTextDirection {
    switch (direction) {
      case Axis.horizontal:
        return true; // because it affects the layout order.
      case Axis.vertical:
        assert(crossAxisAlignment != null);
        return crossAxisAlignment == CrossAxisAlignment.start
            || crossAxisAlignment == CrossAxisAlignment.end;
    }
    return null;
  }

  @protected
  TextDirection getEffectiveTextDirection(BuildContext context) {
    return textDirection ?? (_needTextDirection ? Directionality.of(context) : null);
  }

  @override
  RenderFlex createRenderObject(BuildContext context) {
    return RenderFlex(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
      textDirection: getEffectiveTextDirection(context),
      verticalDirection: verticalDirection,
      textBaseline: textBaseline,
      clipBehavior: clipBehavior,
    );
  }

  @override
  void updateRenderObject(BuildContext context, covariant RenderFlex renderObject) {
    renderObject
      ..direction = direction
      ..mainAxisAlignment = mainAxisAlignment
      ..mainAxisSize = mainAxisSize
      ..crossAxisAlignment = crossAxisAlignment
      ..textDirection = getEffectiveTextDirection(context)
      ..verticalDirection = verticalDirection
      ..textBaseline = textBaseline
      ..clipBehavior = clipBehavior;
  }
}

这是一个对外开放的接口配置组件类,其中的八大属性值可以自由配置,其含义如下:

1. direction

表示子组件在主轴上的布局方向,取值有两种,分别为:水平方向布局:Axis.horizontal;垂直方向布局:Axis.vertical。

2. mainAxisAlignment

表示子组件在主轴方向上的对齐方式,一共六种方式,分别为:

  • MainAxisAlignment.start - 子组件尽量靠近主轴开始的方向,存在两种情况:1.当主轴direction为水平方向时,如果textDirection为从左到右(TextDirection.ltr),则开始位置为左端;如果textDirection为从右到左(TextDirection.rtl),则开始位置为右端(此时第一个孩子从右边开始);2.当主轴direction为垂直方向时,如果verticalDirection为从上到下(VerticalDirection.down),则开始位置为顶端;如果verticalDirection为从下到上(VerticalDirection.up),则开始位置为底端(此时第一个孩子从下边开始)。
  • MainAxisAlignment.end - 子组件尽量靠近主轴结束的方向,同样存在两种情况:1.当主轴direction为水平方向时,如果textDirection为从左到右(TextDirection.ltr),则结束位置为右端;否则结束位置为左端(此时第一个孩子从右边开始);2.当主轴direction为垂直方向时,如果verticalDirection为从上到下(VerticalDirection.down),则结束位置为底端;如果verticalDirection为从下到上(VerticalDirection.up),则结束位置为顶端(此时第一个孩子从下边开始)。
  • MainAxisAlignment.center - 子组件整体居中对齐,两端剩余空间相等。
  • MainAxisAlignment.spaceBetween - 子组件尽量靠近主轴开始和结束两端,并且子组件相互之间的间距相等。
  • MainAxisAlignment.spaceAround - 子组件间平均分配所有剩余空间,首尾距离边界是间距的一半。
  • MainAxisAlignment.spaceEvenly - 子组件间平均分配所有剩余空间,首尾距离边界和间距相等。

3. mainAxisSize

表示将主轴应该占据多少空间,其值有两种,分别为:最小化剩余空间:MainAxisSize.min;最大化剩余空间:MainAxisSize.max。

4. crossAxisAlignment

表示子组件在纵轴方向上的对齐方式,一共五种方式,分别为:

  • start - 紧靠纵轴的开始位置,存在两种情况:1.当主轴direction为水平方向时,纵轴即为垂直方向。如果verticalDirection为从上到下(VerticalDirection.down),则开始位置为顶端,否则开始位置为底端。2.当主轴direction为垂直方向时,纵轴即为水平方向,如果textDirection为从左到右(TextDirection.ltr),则开始位置为左端,否则开始位置为右端。
  • end - 紧靠纵轴的结束位置,存在两种情况:1.当主轴direction为水平方向时,纵轴即为垂直方向。如果verticalDirection为从上到下(VerticalDirection.down),则结束位置为底端,否则结束位置为顶端。2.当主轴direction为垂直方向时,纵轴即为水平方向,如果textDirection为从左到右(TextDirection.ltr),则结束位置为右端,否则结束位置为左端。
  • center - 在纵轴上居中对齐。
  • stretch - 紧靠纵轴开始位置,并铺开纵轴所有剩余空间。
  • baseline - 按照每个子组件自身的基线对齐。因为基线总是水平的,这里分为两种情况:1.当主轴为水平方向时,此时在纵轴垂直方向上的对齐方式优先以基线为准,同时尽量靠近容器顶端;2.当主轴为垂直方向时,此时baseline不产生作用,相当于CrossAxisAlignment.start值,但是此时是无意义的,一般会导致程序崩溃。

5. textDirection

表示在水平方向上的排列方式,分为两种:1.从左到右,值为TextDirection.ltr。如果主轴为水平方向,表示第一个孩子从左边开始;2.从右到左,值为TextDirection.rtl。如果主轴为水平方向,表示第一个孩子从右边开始。

6. verticalDirection

表示在垂直方向上的排列方式,分为两种:1.从上到下,值为VerticalDirection.down,如果主轴为垂直方向,表示第一个孩子从上边开始;2.从下到上,值为VerticalDirection.up,如果主轴为垂直方向,表示第一个孩子从下边开始。

7. textBaseline

组件的基线分为两种,分别为:

  • TextBaseline.alphabetic - 文本基线是标准的字母基线,用于对齐字符底部的水平线,此为默认值
  • TextBaseline.ideographic -用于对齐表意字符的水平线。

8. clipBehavior

当孩子们在主轴上的总空间在父FlexBox空间内有溢出部分,绘制孩子时的裁剪行为,分别为:

  • Clip.none - 不裁剪孩子们在父亲中的溢出部分,溢出部分会在父亲的范围外显示出来。
  • Clip.hardEdge - FlexBox的默认裁剪方式,速度最快,但保真度较低。
  • Clip.antiAlias - 坑锯齿裁剪(边缘更加平滑),裁剪后孩子们不会在新的画布上绘制,比Clip.hardEdge稍慢
  • Clip.antiAliasWithSaveLayer - 坑锯齿裁剪(边缘更加平滑),裁剪后孩子们会在新的画布上绘制,速度最慢,很少被用到。

至此,从以上的属性说明中大致了解了这些属性的含义和作用。再看其创建出的渲染对象类型为RenderFlex,RenderFlex应该实现了上面这些属性的具体逻辑,从上面属性说明中可以得知大部分跟布局相关,所以RenderFlex的重点逻辑应该在布局这一块。

class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlexParentData>,
                                        RenderBoxContainerDefaultsMixin<RenderBox, FlexParentData>,
                                        DebugOverflowIndicatorMixin

上面代码可以看出RenderFlex继承了RenderBox类,并且拥有ContainerRenderObjectMixin特性。渲染对象RenderFlex务必得拥有ContainerRenderObjectMixin特性,因为MultiChildRenderObjectElement类在增删子渲染对象的操作中,第一句便是将渲染对象类型强转为ContainerRenderObjectMixin类型。如下是插入子渲染对象的函数:

@override
  void insertRenderObjectChild(RenderObject child, IndexedSlot<Element> slot) {
    final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject =
    this.renderObject as ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>;
      ......
  }

ContainerRenderObjectMixin类的作用就是管理子节点链表,因为增删操作比较多,所以链表比集合更合适,在我的《小白的flutter之路(二)》这篇文章有更加详细的介绍,RenderBoxContainerDefaultsMixin类提供了默认功能实现,其余就不加讨论。接下来重点讨论下RenderFlex的父类RenderBox类的作用:

abstract class RenderBox extends RenderObject{......}

可以看到RenderBox继承自RenderObject,目的是让其具备布局和绘制的能力。RenderBox语义为渲染盒子(可以称其为方形框布局,即一个正方形或长方形),既然是盒子,那么盒子就应该有尺寸大小,即有宽高。在flutter框架中将其描述为:一个在二维笛卡尔坐标系中的渲染对象,它的尺寸使用宽高来表示,每个RenderBox对象都有它自己的坐标系,以其左上顶点为原点(0,0),由宽高来确定右下角的坐标位置,即右下坐标点为(left + width,top + height)。对于这样的一个RenderBox渲染盒子,其大小的范围被其父亲所约束,每个渲染盒子都有其最小被允许的宽minWidth,最大被允许的宽maxWidth;以及最小被允许的高minHeight,最大被允许的高maxHeight,这个由父亲传过来的范围约束被保存在BoxConstraints对象中。可以看到RenderBox盒子的大小有可能依赖于其孩子的大小,当依赖于子尺寸时,属性parentUsesSize值必须置为true。如果你要自定义一个新的渲染盒子的位置布局方式,也可以称作新的布局协议,比如不以坐标系来定义,或者以坐标系但不是以二维坐标系,那么你完全可以自己实现一个如同RenderBox的类,但必须继承自RenderObject类,提供相关函数的实现即可。对于RenderObject对象的布局和绘制,必须在适当的时机调用适当的函数。如果你的布局方式发生了改变(比如当你重新设置了某个影响布局的属性值),请在设置相关值后调用markNeedsLayout函数,来重新布局,当然布局后是需要重新绘制的,此时你不用再调用markNeedsPaint函数,因为markNeedsLayout函数内部会调用到markNeedsPaint函数;如果你的布局未发生任何改变(比如你只是重新设置了影响绘制的参数),那么此时你只需要调用markNeedsPaint函数重新绘制即可,因为布局的代价十分昂贵,你不必再调用markNeedsLayout做出额外的布局操作。一个具有子渲染对象的类型有以下几种场景:

  1. 只有一个孩子,其继承自RenderBox,孩子需要在坐标系中布局,有两种类型:1.RenderProxyBox,孩子的大小与其大小一致;2.RenderShiftedBox,孩子的大小小于它的大小,并且需要在其内部对齐孩子。
  2. 只有一个孩子,不继承自RenderBox,但是使用到了RenderObjectWithChildMixin的mixin类。
  3. 有多个孩子,使用到了ContainerRenderObjectMixin的mixin类。
  4. 具有一个更为复杂的子模型。

废话不多说了,下面继续RenderBox类的深入探究,一步一步拆解代码来分析:

@override
void setupParentData(covariant RenderObject child) {
  if (child.parentData is! BoxParentData)
    child.parentData = BoxParentData();
}

该函数由其函数名称可知,为安装父数据,其实是安装子渲染对象的父数据,就是来确定子渲染对象父数据的类型。从中可知对于RenderBox渲染对象,其子渲染对象的父数据,由它来确定。既然是由父RenderBox渲染对象确定,那么这个子渲染对象的父数据parentData肯定是跟其他子渲染对象或者其父RenderBox渲染对象相关。而RenderBox渲染对象为其孩子默认创建的父数据类型为BoxParentData,分析其源码,知道BoxParentData类中只保存了一个偏移值offset,那么现在我们就能确定了默认RenderBox渲染对象为其子渲染对象安装的父数据是用来保存该子渲染对象在RenderBox中的位置(也称在父方形框中的偏移位置)的。函数"setupParentData(child)"具体是在子元素被安装的时候调用的,大家可以阅读这个调用流程即可明白(当子元素的安装函数“mount(parent, newSlot)“被调用时):

RenderObjectElement.mount(parent, newSlot)->attachRenderObject(newSlot)->_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot)->调用父渲染对象实现的insertRenderObjectChild函数->父渲染对象调用renderObject.child = child或者adoptChild(child)->父渲染对象调用setupParentData(child)

继续往下看:

Map<_IntrinsicDimensionsCacheEntry, double> _cachedIntrinsicDimensions;

double _computeIntrinsicDimension(_IntrinsicDimension dimension, double argument, double computer(double argument)) {
   
    _cachedIntrinsicDimensions ??= <_IntrinsicDimensionsCacheEntry, double>{};
    return _cachedIntrinsicDimensions.putIfAbsent(
	  _IntrinsicDimensionsCacheEntry(dimension, argument),
	  () => computer(argument),
    );
}

注意看“_computeIntrinsicDimension”函数,该函数有什么作用呢?从函数名称可知其主要作用是计算RenderBox方形框的尺寸,尺寸包括宽和高,那么该函数应该是计算宽和高的值。但是为了提高计算效率,RenderBox类又提供了一个Map类型的变量“_cachedIntrinsicDimensions”来保存已计算过的尺寸结果值,每个结果值由_IntrinsicDimensionsCacheEntry对象一一对应,该对象类使用了dimension和argument两个变量实现了哈希运算。也就是说在缓存map内,对于由函数“_computeIntrinsicDimension”的入参变量“dimension”值和“argument"值确定一个唯一的结果值。对于第三个参数"computer"是一个计算函数,计算的结果值需要参考第二个入参“argument”。我们从讲解RenderBox的时候谈到过,RenderBox方形框由其父亲约束其大小,而约束值一共有四种:最小宽、最大宽、最小高、最大高,但是我自身固有的这四种值还需要通过子元素进行确定。所以“_computeIntrinsicDimension”函数应该是为这四种固有值的计算所服务的。

@mustCallSuper
double getMinIntrinsicWidth(double height) {
  return _computeIntrinsicDimension(_IntrinsicDimension.minWidth, height, computeMinIntrinsicWidth);
}

@protected
double computeMinIntrinsicWidth(double height) {
  return 0.0;
}

@mustCallSuper
double getMaxIntrinsicWidth(double height) {
  return _computeIntrinsicDimension(_IntrinsicDimension.maxWidth, height, computeMaxIntrinsicWidth);
}

@protected
double computeMaxIntrinsicWidth(double height) {
  return 0.0;
}

@mustCallSuper
double getMinIntrinsicHeight(double width) {
  return _computeIntrinsicDimension(_IntrinsicDimension.minHeight, width, computeMinIntrinsicHeight);
}

@protected
double computeMinIntrinsicHeight(double width) {
  return 0.0;
}

@mustCallSuper
double getMaxIntrinsicHeight(double width) {
  return _computeIntrinsicDimension(_IntrinsicDimension.maxHeight, width, computeMaxIntrinsicHeight);
}

@protected
double computeMaxIntrinsicHeight(double width) {
  return 0.0;
}

以上代码便借助了“_computeIntrinsicDimension”函数实现了这四种功能,但其主要的计算逻辑并未实现,可以看到以"compute..."开头的函数都返回了0,而以"get..."开头的函数内部都使用到了与"compute.."对应的计算函数。get开头的函数会尝试从缓存中去读结果值,如果未读取到便调用compute开头的函数计算出结果值并进行值的缓存。而compute开头的函数只会运算出结果值,由此可知compute开头的函数需要每个RenderBox的子类进行具体实现的,你可以转到FlexBox类查看所有的compute函数都被实现了。我们还会发现另外一个特点,计算固有最小或最大宽度值的函数都需要参考高度值height,而计算固有最小或最大高度值的函数都需要参考宽度值width。其实固有的最小宽度和高度其含义为能容纳孩子布局范围的最小限制,绘制其内容时,不需要任何的裁剪即能展现全部内容所需要的最小尺寸;而最大固有宽度和高度含义为...。那参考值的作用是什么呢?参考值主要是用来判断计算值的范围环境,注意参考值一定不为null或者负数,它有两种计算场景:1.当参考值是一个具体值时,计算值该如何表现。2.当参考值为无限范围时,计算值的范围不受约束。

继续往下看:

bool get hasSize => _size != null;

Size get size {
  return _size;
}
 
@protected
set size(Size value) {
  _size = value;
}

这段代码展示了RenderBox方向框尺寸大小的表示形式,size中保存了它的宽和高,当RenderBox在布局时size应该被确定下来。


Map<TextBaseline, double> _cachedBaselines;

double getDistanceToBaseline(TextBaseline baseline, { bool onlyReal = false }) {
  final double result = getDistanceToActualBaseline(baseline);
  if (result == null && !onlyReal)
    return size.height;
  return result;
}

@protected
@mustCallSuper
double getDistanceToActualBaseline(TextBaseline baseline) {
  conventions of this method.');
  _cachedBaselines ??= <TextBaseline, double>{};
  _cachedBaselines.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline));
  return _cachedBaselines[baseline];
}

@protected
double computeDistanceToActualBaseline(TextBaseline baseline) {
  return null;
}

这段代码展示了基线值的计算流程,和计算固有最小或最大宽高一样,也对值做了缓存处理,具体保存在“_cachedBaselines”的map对象中。一般通过"getDistanceToBaseline“函数获取基线值,如果未获取到就直接去RenderBox方形框的高作为基线值,也就是基线在方形框的最底部了。"getDistanceToActualBaseline"函数主要是对计算过的值做了一层缓存处理。而实际真正计算基线值的逻辑在于“computeDistanceToActualBaseline”函数,可以看到该函数直接返回空,也就说明默认的基线在最底部。至于入参TextBaseline是一个枚举类,其值有两种:TextBaseline.alphabetic和TextBaseline.ideographic,代表RenderBox具备两种基线值,这里就不做具体说明,一切以实现逻辑为准。

@override
void markNeedsLayout() {
  if ((_cachedBaselines != null && _cachedBaselines.isNotEmpty) ||
	(_cachedIntrinsicDimensions != null && _cachedIntrinsicDimensions.isNotEmpty)) {
    _cachedBaselines?.clear();
    _cachedIntrinsicDimensions?.clear();
    if (parent is RenderObject) {
	  markParentNeedsLayout();
	  return;
    }
  }
  super.markNeedsLayout();
}

该函数由其名称便可知,在RenderBox需要重新布局时调用markNeedsLayout()函数,if里面的判断逻辑是清理计算值的缓存,因为每次布局时,计算值都需要被重新计算,一旦进入了该判断说明肯定有其他对象(有可能是父对象)使用了这些旧值,这里也需要重新调用父布局函数。

@override
void performResize() {
  size = constraints.smallest;
}

@override
void performLayout() {
}

最后再介绍一下这两个函数,performResize()函数为重新计算RenderBox的尺寸,默认实现为constrants.smallest,意为使用被父亲允许布局范围的最小值,当然不能小于0,该函数会在RenderBox的尺寸被父亲使用到时调用,接着会调用到performLayout()函数。函数performLayout()是RenderBox的真正布局关键所在,交由子类实现具体的布局逻辑。

至此,RenderBox类的代码算是大致讲解完了,主要目的还是为了能更加清晰地去认识FlexBox类的实现,接下来让我们正式阅读FlexBox类的代码功能实现部分,这里不会把所有代码拿出来讲解,只会讲重要函数。

@override
double computeMinIntrinsicWidth(double height) {
  return _getIntrinsicSize(
    sizingDirection: Axis.horizontal,
    extent: height,
    childSize: (RenderBox child, double extent) => child.getMinIntrinsicWidth(extent),
  );
}

@override
double computeMaxIntrinsicWidth(double height) {
  return _getIntrinsicSize(
    sizingDirection: Axis.horizontal,
    extent: height,
    childSize: (RenderBox child, double extent) => child.getMaxIntrinsicWidth(extent),
  );
}

@override
double computeMinIntrinsicHeight(double width) {
  return _getIntrinsicSize(
    sizingDirection: Axis.vertical,
    extent: width,
    childSize: (RenderBox child, double extent) => child.getMinIntrinsicHeight(extent),
  );
}

@override
double computeMaxIntrinsicHeight(double width) {
  return _getIntrinsicSize(
    sizingDirection: Axis.vertical,
    extent: width,
    childSize: (RenderBox child, double extent) => child.getMaxIntrinsicHeight(extent),
  );
}

@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
  if (_direction == Axis.horizontal)
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  return defaultComputeDistanceToFirstActualBaseline(baseline);
}

这些函数是FlexBox实现的其父类RenderBox中的以"compute.."开头的计算函数。分为两部分:1.计算固有尺寸。2.计算基线距离。我们先看基线是如何计算的,当FlexBox的方向为水平时,调用的是“defaultComputeDistanceToHighestActualBaseline”函数,而垂直方向调用的则是“defaultComputeDistanceToFirstActualBaseline”,其实这两个函数都是RenderBoxContainerDefaultsMixin类中提供的默认实现,以下是RenderBoxContainerDefaultsMixin类中为计算默认基线值的具体实现代码:

double defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline) {
  double result;
  ChildType child = firstChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    double candidate = child.getDistanceToActualBaseline(baseline);
    if (candidate != null) {
	  candidate += childParentData.offset.dy;
	  if (result != null)
	    result = math.min(result, candidate);
	  else
	    result = candidate;
    }
    child = childParentData.nextSibling;
  }
  return result;
}

当FlexBox为水平方向时,会调用到该函数,水平方向上的基线肯定计算的是y轴的距离。函数中循环了所有孩子,并计算每孩子的基线值。每个孩子的基线值都是以孩子的左上顶点(0,0)为原点的,所以要将实际基线值转化到父坐标系中,必须得加上每个孩子在父方形框中的在y轴上的偏移距离。可以看到该循环中,实际上取的基线距离为最小基线值的那个孩子。所以FlexBox的基线为孩子中谁的基线最小,就以谁为准,而这个最小值就是在y轴上孩子的水平基线距离父原点(0,0)的长度。

double defaultComputeDistanceToFirstActualBaseline(TextBaseline baseline) {
    ChildType child = firstChild;
    while (child != null) {
      final ParentDataType childParentData = child.parentData as ParentDataType;
      final double result = child.getDistanceToActualBaseline(baseline);
      if (result != null)
        return result + childParentData.offset.dy;
      child = childParentData.nextSibling;
    }
    return null;
}

当FlexBox为垂直方向时,获取基线便会调用到该函数,注意基线只存在在y轴方向上,所以依然需要叠加相对于父亲在y轴上的偏移值,这里获取的是第一个具有基线值的孩子。所以FlexBox为垂直方向时,基线值以第一个具有基线值的孩子为准,同样这个基线值是在y轴上孩子的水平基线距离父原点(0,0)的长度。。

好的,接下来继续观察FlexBox类中计算固有尺寸的函数,所有的函数都调用了“_getIntrinsicSize”函数,第一个参数为计算的尺寸值的方向,第二个参数为非尺寸方向上的一个参考值,第三个参数为一个计算孩子尺寸的函数。计算固有宽时,尺寸方向为水平方向,参考值为一个高度值;计算固有高时,尺寸方向为垂直方向,参考值为一个宽度值。我们具体看“_getIntrinsicSize”函数的逻辑,我们拆分成两部分来理解:

double _getIntrinsicSize({
    @required Axis sizingDirection,
    @required double extent,
    @required _ChildSizingFunction childSize,
  }) {
    if (_direction == sizingDirection) {
      double totalFlex = 0.0;
      double inflexibleSpace = 0.0;
      double maxFlexFractionSoFar = 0.0;
      RenderBox child = firstChild;
      while (child != null) {
        final int flex = _getFlex(child);
        totalFlex += flex;
        if (flex > 0) {
          final double flexFraction = childSize(child, extent) / _getFlex(child);
          maxFlexFractionSoFar = math.max(maxFlexFractionSoFar, flexFraction);
        } else {
          inflexibleSpace += childSize(child, extent);
        }
        final FlexParentData childParentData = child.parentData as FlexParentData;
        child = childParentData.nextSibling;
      }
      return maxFlexFractionSoFar * totalFlex + inflexibleSpace;
    }
    ......

第一部分,当计算的尺寸方向和FlexBox方形框的布局方向一致时,一共需要确定三个值来计算尺寸值,我们先看开头这三个变量:totalFlex为总弹性值、inflexibleSpace为不可弹性的总空间大小、maxFlexFractionSoFar为最大的单位弹性值。所谓最大的单位弹性值是由“childSize(child, extent) / _getFlex(child)”语句计算得到,意为每一份孩子的弹性值所占用的空间,尽量取最大的那个孩子单位弹性空间值,这样最大孩子的尺寸才会被弹性空间所容纳。因为单位弹性空间都是平均分配的,所以将最大的单位弹性空间值乘以总弹性值才是适合的总弹性空间值,再加上非弹性空间值才是最后需要返回的尺寸大小,即公式为:maxFlexFractionSoFar * totalFlex + inflexibleSpace。这个尺寸大小应该是尽量满足孩子自身大小并且适配弹性值的一个合适的尺寸值。再看循环中的具体逻辑,当孩子的弹性值flex大于0时,孩子尺寸才参与弹性计算,否则孩子的尺寸就属于非弹性值的一部分了。可以看到计算孩子尺寸函数“childSize(child, extent)”中第二个参数extent传入的是父亲非布局方向的尺寸,孩子在这个方向上的活动范围当然是参照这个尺寸值了。

第二部分代码比较多,这里再进一部分拆解第二部分代码:

final double availableMainSpace = extent;
int totalFlex = 0;
double inflexibleSpace = 0.0;
double maxCrossSize = 0.0;
RenderBox child = firstChild;
while (child != null) {
  final int flex = _getFlex(child);
  totalFlex += flex;
  double mainSize;
  double crossSize;
  if (flex == 0) {
    switch (_direction) {
	  case Axis.horizontal:
	    mainSize = child.getMaxIntrinsicWidth(double.infinity);
	    crossSize = childSize(child, mainSize);
	    break;
	  case Axis.vertical:
	    mainSize = child.getMaxIntrinsicHeight(double.infinity);
	    crossSize = childSize(child, mainSize);
	    break;
    }
    inflexibleSpace += mainSize;
    maxCrossSize = math.max(maxCrossSize, crossSize);
  }
  final FlexParentData childParentData = child.parentData as FlexParentData;
  child = childParentData.nextSibling;
}
......

第二部分,当计算的尺寸方向和FlexBox的布局方向不一致时,这里的主要逻辑肯定是围绕取纵轴方向上的最大尺寸值,即取最大孩子的纵轴尺寸值了。可以看到第一条语句“final double availableMainSpace = extent”意为主轴方向可分配的总空间大小,extent值肯定是和尺寸方向不一致,即extend此时肯定就是FlexBox的布局方向上的总空间大小了。而该循坏主要是为了计算这个三个值:totalFlex总弹性值、inflexibleSpace不可弹性的总空间大小、maxCrossSize最大纵轴尺寸(这里只会计算出非弹性的孩子的那部分)。可以看到这里的主要逻辑是计算跟非弹性相关的尺寸,当flex=0时,具体可通过FlexBox的水平和垂直两个布局方向进行计算。注意孩子纵轴尺寸crossSize值的获取一直是通过“childSize”函数获取的(因为子尺寸方向和计算的尺寸方向总是保持一致,并且尺寸方向为纵轴方向),但是“childSize”函数第二个入参值你也需要提供,该值为孩子在布局方向上的尺寸值,可以看到逻辑中取的是孩子的最大固有宽或者高的值。当循环逻辑跳出时,即总弹性值totalFlex和非弹性的总空间大小便计算得到,而最大的纵轴尺寸值只是计算出了非弹性那部分,还有另外的弹性部分并未进行计算。那余下的代码逻辑便主要是计算弹性部分的最大纵轴尺寸值了:

final double spacePerFlex = math.max(0.0,
          (availableMainSpace - inflexibleSpace) / totalFlex);

child = firstChild;
while (child != null) {

  final int flex = _getFlex(child);

  if (flex > 0)
     maxCrossSize = math.max(maxCrossSize, childSize(child, spacePerFlex * flex));

  final FlexParentData childParentData = child.parentData as FlexParentData;
  child = childParentData.nextSibling;
}

return maxCrossSize

代码中的第一条语句便是计算出每个弹性单位表示的空间大小spacePerFlex值,接着再循环每一个孩子,当flex>0时,即进入到具有弹性值的孩子的逻辑中。该逻辑的代码语句为“maxCrossSize = math.max(maxCrossSize, childSize(child, spacePerFlex * flex))”,语句中依然是通过“childSize”函数计算出具有弹性值的孩子的纵轴尺寸大小,可以看到第二个入参值是通过弹性值计算得到的,单位弹性空间大小乘以孩子的弹性值,即公式为:spacePerFlex * flex。当循环跳出时,此时maxCrossSize值即为最大孩子的纵轴尺寸值了,即计算的FlexBox在尺寸方向上的值。

好了,这里便将FlexBox类的基线值和固有值的计算逻辑讲完了,下面接着看FlexBox类中的其余代码:

int _getFlex(RenderBox child) {
    final FlexParentData childParentData = child.parentData as FlexParentData;
    return childParentData.flex ?? 0;
}

该函数是获取孩子的弹性值,其存在子渲染对象的父数据对象中,所以如果我们要实现一个具备弹性值的FlexBox子渲染对象,一定要在安装父数据的“setupParentData(covariant RenderObject child)”函数中,将父数据类型确定为FlexParentData类型才行。如果不设置弹性值,默认为0值,即属于非弹性空间的部分。

FlexFit _getFit(RenderBox child) {
  final FlexParentData childParentData = child.parentData as FlexParentData;
  return childParentData.fit ?? FlexFit.tight;
}

该函数获取的是孩子的适应值,意为孩子如何适应父空间大小。该值的类型为FlexFit,这是一个枚举类型,只有两种适应值:FlexFit.tight - 表示填满父亲的可用空间;FlexFit.loose - 表示可以填满父亲的可用空间,但是尽量适配自己的大小。

double _getCrossSize(RenderBox child) {
  switch (_direction) {
    case Axis.horizontal:
      return child.size.height;
    case Axis.vertical:
      return child.size.width;
  }
  return null;
}

该函数获取的是孩子的纵轴尺寸大小,如果FlexBox的布局方向为水平方向,则获取的是孩子的高度值;如果为垂直方向,则获取的是孩子的宽度值。

double _getMainSize(RenderBox child) {
  switch (_direction) {
    case Axis.horizontal:
      return child.size.width;
    case Axis.vertical:
      return child.size.height;
  }
  return null;
}

该函数获取的是孩子主轴尺寸大小,如果FlexBox的布局方向为水平方向,则获取的是孩子的宽度值;如果为垂直方向,则获取的是孩子的高度值。

这里基本讲完了FlexBox类中除布局和绘制以外的主要函数,那么接下来就正式讲解FlexBox类中最重要的函数,FlexBox类的布局函数:performLayout()。

由于该函数逻辑十分庞大,这里我们从上往下依次拆分逻辑一步步来理解每个部分的实现含义:

final BoxConstraints constraints = this.constraints;

// Determine used flex factor, size inflexible items, calculate free space.
int totalFlex = 0;
int totalChildren = 0;
final double maxMainSize = _direction == Axis.horizontal ? constraints.maxWidth : constraints.maxHeight;
final bool canFlex = maxMainSize < double.infinity;

double crossSize = 0.0;
double allocatedSize = 0.0; // Sum of the sizes of the non-flexible children.
RenderBox child = firstChild;
RenderBox lastFlexChild;

开始部分是一些变量的定义,含义分别如下:

  • constraints - 为父亲传过来的约束范围,有最小宽度、最大宽度、最小高度、最大高度。
  • totalFlex - 为具有弹性大小(flex > 0)的孩子们的弹性值的总和。
  • totalChildren - 为孩子的总数量。
  • maxMainSize - 为主轴方向的尺寸大小,此处取的是父亲允许的最大值。
  • canFlex - 为判断主轴上是否可以进行弹性空间大小的计算,如果主轴尺寸为无限大,肯定是不可以计算的,比如父亲在主轴上是一个无限滚动列表。
  • crossSize - 为纵轴方向的尺寸大小,为孩子们在布局后的真实尺寸总和。
  • allocatedSize - 为主轴方向的尺寸大小,为孩子们在布局后的真实尺寸总和。
  • lastFlexChild - 最后一个具有弹性值的孩子。

以上这些值中,其中totalFlex、totalChildren、crossSize、allocatedSize、lastFlexChild这五个值是需要在孩子的循环中计算获取的,以下是计算代码:

while (child != null) {
  final FlexParentData childParentData = child.parentData as FlexParentData;
  totalChildren++;
  final int flex = _getFlex(child);
  if (flex > 0) {
	totalFlex += childParentData.flex;
	lastFlexChild = child;
  } else {
	BoxConstraints innerConstraints;
	if (crossAxisAlignment == CrossAxisAlignment.stretch) {
	  switch (_direction) {
		case Axis.horizontal:
		  innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
		  break;
		case Axis.vertical:
		  innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth);
		  break;
	  }
	} else {
	  switch (_direction) {
		case Axis.horizontal:
		  innerConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
		  break;
		case Axis.vertical:
		  innerConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
		  break;
	  }
	}
	child.layout(innerConstraints, parentUsesSize: true);
	allocatedSize += _getMainSize(child);
	crossSize = math.max(crossSize, _getCrossSize(child));
  }
  child = childParentData.nextSibling;
}

可以看到在此循环中,对于flex > 0的情况则只是简单的计算了一下总弹性值totalFlex和最后一个具有弹性值的孩子;而主要逻辑在弹性值flex <= 0的判断内实现。对于flex <= 0的情况,没有弹性值的孩子直接进行了子元素的布局函数“layout“的调用,该函数需要传入一个约束范围innerConstraints值和父亲是否使用到孩子尺寸的布尔值parentUsesSize。可以看到parentUsesSize值直接传入的是true,表示父尺寸的计算是依赖于不具备弹性值的孩子们的尺寸大小,而主要的逻辑为确定孩子的约束范围innerConstraints值。对于不具备弹性值的孩子们的内部约束的计算,是依赖于CrossAxisAlignment.stretch值的,我们从前面有提到该值,CrossAxisAlignment.stretch为铺开纵轴方向的范围,这里的范围肯定是纵轴方向上的最大允许布局的范围了。可以看到在crossAxisAlignment = CrossAxisAlignment.stretch的逻辑中,直接使用“BoxConstraints.tightFor”函数来固定纵轴尺寸大小,取值为父亲允许的最大值,而主轴的约束则为0到无穷大。以下是BoxConstraints类中的函数“tightFor“的实现:

const BoxConstraints.tightFor({
    double width,
    double height,
  }) : minWidth = width ?? 0.0,
       maxWidth = width ?? double.infinity,
       minHeight = height ?? 0.0,
       maxHeight = height ?? double.infinity;

由此可见,纵轴尺寸永远传入的是一个固定值(最小值等于最大值),而且是父亲允许的最大值。而主轴值未传入,为空值,则最小值取的是0,最大值取的是无穷大double.infinity,主轴尺寸范围为0到无穷大,即主轴尺寸不受约束。对于纵轴尺寸孩子的大小已经被其父亲确定下来了,而孩子的主轴大小是不受父亲的约束范围所限制的。

再来看当crossAxisAlignment != CrossAxisAlignment.stretch的逻辑中是如何确定孩子的约束范围innerConstraints的,可以看到其使用了“innerConstraints = BoxConstraints(maxHeight: constraints.maxHeight)”来确定这个值,直接使用了BoxConstraints的构造函数创建了这个约束对象,而传入的是纵轴方向被父亲允许的最大尺寸,下面是BoxConstraints类的构造函数的代码实现:

const BoxConstraints({
    this.minWidth = 0.0,
    this.maxWidth = double.infinity,
    this.minHeight = 0.0,
    this.maxHeight = double.infinity,
  });

从上面代码中可以得知,主轴方向尺寸是没有传入的,所以主轴尺寸范围为0到无穷大。而纵轴只传入了一个被父亲允许的最大值,最小值默认为0,所以纵轴尺寸范围为0到父亲约束的最大值之间。此时纵轴值不是固定的,所以在纵轴方向上肯定不是展开所有父尺寸允许的范围大小了,所以此时孩子的大小没有被父亲确定下来,还要通过其自身进行计算,但要在这个约束内进行;而在主轴方向上,孩子的主轴大小同样是不受父亲的约束范围所限制的。

上面仅仅是对没有弹性值的孩子们进行了布局,而布局的主要逻辑在于具有弹性值的孩子们的布局计算,我们接着往下看:

 final double freeSpace = math.max(0.0, (canFlex ? maxMainSize : 0.0) - allocatedSize);
 double allocatedFlexSpace = 0.0;
 double maxBaselineDistance = 0.0;

上面已经对allocatedSize值做了说明,该值为孩子们在主轴方向上的尺寸总和,而此时的总和仅仅为非弹性孩子们尺寸的总和,所以当父亲在允许布局的最大尺寸不是无限大时,则可进行弹性计算的空间大小为:maxMainSize - allocatedSize,否则则取值为0。下面对这三个属性简要解释下其含义:

  • freeSpace - 主轴方向上可进行弹性计算的总空间大小。
  • allocatedFlexSpace - 具有弹性值的孩子们在布局后的真实空间大小总和。
  • maxBaselineDistance - 孩子自身的水平基线距离孩子顶端(即孩子左上顶点(0,0)所在的那条水平线)的距离,在所有孩子中找到最大的那个值。
if (totalFlex > 0 || crossAxisAlignment == CrossAxisAlignment.baseline) {
      final double spacePerFlex = canFlex && totalFlex > 0 ? (freeSpace / totalFlex) : double.nan;
      child = firstChild;
      double maxSizeAboveBaseline = 0;
      double maxSizeBelowBaseline = 0;
      while (child != null) {
        final int flex = _getFlex(child);
        if (flex > 0) {

          final double maxChildExtent = canFlex ? (child == lastFlexChild ? (freeSpace - allocatedFlexSpace) : spacePerFlex * flex) : double.infinity;
          double minChildExtent;

          switch (_getFit(child)) {
            case FlexFit.tight:
              assert(maxChildExtent < double.infinity);
              minChildExtent = maxChildExtent;
              break;
            case FlexFit.loose:
              minChildExtent = 0.0;
              break;
          }
       ......

以上为if判断中的前半部分逻辑代码,只有在总弹性值totalFlex大于0(至少有一个孩子有弹性值)或者纵轴的对齐方向为基线对齐的情况才会进入该if判断中,此段if判断代码逻辑主要是为了进行具备弹性值的孩子们的布局(也可以说是布局剩下的孩子们)。注意看while循环中的逻辑,所有具备弹性值的孩子的布局都在flex > 0的if逻辑中进行,此逻辑也是totalFlex > 0的情况(因为totalFlex > 0才会存在flex > 0),即存在弹性值的孩子进行布局的情况。而单位弹性空间占用大小有可能无意义,当canFlex为为false时,即主轴被父亲允许的最大布局范围为无限大时,单位弹性值spacePerFlex为一个无意义的值。在while循环中,当flex > 0的if判断中,如果canFlex为false,即当spacePerFlex为一个无意义的值时,maxChildExtent便取值为无限大。而循环中的minChildExtent和maxChildExtent这两个值具体含义是什么呢?其实为计算出要传递给具备弹性值的孩子们在主轴上的最小和最大约束值。如果父亲在主轴上最大的布局范围为无限大时,该值maxChildExtent便取值为无限大,否则取值为单位弹性尺寸乘以孩子的弹性值了,即spacePerFlex * flex。可以看到语句“final double maxChildExtent = canFlex ? (child == lastFlexChild ? (freeSpace - allocatedFlexSpace) : spacePerFlex * flex) : double.infinity”在计算上做了优化,这时候最后一个具备弹性值孩子的尺寸计算总是通过减法得到,原因很简单,为了尽量使用完freeSpace的空间,最后的那个孩子空间的最大允许布局的尺寸当然是用这个剩下的空间了。而switch中的逻辑是计算具备弹性值的孩子们在主轴上最小被允许的尺寸,这个值是通过孩子的适应值FlexFit计算获取的。当具备弹性值的孩子的适应值为FlexFit.tight,则孩子的最小被允许的尺寸取值和最大被允许的尺寸值一致,即孩子在主轴上的尺寸值为一个固定值(按弹性值分配的空间大小);当具备弹性值的孩子的适应值为FlexFit.loose,则孩子的最小被允许的尺寸取值为0,此时孩子在主轴上的尺寸布局范围为0到按弹性值分配的空间大小。对于FlexBox类,其孩子默认适配值为FlexFit.tight,按弹性值分配的空间大小。这里需特别注意一下,如果主轴最大尺寸为无穷大(比如可滚动列表),其具备FlexFit.tight值的孩子的尺寸也是无穷大,此时一般会引起程序崩溃;而具备FlexFit.loose值的孩子的尺寸范围为0到无穷大,这也是一个没有约束的大小,也会引起程序崩溃,因其具备弹性值即会引起歧义。当然不具备弹性值的孩子在0到无穷大(既不受约束)的父亲中布局是不会崩溃的。

接着看while循环中属于布局弹性孩子的其余逻辑部分:

BoxConstraints innerConstraints;
if (crossAxisAlignment == CrossAxisAlignment.stretch) {
	switch (_direction) {
	  case Axis.horizontal:
		innerConstraints = BoxConstraints(minWidth: minChildExtent,
						  maxWidth: maxChildExtent,
						  minHeight: constraints.maxHeight,
						  maxHeight: constraints.maxHeight);
		break;
	  case Axis.vertical:
		innerConstraints = BoxConstraints(minWidth: constraints.maxWidth,
						  maxWidth: constraints.maxWidth,
						  minHeight: minChildExtent,
						  maxHeight: maxChildExtent);
		break;
	}
} else {
	switch (_direction) {
	  case Axis.horizontal:
		innerConstraints = BoxConstraints(minWidth: minChildExtent,
						  maxWidth: maxChildExtent,
						  maxHeight: constraints.maxHeight);
		break;
	  case Axis.vertical:
		innerConstraints = BoxConstraints(maxWidth: constraints.maxWidth,
						  minHeight: minChildExtent,
						  maxHeight: maxChildExtent);
		break;
	}
}
child.layout(innerConstraints, parentUsesSize: true);
final double childSize = _getMainSize(child);
assert(childSize <= maxChildExtent);
allocatedSize += childSize;
allocatedFlexSpace += maxChildExtent;
crossSize = math.max(crossSize, _getCrossSize(child));

代码的前半部分在确定弹性孩子的约束范围,对于主轴上的约束前面已经确定过,此处主要是通过CrossAxisAlignment.stretch来确定弹性孩子在纵轴方向上的尺寸范围。当crossAxisAlignment == CrossAxisAlignment.stretch时纵轴尺寸永远取的是父亲允许的最大值,否则纵轴方向的取值范围为0到父亲允许的最大值,该逻辑其实是和非弹性孩子的计算逻辑保持一致。

当弹性孩子的内部约束对象确定之后,即可调用孩子的布局函数“layout”来对弹性孩子进行布局,此时孩子的尺寸size(即宽width和高height)也就被确定下来了,最后主要就是累加弹性孩子的主轴尺寸到总尺寸allocatedSize上,并且计算出最大的纵轴尺寸crossSize值。

而while中余下的逻辑便是计算跟基线相关的尺寸逻辑,下面是当“crossAxisAlignment == CrossAxisAlignment.baseline”的代码片段:

if (crossAxisAlignment == CrossAxisAlignment.baseline) {
  final double distance = child.getDistanceToBaseline(textBaseline, onlyReal: true);
  if (distance != null) {
	maxBaselineDistance = math.max(maxBaselineDistance, distance);
	maxSizeAboveBaseline = math.max(
	  distance,
	  maxSizeAboveBaseline,
	);
	maxSizeBelowBaseline = math.max(
	  child.size.height - distance,
	  maxSizeBelowBaseline,
	);
	crossSize = math.max(maxSizeAboveBaseline + maxSizeBelowBaseline, crossSize);
  }
}

可以看到基线相关的尺寸计算是放在孩子的布局之后的,这个“child”永远是已经布局后的孩子(即调用了“layout”函数)。因为如果你要调用任何RenderBox的获取基线距离函数"getDistanceToBaseline",必须是在该RenderBox完成布局后才有效,由于基线的距离计算绝大部分都是依赖于其在父方向框中的偏移值的,而这个偏移值总是在布局函数"layout"中计算得到的。这里主要是计算出这两个值:最大的基线距离maxBaselineDistance和利用基线重新估量最大的纵轴尺寸crossSize。这里的最大基线距离maxBaselineDistance具体含义为孩子自身的水平基线距离孩子顶端(孩子左上顶点(0,0)所在的那条水平线)的距离,这个距离同时也等于maxSizeAboveBaseline的值,而孩子自身的水平基线距离孩子底部的长度maxSizeBelowBaseline则可通过child.size.height - distance得到并计算出最大值即可。由于此逻辑为基线对齐,即按照每个孩子们的基线对齐,具体看下面一张图即可。

上图中可以看到,蓝色基线为每一个孩子的基线,并且父亲已经按照这条基线对齐了孩子们。蓝色基线距离顶部最大长度的孩子应该为第三个孩子child3,距离底部最大长度的孩子应该为第4个孩子child4,将这两个长度相加(maxSizeAboveBaseline + maxSizeBelowBaseline)即为纵轴方向(该图中为垂直方向)上最小允许的尺寸了,这里就是crossSize值的计算逻辑。

以上while循环跳出后,所有孩子便布局完成了,这些孩子们的尺寸也就确定下来了,这时候就需要确定自身的尺寸了,看以下代码:

// Align items along the main axis.
final double idealSize = canFlex && mainAxisSize == MainAxisSize.max ? maxMainSize : allocatedSize;
double actualSize;
switch (_direction) {
  case Axis.horizontal:
	size = constraints.constrain(Size(idealSize, crossSize));
	actualSize = size.width;
	crossSize = size.height;
	break;
  case Axis.vertical:
	size = constraints.constrain(Size(crossSize, idealSize));
	actualSize = size.height;
	crossSize = size.width;
	break;
}

语句“final double idealSize = canFlex && mainAxisSize == MainAxisSize.max ? maxMainSize : allocatedSize”便在确定主轴上一个合适的尺寸,如果主轴可布局的范围不是无限大的即canFlex为true同时mainAxisSize 配置了取最大值MainAxisSize.max,则选取父亲允许的最大值;否则取孩子们尺寸的总大小allocatedSize值。在switch内部使用了“constraints.constrain“对尺寸又做了一次约束,这个约束意味着不管你最终计算的结果值如何,你都必须在父亲约束值范围内,不能小于最小值同时不能大于最大值,通过这个约束便将自身的大小size确定下来了。而acturalSize便是size中实际的主轴尺寸,而crossSize是size中实际的纵轴尺寸。这里需要特别注意一下,对于最终FlexBox的尺寸size的计算是通过父约束来获取的,具体来看一下BoxContraints类中的函数“constrain(size)“的实现:

Size constrain(Size size) {
  Size result = Size(constrainWidth(size.width), constrainHeight(size.height));
  return result;
}

double constrainWidth([ double width = double.infinity ]) {
  return width.clamp(minWidth, maxWidth) as double;
}

double constrainHeight([ double height = double.infinity ]) {
  return height.clamp(minHeight, maxHeight) as double;
}

该函数分别使用了“constrainWidth(width)”和“constrainHeight(height)”这两个函数来计算出宽和高,而最终会走到Double类中的“clamp(minVal, maxVal)“这个函数,也就是说宽和高最小不能小于父约束的最小值,最大不能大于父约束的最大值maxHeight。你可以试想一下这个例子:一个固定尺寸(width和height都设置为具体值)的SizedBox组件下嵌套一个FlexBox组件,那么这个FlexBox组件的宽高会是什么?结合上面的代码便可知,SizedBox组件传给FlexBox组件的约束宽和高是一个固定值,即最小宽度等于最大宽度,最小高度等于最大高度。所以当计算FlexBox组件的尺寸size是,最终会通过Double类的clamp(minVal, maxVal)函数计算得到,这时候minVal是等于maxVal值的,这样不管你传任何值到这个函数clamp中,它的最终结果跟传进来的约束不相关,只跟minVal=maxVal相关,即最终FlexBox组件的宽高为SizedBox约束的固定宽高。

 下面我们再回到FlexBox类布局函数“performLayout“的其余逻辑:

final double actualSizeDelta = actualSize - allocatedSize;
_overflow = math.max(0.0, -actualSizeDelta);
final double remainingSpace = math.max(0.0, actualSizeDelta);
double leadingSpace;
double betweenSpace;
final bool flipMainAxis = !(_startIsTopLeft(direction, textDirection, verticalDirection) ?? true);
switch (_mainAxisAlignment) {
  case MainAxisAlignment.start:
	leadingSpace = 0.0;
	betweenSpace = 0.0;
	break;
  case MainAxisAlignment.end:
	leadingSpace = remainingSpace;
	betweenSpace = 0.0;
	break;
  case MainAxisAlignment.center:
	leadingSpace = remainingSpace / 2.0;
	betweenSpace = 0.0;
	break;
  case MainAxisAlignment.spaceBetween:
	leadingSpace = 0.0;
	betweenSpace = totalChildren > 1 ? remainingSpace / (totalChildren - 1) : 0.0;
	break;
  case MainAxisAlignment.spaceAround:
	betweenSpace = totalChildren > 0 ? remainingSpace / totalChildren : 0.0;
	leadingSpace = betweenSpace / 2.0;
	break;
  case MainAxisAlignment.spaceEvenly:
	betweenSpace = totalChildren > 0 ? remainingSpace / (totalChildren + 1) : 0.0;
	leadingSpace = betweenSpace;
	break;
}

这段逻辑便在根据对齐方式来确定孩子们在排序后首间距leadingSpace,即在主轴方向上第一个孩子距离父首边界的距离;以及确定孩子们相互之间的间距betweenSpace。我们先来分析前面几个变量的含义,首先需要理解actualSize和allocatedSize这两个值的含义,actualSize为FlexBox的真实尺寸,而allocatedSize为孩子们布局后的真实尺寸的和,当然acturalSize是有可能等于allocatedSize,也有可能不等于。对于_overflow和remainingSpace值,主要是为了计算不相等的情况。_overflow根据其名称便可知,为溢出的空间,也就是当孩子们布局后的真实尺寸大于父亲FlexBox的真实尺寸,这时孩子们有一部分空间父亲容纳不下,即用_overflow表示这段距离。而remainingSpace表示剩余的空间,即当孩子们布局后的真实尺寸小于父亲FlexBox的真实尺寸,这时父亲将孩子们布局完后还会余下一段未使用的空间,即用remainingSpace表示这段距离。语句“final bool flipMainAxis = !(_startIsTopLeft(direction, textDirection, verticalDirection) ?? true)”中的变量flipMainAxis右表示什么呢?主要是为了区分孩子在主轴上的排列方向是否是从左到右或者从上到下(flipMainAxis=false),还是从右到左或者从下到上(flipMainAxis=true)。接下来这段代码的主要逻辑在swich代码块内,根据主轴的对齐方式_mainAxisAlignment来计算leadingSpace和betweenSpace这两个值。下面分六种对齐方式来一个个讨论:

1. 对齐方式为:MainAxisAlignment.start

这种对齐方式是孩子尽量靠近父方形框的首边界。此时leadingSpace和betweenSpace两个值都取值为0:首尾间距leadingSpace应该取值为0;而孩子之间是没有任何间距的,即betweenSpace同样取值为0值。

2. 对齐方式为:MainAxisAlignment.end

这种对齐方式是孩子尽量靠近父方形框的尾边界。此时对于首边界leadingSpace的计算便是偏移remainingSpace值的距离,即leadingSpace=remainingSpace,此时孩子的尾部边界正好与父方向框的尾部边界重合。同MainAxisAlignment.start方式一样,间距betweenSpace同样取值为0。

3. 对齐方式为:MainAxisAlignment.center

这种对齐方式是孩子放在父方形框的正中心位置。即对于首边界leadingSpace的计算便是取剩余空间的一半,即leadingSpace=remainingSpace / 2,此时首间距和尾间距相同,即孩子们放置在中心位置。这种对齐方式对于间距betweenSpace同样取值为0,孩子之间是没有任何间距的。

4. 对齐方式为:MainAxisAlignment.spaceBetween

这种对齐方式是孩子尽量靠近父方形框的首尾边界,并且孩子之间的距离保持一致(拥有相等的间距)。此时首间距leadingSpace应该取值为0,还要通过计算间距betweenSpace来让尾间距保持0值。间距数量应该是孩子数量减去1,在拿总得剩余空间除以总间距数量即为单位间距空间大小,具体计算公式为:remainingSpace / (totalChildren - 1),当然孩子数量必须大于1才有间距,否则betweenSpace直接取值为0。

5. 对齐方式为:MainAxisAlignment.spaceAround

这种对齐方式是孩子的首尾边距是孩子间距的一半,即leadingSpace = betweenSpace / 2。所有孩子们的总间距数量应该为totalChildren,这时候对于间距betweenSpace的计算就很容易了,即:remainingSpace / totalChildren。

6. 对齐方式为:MainAxisAlignment.spaceEvenly

这种对齐方式是孩子的首位边距和孩子的间距相等,此时孩子的间距数量为totalChildren + 1,betweenSpace的计算公式则为:remainingSpace / (totalChildren + 1),而leadingSpace则和betweenSpace相等。

 

以上关于对齐方式相关的逻辑就讲解完了,此时每个孩子的在父方形框中的偏移位置也能很好的计算出来了,接下来计算孩子的偏移位置就是FlexBox的布局函数“performLayout”的最后部分代码了:

// Position elements
double childMainPosition = flipMainAxis ? actualSize - leadingSpace : leadingSpace;
child = firstChild;
while (child != null) {
  final FlexParentData childParentData = child.parentData as FlexParentData;
  double childCrossPosition;
  switch (_crossAxisAlignment) {
	case CrossAxisAlignment.start:
	case CrossAxisAlignment.end:
	  childCrossPosition = _startIsTopLeft(flipAxis(direction), textDirection, verticalDirection)
						   == (_crossAxisAlignment == CrossAxisAlignment.start)
						 ? 0.0
						 : crossSize - _getCrossSize(child);
	  break;
	case CrossAxisAlignment.center:
	  childCrossPosition = crossSize / 2.0 - _getCrossSize(child) / 2.0;
	  break;
	case CrossAxisAlignment.stretch:
	  childCrossPosition = 0.0;
	  break;
	case CrossAxisAlignment.baseline:
	  childCrossPosition = 0.0;
	  if (_direction == Axis.horizontal) {
		assert(textBaseline != null);
		final double distance = child.getDistanceToBaseline(textBaseline, onlyReal: true);
		if (distance != null)
		  childCrossPosition = maxBaselineDistance - distance;
	  }
	  break;
  }
  if (flipMainAxis)
	childMainPosition -= _getMainSize(child);
  switch (_direction) {
	case Axis.horizontal:
	  childParentData.offset = Offset(childMainPosition, childCrossPosition);
	  break;
	case Axis.vertical:
	  childParentData.offset = Offset(childCrossPosition, childMainPosition);
	  break;
  }
  if (flipMainAxis) {
	childMainPosition -= betweenSpace;
  } else {
	childMainPosition += _getMainSize(child) + betweenSpace;
  }
  child = childParentData.nextSibling;
}

以上我就将布局中剩余的代码全部展示出来,代码内容虽然相对较多,但是我相信能阅读到这里的读者的耐心应该是相当大的,理解这些内容肯定不是难事。注释“Position elements”意为对齐元素的位置,实则为计算孩子们在父FlexBox中的偏移位置“childParentData.offset”,该偏移位置具有两个方向上的偏移值:主轴偏移值childMainPosition和纵轴偏移值childCrossPosition。我们先来看主轴偏移值的计算逻辑,语句“double childMainPosition = flipMainAxis ? actualSize - leadingSpace : leadingSpace”表示第一个孩子在主轴上的偏移值。这里分为两种情况:

1. 当flipMainAxis为true

即主轴的排列方式为从右到左或从下到上,此时第一个孩子在最右边或最下边,childMainPosition值如下图所示(这里举个主轴排列方式为从右到左的例子,从下到上同理):

从上图可知,此时的head-sp=leadingSpace,sp=betweenSpace,而actualSize为这个外围父方形框的宽度,所以childCrossPosition = actualSize - head-sp的值为L1线在主轴上的位置。由于要计算child1左上顶点A点x的值,所以在while循环中还需要减去child1的宽width值,即此时第一个孩子在主轴上x的值为childCrossPosition再减去child1自身的宽,即childCrossPosition  -= _getMainSize(child)。

而后面孩子们的x值计算则为每个孩子偏移自身的宽再加上与其右边孩子的间距即可,由此便可理解后面当flipMainAxis为true的代码逻辑了。

2.当flipMainAxis为false

即主轴的排列方式为从左到右或从上到下,此时第一个孩子在最左边或最上边,childMainPosition值如下图所示(这里举个主轴排列方式为从左到右的例子,从上到下同理):

此时上图的head-sp=leadingSpace,sp=betweenSpace,所以childCrossPosition = leadingSpace的值为L2线在主轴上的位置,此时也正好是点A在主轴上x的值,而其他孩子的x值则为左边孩子的宽度值加上孩子之间的间距,即childCrossPosition   += _getMainSize(child) + betweenSpace。

 

以上是孩子们在主轴上偏移值的计算逻辑,最后来看看在纵轴上偏移值的具体算法。由代码可知,主要逻辑在while循环中switch的代码体中,其需要根据FlexBox的纵轴的对齐方式_crossAxisAlignment值来计算,一共有以下几种计算场景:

1. 当_crossAxisAlignment = CrossAxisAlignment.start或_crossAxisAlignment = CrossAxisAlignment.end

这两种对齐方式是需要根据textDirection或verticalDirection来进一步确定纵轴方向的偏移值的,当纵轴方向为水平方向,需要参考textDirection值;当纵轴方向为垂直方向,需要参考verticalDirection值。我们知道如果纵轴方向是从左到右或从上到下,即当“_startIsTopLeft”函数返回true时,这时如果对齐方式为CrossAxisAlignment.start,则偏移值肯定取0;如果对齐方式为CrossAxisAlignment.end,则孩子在纵轴方向上的偏移值肯定是父方形框尺寸减去自身的尺寸,即childCrossPosition=crossSize - _getCrossSize(child);如果纵轴方向是从右到左或从下到上,即当“_startIsTopLeft”函数返回false时,偏移值的计算逻辑同理可得,这里就不再做重复讲解。

2. 当_crossAxisAlignment = CrossAxisAlignment.center

这种对齐方式是在纵轴上居中孩子们的位置,此时偏移值计算公式为:(crossSize  - _getCrossSize(child)) / 2.0。

3.当_crossAxisAlignment = CrossAxisAlignment.stretch

这种对齐方式是不管纵轴方向是水平还是垂直,永远是靠在父方向框的最左端和最顶端,不依赖于textDirection或verticalDirection两个方向值,即childCrossPosition取0值即可

4.当_crossAxisAlignment = CrossAxisAlignment.baseline

这种对齐方式是按照孩子们的基线对齐,看具体逻辑代码:

childCrossPosition = 0.0;
if (_direction == Axis.horizontal) {
     final double distance = child.getDistanceToBaseline(textBaseline, onlyReal: true);
  if (distance != null)
     childCrossPosition = maxBaselineDistance - distance;
}

只有当FlexBox的布局方向为水平方向才可以在纵轴上以孩子们的基线来对齐孩子,如果为垂直方向,或者没有基线值的孩子们,默认取值和CrossAxisAlignment.stretch对齐方式的取值一致,都是靠在父方形框的顶部边界。从前面已经介绍过,distance为孩子自身的基线距离孩子顶端距离,而maxBaselineDistance为孩子们这个距离值最大的那个孩子的距离。此时是一个什么逻辑呢?以最大基线值的那个孩子的基线值为基准,该孩子靠近父方向框顶部边界,其他孩子按照这条基线值对齐的偏移值计算公式为:maxBaselineDistance - distance。如下图:

该图中第三个孩子child3是具有最大基线值的孩子,具体看第二个孩子child2偏移值的计算,一看便知childCrossPosition = maxBaselineDistance - childBaselineDistance,也就符合上面代码里面的计算逻辑了。其他孩子基于基线的纵轴偏移值计算也是同理可得。

 


好了至此为止,有关FlexBox类的布局函数"performLayout"的逻辑也就全部讲完了,最后我们来对FlexBox的布局按以下几步做一下总结:

1. 计算孩子的内部约束innerConstraints。

2. 根据孩子的内部约束调用孩子的布局函数“layout”。

3. 根据孩子们在布局后的真实尺寸计算父方形框自身的尺寸size值。

4. 根据父方兴框的尺寸size值,计算孩子们的偏移位置childParentData.offset。

即:确定孩子的约束constraints->调用孩子的layout->计算自身的尺寸size->计算孩子的偏移位置offset。

 

接下来我们再看一下FlexBox类中的绘制函数“paint“的实现逻辑:

@override
void paint(PaintingContext context, Offset offset) {
  if (!_hasOverflow) {
    defaultPaint(context, offset);
    return;
  }

  // There's no point in drawing the children if we're empty.
  if (size.isEmpty)
    return;

  if (clipBehavior == Clip.none) {
    defaultPaint(context, offset);
  } else {
    // We have overflow and the clipBehavior isn't none. Clip it.
    context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint, clipBehavior: clipBehavior);
  }
}

从绘制函数代码分析,开头对_hasOverflow的逻辑做了处理,前面已经提到_hasOverflow为孩子们在主轴上的总空间是否在父FlexBox空间内有溢出部分(也可以说FlexBox在主轴上的布局空间是否不能容纳所有孩子)。当没有溢出部分时,则调用defaultPaint(context, offset)函数;如果有溢出,还要判断父亲的尺寸是否为空(宽和高任意一个值为0值),如果为空就不需要绘制任何内容了,如果不为空,这时就需要根据裁剪行为来裁剪溢出的内容。当clipBehavior为Clip.none时,表示不裁剪孩子的溢出部分,这时候孩子的内容就能完全展示了(但最多也只能在屏幕的尺寸范围内展示),也是直接调用的defaultPaint(context, offset)来绘制孩子。当clipBehavior不为Clip.none时是需要对溢出的孩子部分进行裁剪,这部分内容就看不到了,这时候调用的是PaintingContext类的“pushClipRect”函数,注意第四个入参为“defaultPaint(context, offset)”函数,所有最终裁剪后也是需要调用defaultPaint(context, offset)来绘制孩子。下面是defaultPaint(context, offset)函数的具体实现:

void defaultPaint(PaintingContext context, Offset offset) {
  ChildType child = firstChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    context.paintChild(child, childParentData.offset + offset);
    child = childParentData.nextSibling;
  }
}

该函数在循环中会根据父亲的偏移位置offset加上孩子自身在父亲中的偏移位置childParentData.offset来偏移画布,这便按照了布局逻辑绘制出了孩子们的具体位置了。

至于PaintingContext类的“pushClipRect”函数我们暂且不讨论,你只需要知道该函数会根据裁剪行为来裁剪孩子的内容即可,当然其内部还有一些其余逻辑,这里就不做分析了。

 

至此,有关FlexBox类实现的全部逻辑就已讲解完毕了,欢迎大家指正文中不足之处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值