Flutter必须理解Widget、Element、RenderObject的关系(二)

3,621 阅读5分钟

注意:为了让分析更加简单,和逻辑清晰,我们去掉了部分源码和注释,只留下了主要的代码和逻辑。如果没有看过上一篇文章,请点击下面的链接。 Flutter必须理解Widget、Element、RenderObject的关系(一)

Element概述

updateChild方法

接着上篇接着来,上面提到过这个这个方法比较重要,我们将单独拿一个章节讲解,下面是updateChild()源码。

abstract class Element extends DiagnosticableTree implements BuildContext {
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
      if (newWidget == null) {
        if (child != null)
          deactivateChild(child);//注释1
        return null;
      }
      if (child != null) {
        if (child.widget == newWidget) {
          if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);//注释2
          return child;
        }
        if (Widget.canUpdate(child.widget, newWidget)) {
          if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);
          child.update(newWidget);//注释3
          return child;
        }
        deactivateChild(child);
      }
      return inflateWidget(newWidget, newSlot);//注释4
    }  
}

这个方法的意思就更新Element配置的一个函数,具体是怎么更新的呢,其实还是比较简单的,在前面的的文章中我们说过关于Element树的概念,这个函数就是从树中移除相关的数据。

  • 注释1

    deactivateChild()是把Element从Element树上删除,

分析到这里我们先告一个小段落,现在终结一下Element。

RenderObject概述

RenderObject定义

还是先从概念上入手,下面是ReaderObject的概念。

An object in the render tree.

这个概念很简单,大概的意思是渲染树上的一个对象。从概念上得出每个RenderObject都挂在一个渲染树上,我们看一下RenderObject的源码,下面是RenderObject的源码。

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  ParentData parentData;
  Constraints _constraints;
  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    
  }
  void paint(PaintingContext context, Offset offset) { }
  
  void performLayout();
  void markNeedsPaint() {
  }
}

我们共提供这些方法可用看出来,RenderObject的主要职责是绘制和布局,也就是我们说的,这是一个真正的渲染对象,我们下面分析一下这个对象是怎么布局和渲染的。

如果我们想在屏幕上想画一个红色的正方形,两个重要的问题需要解决,第一是,画在哪里?第二是,是怎么画?画在哪里就是布局,怎么画就是渲染,下面我们对着两个问题进行分析,先从布局开始。

RenderObject布局

我们先从layout入手,这个方法是计算渲染对象的大小和布局,这方法通常不要被在类覆盖,如果你想重写布局,要重写performLayout()。

  abstract class RenderObject {
    void layout(Constraints constraints, { bool parentUsesSize = false }) {
      
      RenderObject relayoutBoundary;//注释1
      if (!parentUsesSize || sizedByParent || constraints.isTight 
          || parent is! RenderObject) {//注释2
        relayoutBoundary = this;//注释3
      } else {
        final RenderObject parent = this.parent;
        relayoutBoundary = parent._relayoutBoundary;//注释4
      }
       _relayoutBoundary = relayoutBoundary;
      if (sizedByParent) {
          performResize();//注释5
      }
      performLayout();//注释6
      markNeedsPaint();
    }
	}

上面就是layout方法,这方法有两个参数,第一个参数constraints,是通过父类传入的,也就是大家常说的约束从上到下。

  • 注释1

    relayoutBoundary这变量比较重要,我们打算用用个小结讲解。

RelayoutBoundary

在注释1处声明了一个属性relayoutBoundary,属性叫做布局边界,这个这属性是提高渲染效率的(因为在布局和渲染的都会用到这属性),我们知道RenderObject在渲染树中,现在如果一个叶子RenderObject对象发生布局变化,那么一定会导致这个叶子节点的父布局从新布局,这必定导致低效,那么Flutter就用这个属性防止父节点从新布局,但是这个需要满足几个条。

  • parentUsesSize

    layout的第二个参数,父控件的布局是依赖子控件的布局,默认值是false,也就是默认父控件不依赖子控件布局。

  • sizedByParent

    子控件的大小完全在父控件的约束条件下,也就是子控件在父控件的min和max之间。

  • constraints.isTight

    就是min等于max,这比较容易理解。

  • parent is! RenderObject

    这个条件很容易理解了,就是parent不是RenderObject。

上面的4个条件,如果一个成立就执行注释3的代码,注释3的意思布局边界是自己,因为这个节点的布局变化不会引起父节点的从新布局。否则执行注释4,把父节点的布局边界赋值给自己。

  • 注释5

performResize()方法子类实现的方法,这个方法主要的功能是更新渲染对象的大小,当然这个这个方法被调用的条件是sizedByParent是true,也就是说子控件的大小完全在父控件的约束条件下,执行这个方法。

  • 注释6

performLayout()方法同样的是需要子类去实现的,计算RenderObject的布局。

下面我们分析一下RenderConstrainedBox的performLayout方法,还是先看一下RenderConstrainedBox的继承关系。

class RenderConstrainedBox extends RenderProxyBox {
  
}
class RenderProxyBox extends RenderBox {
  
}
abstract class RenderBox extends RenderObject {
  
}

从上面的代码中很明显能看出来RenderConstrainedBox最终继承RenderObject,我们下面就看一下performLayout()方法的具体实现。

class RenderConstrainedBox extends RenderProxyBox {
	  @override
    void performLayout() {
      if (child != null) {
        child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true)
        size = child.size;
      } else {
        size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
      }
    }
}

这个方法能看出来,当调用RenderObject的layout()方法的时候,会调用RenderConstrainedBox的performLayout(),这个方法又会调用child.layout()方法,直到布局完成。约束是从父类传到子类的,但是size是从子类给到父类的,用一张图表示就清楚多了,这个就是大家常说的,constraints从上到下,size从下到上。

RenderObject渲染

在RenderObject中主要渲染的的函数是paint(),在RenderObject中没有具体的实现,需要子类自己实现,下面是paint方法。

void pait(PaintingContext context, Offset offset) { }

上面的paint有两个参数,先解释两个参数的意思,让后自然就能明白这个函数的意思。

  • context

    Context是PaintingContext,这类继承自ClipContext,在ClipContext中有一个Canvas,这样好像就理解了,原来context是用来画东西的画布。

  • offset

    offset是Offset,就是屏幕坐标上的一个位置。

这样我们应该能猜到这个paint()就是一个,将一个context,画到一个屏幕位置上,这里大家可能有一个疑问,比如我们想画一个红色的正方形,现在只有画布和画到那个位置,还没有告诉我红色方方块的大小呢,怎么画?在上一小节中(RenderObject布局),我们讲了performResize()这个函数,这个函数已经计算出红色方块的大小了。

其它推荐

参考