Flutter - Widget

2021-03-27  本文已影响0人  Codepgq

Flutter中一切都是Widget构成,Widget是不可变的,每个Widget的状态都代表了一帧。

Flutter中一切都是Widget构成,Widget是不可变的,每个Widget的状态都代表了一帧。

Flutter中一切都是Widget构成,Widget是不可变的,每个Widget的状态都代表了一帧。

由于Widge不可变的特性,所以Widget必须是轻量级,不可能是真正的绘制对象。那UI是如何绘制到屏幕之上的呢?

Element

比如要显示一行字符串到屏幕上

@override
Widget build(BuildContext context) {
    return Text("Hello");
}

当程序运行起来之后,首先会根据Widget创建对应的Element,然后Element通过Widget的状态信息(比如大小、位置、文本等),最终转化为RenderObject对象绘制。

所以Widget的定位更像是描述文件,他并不负责绘制等相关内容。而RenderObject只负责绘制,是真正意义上的View。Element负责管理,比如视图的加载、更新操作都由他处理。

Element除了负责做管理者以外,还具有存储属性,比如StatefulElement中的State,就是在StatefulElement中初始化的时候被创建并保存,从而实现了跨Widget的状态恢复功能。

下面代码代码删除了一些判断相关和不影响阅读的部分

class StatefulElement extends ComponentElement {
 
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    _state._element = this;
    _state._widget = widget;
  }

  /// 可以看到这里调用的是_state.build
  @override
  Widget build() => _state.build(this);

  State<StatefulWidget> get state => _state;
  State<StatefulWidget> _state;

  @override
  void reassemble() {
    state.reassemble();
    super.reassemble();
  }

  /// 第一次build
  @override
  void _firstBuild() {
    try {
      final dynamic debugCheckForReturnedFuture = _state.initState() as dynamic;
    } finally {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
    }
    _state.didChangeDependencies();
    super._firstBuild();
  }

  /// 重新构建
  @override
  void performRebuild() {
    if (_didChangeDependencies) {
      _state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
  }

  /// 更新
  @override
  void update(StatefulWidget newWidget) {
    super.update(newWidget);
    final StatefulWidget oldWidget = _state._widget;
    _dirty = true;
    _state._widget = widget as StatefulWidget;
    try {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
      final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
    } finally {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
    }
    rebuild();
  }

  @override
  void activate() {
    super.activate();
    markNeedsBuild();
  }

  @override
  void deactivate() {
    _state.deactivate();
    super.deactivate();
  }

  @override
  void unmount() {
    super.unmount();
    _state.dispose();
    _state._element = null;
    _state = null;
  }

  @Deprecated(
    'Use dependOnInheritedElement instead. '
    'This feature was deprecated after v1.12.1.'
  )
  @override
  InheritedWidget inheritFromElement(Element ancestor, { Object aspect }) {
    return dependOnInheritedElement(ancestor, aspect: aspect);
  }

  @override
  InheritedWidget dependOnInheritedElement(Element ancestor, { Object aspect }) {
   return super.dependOnInheritedElement(ancestor as InheritedElement, aspect: aspect);
  }
  
  bool _didChangeDependencies = false;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _didChangeDependencies = true;
  }

  @override
  DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) {
    return _ElementDiagnosticableTreeNode(
      name: name,
      value: this,
      style: style,
      stateful: true,
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<State<StatefulWidget>>('state', state, defaultValue: null));
  }
}

可能你会觉得你没有用过Element,但其实你应该已经用过很多次啦,ElementBuildContext的实现类,所以我们才可以在context中获取到一些存储的信息。

Flutter中并不是所有的Element都具备RenderObject,仅当Element的子类是RenderObjectElement是才具备RenderObject,如果子类是ComponentElement时则不再RenderObject

一般如:PaddingFlexTextWidgetElement属于RenderObjectElement;而我们常用的StatelessWidgetStatefulElement他们属于ComponentElement,并不具备RenderObject

那你可能就有疑惑了,ComponentElement是怎么刷新的呢?答案是通过:Widget.build()

Element总结

Widget作为配置文件描述如何渲染界面,多个Widget在一起够成Widget Tree(小部件树);而Element表示Widget Tree中的特定位置的实例,多个Elementmount之后,会构成Element TreeElementmount之后才算是激活,激活之后如果Element存在RenderObjectElement就会通过WidgetcreateRenderObject方法创建对应的RenderObject,并与Element一一绑定。

RenderObject

RenderObject是真正的绘制对象,我们的UI如何绘制就是由他控制,我们可以根据Widget对应的RenderObject查看某个Widget的绘制对象。

但是由于RenderObject只实现了最基本的layoutpaint等相关功能,而绘制到屏幕上面还需要坐标体系和布局协议。所以我们在多数情况会用它的子类,RenderBoxRenderSliver。两者的区别就是Sliver用于可滑动的的控件内,例如:ListViewGridView,其他都基本都属于RenderBox

RenderBox

abstract class RenderBox extends RenderObject {
  /// 把ParentData转化为BoxParentData
  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! BoxParentData)
      child.parentData = BoxParentData();
  }

  /// 计算最小的宽度
  @protected
  double computeMinIntrinsicWidth(double height) {
    return 0.0;
  }

  /// 计算最大的宽度
  @protected
  double computeMaxIntrinsicWidth(double height) {
    return 0.0;
  }

  /// 计算最小的高度
  @protected
  double computeMinIntrinsicHeight(double width) {
    return 0.0;
  }

  /// 计算最大的高度
  @protected
  double computeMaxIntrinsicHeight(double width) {
    return 0.0;
  }

 /// 从父类接受的布局约束,一般控件在嵌套的时候是需要根据parent的布局来动态调整自身大小的
  @override
  BoxConstraints get constraints => super.constraints as BoxConstraints;


  /// 计算基线,得到y轴的偏移量
  @protected
  double? computeDistanceToActualBaseline(TextBaseline baseline) { }
  
  /// 执行布局(开始布局)
  @override
  void performLayout() {}
}

computeMinIntrinsicWidthcomputeMaxIntrinsicWidthcomputeMinIntrinsicHeightcomputeMaxIntrinsicHeight这个几个方法值会根据子类对象决定的。同时他们也不是主动调用了,而是通过各自的get方法去获取在调用,然后缓存结果。

我们通过RenderPadding实现细节可以了解
class RenderPadding extends RenderShiftedBox {
  /// Creates a render object that insets its child.
  ///
  /// The [padding] argument must not be null and must have non-negative insets.
  RenderPadding({
    required EdgeInsetsGeometry padding,
    TextDirection? textDirection,
    RenderBox? child,
  }) : assert(padding != null),
       assert(padding.isNonNegative),
       _textDirection = textDirection,
       _padding = padding,
       super(child);

  EdgeInsets? _resolvedPadding;

  void _resolve() {
    if (_resolvedPadding != null)
      return;
    _resolvedPadding = padding.resolve(textDirection);
    assert(_resolvedPadding!.isNonNegative);
  }


  @override
  double computeMinIntrinsicWidth(double height) {
    _resolve();
    final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
    final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
    /// 这里如果child不为空,通过子类的getMinIntrinsicWidth方法获取
    if (child != null) // next line relies on double.infinity absorption
      return child!.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
    return totalHorizontalPadding;
  }
  
   @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    _resolve();
    assert(_resolvedPadding != null);
    
    /// 如果没有child,那么通过自己就可以得出size
    if (child == null) {
      size = constraints.constrain(Size(
        _resolvedPadding!.left + _resolvedPadding!.right,
        _resolvedPadding!.top + _resolvedPadding!.bottom,
      ));
      return;
    }
    
    
    /// 有child的情况,减去padding
    final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!);
    /// 通过child的layout方法,去计算size
    child!.layout(innerConstraints, parentUsesSize: true);
    /// 得到child计算完的数据
    final BoxParentData childParentData = child!.parentData as BoxParentData;
    /// 计算偏移量
    childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top);
    /// 得到size
    size = constraints.constrain(Size(
      _resolvedPadding!.left + child!.size.width + _resolvedPadding!.right,
      _resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom,
    ));
  }
  
}
PerformLayout

通过上图就可以知道Widget是如何通过约束去获取大小。

RenderPadding并没有实现paint方法,因为其继承的是RenderShiftedBox,在RenderShiftedBox内部实现了paint方法,paint方法何时调用并不会给到用户去处理,需要更新绘制的时候,必须通过markNeedsPaint触发界面执行paint绘制。

渲染图层Layer

当调用markNeedsPaint()触发界面重绘是,markNeedsPaint会通过requestVisualUpdate方法触发引擎更新绘制页面,最终通过RenderBindingdrawFrame开始执行RenderObjectpaint方法。

下面代码删除了断言部分的实现

void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
  
  /// 根据isRepaintBoundary属性去判断要更新哪些区域,如果为YES 就判断owner是否为空,不为空就把自身加入到绘制区域中,然后开始向下绘制
    if (isRepaintBoundary) {
      assert(_layer is OffsetLayer);
      if (owner != null) {
        owner!._nodesNeedingPaint.add(this);
        owner!.requestVisualUpdate();
      }
    } else if (parent is RenderObject) { /// 如果父类是RenderObject的实例对象,就往上查找,看是否需要绘制
      
      final RenderObject parent = this.parent as RenderObject;
      parent.markNeedsPaint();
    } else {
      /// 直接向下绘制
      if (owner != null)
        owner!.requestVisualUpdate();
    }
  }

通过源码可以发现isRepaintBoundary是一个get字段,如果一个renderObject需要频繁绘制,那么就可以直接设置为YES,优化性能。

当绘制区域确定时候就会调用pushLayer的方法,其内部会调用createChildContext得到PaintingContext,然后根据childContextoffset去进行绘制图层。所以可以得知PaintingContextLayer是有关联的,每个Layer上绘制的都是独立的图层。

  void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) {
    assert(painter != null);
    // If a layer is being reused, it may already contain children. We remove
    // them so that `painter` can add children that are relevant for this frame.
    if (childLayer.hasChildren) {
      childLayer.removeAllChildren();
    }
    stopRecordingIfNeeded();
    appendLayer(childLayer);
    final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
    painter(childContext, offset);
    childContext.stopRecordingIfNeeded();
  }

  @protected
  PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
    return PaintingContext(childLayer, bounds);
  }

为什么push之后的页面不会影响之前的页面?

因为我们在调用Navigator.push(context, MaterialPageRoute)打开的页面,MaterialPageRoute的父类使用了RepaintBoundary嵌套显示,而RepaintBoundaryRenderObjectRenderRepaintBoundaryRenderPEpaintBoundaryisRepaintBoundary正好是true,所以才可以实现路由堆栈内的页面互不干扰,因为他的PaintingContextLayer不同。

isRepaintBoundary和alawaysNeedsComposition的区别是什么?

两者都会影响Layer的存在,不同的是alwaysNeedsComposition是用于图层混合的,他混合的条件是child != nullalpha != 0alpha != 255

    /// child不为空 并且 透明度不等于0 不等于 255
@override
        bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255);


  @override
  void paint(PaintingContext context, Offset offset) {
    /// 这里进行了优化,如果没有child那么,就不需要进行绘制了
    if (child != null) {
      /// 不需要绘制
      if (_alpha == 0) {
        // No need to keep the layer. We'll create a new one if necessary.
        layer = null;
        return;
      }
      /// 不要透明度
      if (_alpha == 255) {
        // No need to keep the layer. We'll create a new one if necessary.
        layer = null;
        context.paintChild(child!, offset);
        return;
      }
      assert(needsCompositing);
      /// 得到带透明度的layer
      layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
    }
  }

  OpacityLayer pushOpacity(Offset offset, int alpha, PaintingContextCallback painter, { OpacityLayer? oldLayer }) {
    final OpacityLayer layer = oldLayer ?? OpacityLayer();
    layer
      ..alpha = alpha
      ..offset = offset;
    pushLayer(layer, painter, Offset.zero);
    return layer;
  }

Widget、Element、RenderObject、Layer之间的关系?

WidgetElement之间是一对多;

ElementRenderObject的情况下,ElementRenderObject之间是一对一;

RenderObjectLayer之间是多对一,但不是所有的RenderObject都有Layer

示意图
上一篇下一篇

猜你喜欢

热点阅读