Skip to content

深入研究Flutter布局原理

shenchunxing edited this page Aug 25, 2023 · 1 revision

Widget原理简介

1、何为StatelessWidget、StatefulWidget

image

StatelessWidget、StatefulWidget都继承自Widget。StatelessWidget的方法比较少,没有刷新逻辑,只需要实现build方法返回Widget树即可。StatefulWidget存在页面刷新逻辑,因而将build方法移动到派生类State中,State类用于保存页面状态,有一系列的回调方法,可以通过setState方法触发rebuild,以刷新页面。

​两个类都有一个createElement()方法,以StatelessElement为例:

/// An [Element] that uses a [StatelessWidget] as its configuration.
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}

构造方法会将当前Widget实例传入,build方法会调用Widget的方法,并将自身传入。这里我们可以看出build方法中的参数BuildContext即为Element类。

abstract class Element extends DiagnosticableTree implements BuildContext

我们其实可以看出,这两个类本身并不负责控件的绘制和布局,它们存在的价值就是将build方法产生的Widget树和Element做一个关联,实际的绘制、布局等其实都是由build方法下的各个Widget实现的

2、SingleChildRenderObjectWidget

image

SingleChildRenderObjectWidget继承自RenderObjectWidget,可以容纳一个child Widget,重写了createElement方法。Flutter中很多容器的实现都是继承此实现的,例如Align、ColoredBox、ConstrainedBox等。SingleChildRenderObjectWidget和RenderObjectWidget类都是抽象类,子类需要实现createRenderObject方法来返回不同的RenderObject,并重写updateRenderObject方法以更新,以此来完成不同的布局或者绘制效果。

3、MultiChildRenderObjectWidget

image

MultiChildRenderObjectWidget同样继承自RenderObjectWidget,和SingleChildRenderObjectWidget不同的是,他可以容纳一个child集合,创建的Element实例也不相同。Flutter中,能够容纳多个child widget的容器基本都是继承此类实现。例如Row和Column,他们继承自Flex,而Flex则继承自MultiChildRenderObjectWidget。

4、LeafRenderObjectWidget

这一类可能接触的会相对少一点。官方的类注释中是这么写的:

/// A superclass for RenderObjectWidgets that configure RenderObject subclasses
/// that have no children.

LeafRenderObjectWidget同样继承自RenderObjectWidget,只是没有容纳任何的子Widget,此Widget主要职责就是自己进行绘制操作了。诸如Texture、AndroidSurfaceView、RawImage等,他们所关注的要点是绘制。

5、小结

​在Flutter的官方库中,为我们提供了数百个Widget的实现,同时掌握和了解这么多的Widget的使用是不现实的,也没有必要。官方提供的很多Widget其实都是继承实现的,而从上文我们可以看出,影响Widget样式的实际类是RenderObject,此类才是绘制和布局的核心。掌握并了解了RenderObject,我们完全可以自定进行一些布局或者绘制,这样当官方类库的实现无法满足我们需求的时候,我们也不会毫无办法。

RenderObject解析

1、什么是RenderObject

​ 先来看看官方的介绍:

/// An object in the render tree.
///
/// The [RenderObject] class hierarchy is the core of the rendering
/// library's reason for being.
///
/// [RenderObject]s have a [parent], and have a slot called [parentData] in
/// which the parent [RenderObject] can store child-specific data, for example,
/// the child position. The [RenderObject] class also implements the basic
/// layout and paint protocols.

可以看出,RenderObject是渲染库实现的核心。我们可以通过实现它来完成Widget的测量、布局绘制等一系列操作。在上篇中我们也可以看到各个核心类库的实现都是通过自定义RenderObject实现。

​RenderObject可以有一个parentData,可以用来存储一些child的特定信息,比如child的位置信息等。但RenderObject类本身并没有定义为一个容器,想要容纳其他的RenderObject,需要通过官方提供的一些mixin实现。RenderObject是一个比较顶层的定义,在很多场景下,实现一个RenderObject的子类RenderBox会是一个更好的选择。

2、RenderObject主要方法介绍

void setupParentData(covariant RenderObject child)

​重写此方法,可以在child被加入进来之前为他设置一个ParenData,定义不同的ParenData可以存储一些我们需要的信息,满足不同的需求。

void markNeedsLayout()

​此方法会对布局信息进行标记,渲染管道会在一个合适的时间对此对象的布局信息进行更新。如果一个RenderObject的Parent声明了需要用到此RenderObject的布局信息,当为此RenderObject调用markNeedsLayout()方法后,也会对此RenderObject的Parent进行标记。当父容器和子容器都需要重新布局的时候,会仅仅通知父容器,当其布局完成后,会调用子容器的layout方法,这样child也会完成布局。

  • Flutter中的layout和Android的layout命名看起来类似,实际的作用却不太一样,在Flutter中执行的layout操作更接近与Android中的measure操作,他主要负责完成Widget的Constraints布局约束的配置。Constraints会影响RenderObject展示的区域。

void layout(Constraints constraints, { bool parentUsesSize = false })

​这个方法通常是为child调用的。这个方法会将Constraints传递给child,用来描述child可以使用的宽高约束,child需要遵循这个约束。如果需要child的宽高来调整大小信息,则需要传递parentUsesSize为true,这样当child布局信息发生改变之后会通知此RenderObject来更新布局信息。这个方法不应该被重写,作为替代应该去重写performResize或者performLayout方法,layout方法会把实际工作代理给这两个方法。如果RenderObject定义为容器,那么他需要为容纳的所有RenderObject调用此方法。

bool get sizedByParent

​这个get方法默认返回的是false,这样返回的话表明这个RenderObject的大小信息不会受到父容器的影响,这样我们可以在performLayout方法中为这个RenderObject设置大小信息。如果返回true,那我们需要把RenderObject的大小设置调整到performResize方法中实现。

void performResize()

​重写此方法以用来更新Widget的大小信息。如果想要依赖父容器的大小来设置大小,则需要sizedByParent 返回true。父容器完成布局工作后,这个方法会被调用,可以在这里拿到父容器传递的布局约束信息。这个方法只有sizedByParent返回true的时候才会被调用。

void performLayout() ​通常情况下我们会重写此方法来为此RenderObject设置大小信息,如果有child,还需要在这个方法里为child调用layout方法。

void markNeedsPaint()

​这个方法在RenderObject需要重新绘制的时候调用。调用之后渲染管道会进行规划,会在一个合适的实际调用次RenderObject的paint方法完成绘制更新。

Rect get paintBounds

​通过这个方法拿到此RenderObject应该绘制的区域。如果返回为null,那么这个RenderObject将会被完整绘制。这个Rect也是showOnScreen方法显示Rect所使用的。

void paint(PaintingContext context, Offset offset)

​重写此方法,通过调用PaintingContext.canvas ,我们可以拿到Canvas对象来进行一些绘制。通常情况下,我们需要把此canvas的起点平移到offset,如果不进行移动,canvas的默认起点将会是屏幕的左上角。如果此RenderObject是一个容器类型,那么可以在这里调用PaintingContext.paintChild来绘制指定的child,paintChild方法需要传递一个child和Offset,根据传入的Offset不同,会影响到child在屏幕中显示位置。

​相对Android的onDraw方法,Flutter中的paint方法里进行绘制的时候,需要根据Offset进行偏移绘制。这个Offset定义了此RenderObject应该绘制的起点信息。

Rect operator &(Size other) => Rect.fromLTWH(dx, dy, other.width, other.height);

​Offset类中重写了操作符&,我们之前也确定了此RenderObject的Size,通过Offset & Size我们可以拿到Rect,这个区域就是此RenderObject应该绘制的区域。

  • 虽然通过offset & size可以拿到一个rect,但是我们实际绘制中是可以不在这个区域内进行的,你可以在屏幕的任何区域进行绘制,但是最好还是遵守此区域的设置,不要绘制出边界,除非你有特殊的需求。

3、RenderBox

​说到RenderObject就不得不说到RenderBox,RenderBox是RenderObject的一个非常重要的子类。因为RenderObject的定义比较顶层,甚至连宽高信息这一很重要的属性都没有定义,因而我们通常都需要继承RenderBox来进行定制RenderObject。

​RenderBox中定义了一系列的获取宽高的方法,诸如getMinIntrinsicHeight、getMaxIntrinsicWidth等,这类方法本质交由外部调用的,用于暴露此RenderBox的宽高信息。我们应该重写的是一些诸如computeMaxIntrinsicWidth、computeMinIntrinsicHeight

方法,计算并返回我们想要的宽高信息。RenderBox还提供了Size属性,用来指定此RenderBox的尺寸,这个Size通常会根据约束信息进行计算,从而得到一个合适的尺寸。Size的尺寸不能超过布局约束中的constraints.maxWidth和constraints.maxHeight,否则会导致渲染出错。RenderBox中还定义了getDistanceToBaseline方法供外部调用,用来返回距离给定的TextBaseline的y轴距离。getDistanceToBaseline方法的返回值由computeDistanceToActualBaseline方法计算得来。

​RenderBox的子类众多,官方提供的大部分容器的的RenderObject的实现都是RenderBox的子类。诸如Align,他继承自SingleChildRenderObjectWidget,RenderObject的具体实现是RenderPositionedBox,RenderPositionedBox的继承关系如下: image

看起来是不是很多?其实这只是官方做的职责分明,各个实现类里做了一些不同的事情。RenderShiftedBox会沿用child的宽高信息,并重写了paint方法,使child的绘制在正确的区域。RenderAligningShiftedBox则会通过计算Alignment,为child的parentData设置一个Offset,

核心方法如下

/// Apply the current [alignment] to the [child].
///
/// Subclasses should call this method if they have a child, to have
/// this class perform the actual alignment. If there is no child,
/// do not call this method.
///
/// This method must be called after the child has been laid out and
/// this object's own size has been set.
@protected
void alignChild() {
  _resolve();
  assert(child != null);
  assert(!child!.debugNeedsLayout);
  assert(child!.hasSize);
  assert(hasSize);
  assert(_resolvedAlignment != null);
  final BoxParentData childParentData = child!.parentData as BoxParentData;
  childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}

RenderPositionedBox则主要用来定义容器的大小,他的size会尽可能的填满布局constraints的大小。如果有child,则会调用RenderAligningShiftedBox的alignChild方法计算偏移,最终让child显示在指定的位置。

4、小结

​RenderObject是Render树里的一个对象,他的实现会影响到Widget最终显示在屏幕上的效果。他定义了一些基本的方法,通过重写 performLayout()或者performResize()方法,我们确认此RenderObject的大小信息,通过在performLayout()方法中为child调用layout,来传递布局约束信息。paint方法里我们可以进行一些绘制,如果有child,还需要为child进行绘制。绘制顺序不同也会影响实际显示效果。诸如Decoration的实现,前景色和背景色的效果主要还是绘制顺序的影响。想要改变child显示的位置,我们需要给child设置一定的绘制Offset,Offset是相对于此Widget而言的。通常来说,Offset存储在child的parentData里,调用paintChild绘制时,需要取出parentData中的Offset结合上文传递的Offset进行绘制。

Clone this wiki locally