美文网首页
Flutter - Widget

Flutter - Widget

作者: Codepgq | 来源:发表于2021-03-27 09:47 被阅读0次

    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

    示意图

    相关文章

      网友评论

          本文标题:Flutter - Widget

          本文链接:https://www.haomeiwen.com/subject/lsewqltx.html