美文网首页Flutter学习日记flutter
Flutter自定义布局套路

Flutter自定义布局套路

作者: 最近不在 | 来源:发表于2018-08-13 21:02 被阅读168次

    开始

    在Android中我们要实现一个布局需要继承ViewGroup, 重写其中的onLayoutonMeasure方法. 其中onLayout负责给子控件设置布局区域, onMeaseure度量子控件大小和自身大小. 今天我们就研究下Flutter是如何实现布局的.

    Flutter布局

    首先我们挑选一个Flutter控件去看源码, 我们就选Stack, 因为它足够简单. 从表象上讲它只要重叠摆放一组子控件即可. 先看下Stack的源码:

    class Stack extends MultiChildRenderObjectWidget {
      Stack({
        Key key,
        this.alignment: AlignmentDirectional.topStart,
        this.textDirection,
        this.fit: StackFit.loose,
        this.overflow: Overflow.clip,
        List<Widget> children: const <Widget>[],
      }) : super(key: key, children: children);
    
      final AlignmentGeometry alignment;
      final StackFit fit;
      final Overflow overflow;
    
      @override
      RenderStack createRenderObject(BuildContext context) {
        return new RenderStack(
          alignment: alignment,
          textDirection: textDirection ?? Directionality.of(context),
          fit: fit,
          overflow: overflow,
        );
      }
    
      @override
      void updateRenderObject(BuildContext context, RenderStack renderObject) {
        renderObject
          ..alignment = alignment
          ..textDirection = textDirection ?? Directionality.of(context)
          ..fit = fit
          ..overflow = overflow;
      }
    
      @override
      void debugFillProperties(DiagnosticPropertiesBuilder properties) {
        super.debugFillProperties(properties);
        properties.add(new DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
        properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
        properties.add(new EnumProperty<StackFit>('fit', fit));
        properties.add(new EnumProperty<Overflow>('overflow', overflow));
      }
    }
    

    Stack继承自MultiChildRenderObjectWidget, 重写了createRenderObject其返回了一个RenderStack对象, 实际的工作者. 而updateRenderObject则只是修改RenderStack对象的属性. debugFillProperties方法则是填充该类属性的参数值到DiagnosticPropertiesBuilder中.

    我们看看Flex, 也是如此, 重写了createRenderObject其返回了一个RenderFlex对象, 实际的工作者. 而updateRenderObject则只是修改RenderFlex对象的属性.

    所以我们接下来看看RenderStack, 精简代码如下:

    class RenderStack extends RenderBox
        with ContainerRenderObjectMixin<RenderBox, StackParentData>,
             RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
      RenderStack({
        List<RenderBox> children,
        AlignmentGeometry alignment: AlignmentDirectional.topStart,
        TextDirection textDirection,
        StackFit fit: StackFit.loose,
        Overflow overflow: Overflow.clip,
      }) : assert(alignment != null),
           assert(fit != null),
           assert(overflow != null),
           _alignment = alignment,
           _textDirection = textDirection,
           _fit = fit,
           _overflow = overflow {
        addAll(children);
      }
    
      bool _hasVisualOverflow = false;
    
      @override
      void performLayout() {
        _resolve();
        assert(_resolvedAlignment != null);
        _hasVisualOverflow = false;
        bool hasNonPositionedChildren = false;
        if (childCount == 0) {
          size = constraints.biggest;
          assert(size.isFinite);
          return;
        }
    
        double width = constraints.minWidth;
        double height = constraints.minHeight;
    
        BoxConstraints nonPositionedConstraints;
        assert(fit != null);
        switch (fit) {
          case StackFit.loose:
            nonPositionedConstraints = constraints.loosen();
            break;
          case StackFit.expand:
            nonPositionedConstraints = new BoxConstraints.tight(constraints.biggest);
            break;
          case StackFit.passthrough:
            nonPositionedConstraints = constraints;
            break;
        }
        assert(nonPositionedConstraints != null);
    
        RenderBox child = firstChild;
        while (child != null) {
          final StackParentData childParentData = child.parentData;
    
          if (!childParentData.isPositioned) {
            hasNonPositionedChildren = true;
    
            child.layout(nonPositionedConstraints, parentUsesSize: true);
    
            final Size childSize = child.size;
            width = math.max(width, childSize.width);
            height = math.max(height, childSize.height);
          }
    
          child = childParentData.nextSibling;
        }
    
        if (hasNonPositionedChildren) {
          size = new Size(width, height);
          assert(size.width == constraints.constrainWidth(width));
          assert(size.height == constraints.constrainHeight(height));
        } else {
          size = constraints.biggest;
        }
    
        assert(size.isFinite);
    
        child = firstChild;
        while (child != null) {
          final StackParentData childParentData = child.parentData;
    
          if (!childParentData.isPositioned) {
            childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
          } else {
            BoxConstraints childConstraints = const BoxConstraints();
    
            if (childParentData.left != null && childParentData.right != null)
              childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
            else if (childParentData.width != null)
              childConstraints = childConstraints.tighten(width: childParentData.width);
    
            if (childParentData.top != null && childParentData.bottom != null)
              childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
            else if (childParentData.height != null)
              childConstraints = childConstraints.tighten(height: childParentData.height);
    
            child.layout(childConstraints, parentUsesSize: true);
    
            double x;
            if (childParentData.left != null) {
              x = childParentData.left;
            } else if (childParentData.right != null) {
              x = size.width - childParentData.right - child.size.width;
            } else {
              x = _resolvedAlignment.alongOffset(size - child.size).dx;
            }
    
            if (x < 0.0 || x + child.size.width > size.width)
              _hasVisualOverflow = true;
    
            double y;
            if (childParentData.top != null) {
              y = childParentData.top;
            } else if (childParentData.bottom != null) {
              y = size.height - childParentData.bottom - child.size.height;
            } else {
              y = _resolvedAlignment.alongOffset(size - child.size).dy;
            }
    
            if (y < 0.0 || y + child.size.height > size.height)
              _hasVisualOverflow = true;
    
            childParentData.offset = new Offset(x, y);
          }
    
          assert(child.parentData == childParentData);
          child = childParentData.nextSibling;
        }
      }
    
      @protected
      void paintStack(PaintingContext context, Offset offset) {
        defaultPaint(context, offset);
      }
    
      @override
      void paint(PaintingContext context, Offset offset) {
        if (_overflow == Overflow.clip && _hasVisualOverflow) {
          context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintStack);
        } else {
          paintStack(context, offset);
        }
      }
    }
    

    可以看出RenderStack接收了所有传递给Stack的参数, 毕竟RenderStack才是实际干活的^^. performLayout负责了所有布局相关的工作. performLayout首先分析StackFit参数, 该参数有3个值:

    • StackFit.loose 按最小的来.
    • StackFit.expand 按最大的来.
    • StackFit.passthrough Stack上层为->Expanded->Row, 横向尽量大, 纵向尽量小.

    得出BoxConstraints. 然后遍历所有子控件, 如果不是Positioned类型子控件, 则将BoxConstraints传给子控件让它根据父控件大小自己内部布局. 并且记录下所有子控件结合RenderStack自生大小得出的最大高度和宽度. 将其设置为当前控件大小.

    接着再继续从头遍历子控件, 如果不是Positioned类型子控件, 根据alignment参数, 设置子控件在父控件中的偏移量, 比如Stack设置了居中, 上面计算出宽100, 高200, 而子控件宽30, 高30, 那么子控件需要偏移x=35, y=85. 如果是Positioned类型的子控件, 先将RenderStacksize大小, 减去Positioned属性里的大小. 再来计算便宜量.

    这个里面有_hasVisualOverflow变量, 如果内容超出RenderStack大小, 其值为true. 也就是我们写布局时, 内容超过范围了, 报出来一个色块提示, 就是如此得出的.
    _overflow属性则指定了子控件的绘制区域是否能超过父控件, 跟Android中的clipChildren属性很像.

    另外我们再分析下IndexedStack, 该控件一次只能显示一个子控件. 其实际差异在RenderIndexedStack

    class RenderIndexedStack extends RenderStack {
      ...
      @override
      bool hitTestChildren(HitTestResult result, { @required Offset position }) {
        if (firstChild == null || index == null)
          return false;
        assert(position != null);
        final RenderBox child = _childAtIndex();
        final StackParentData childParentData = child.parentData;
        return child.hitTest(result, position: position - childParentData.offset);
      }
    
      @override
      void paintStack(PaintingContext context, Offset offset) {
        if (firstChild == null || index == null)
          return;
        final RenderBox child = _childAtIndex();
        final StackParentData childParentData = child.parentData;
        context.paintChild(child, childParentData.offset + offset);
      }
      ...
    }
    

    重写了RenderStackpaintStackhitTestChildren方法, 只绘制选中的子控件, 和接收事件.

    总结

    实现一个自定义布局, 我们需要先继承MultiChildRenderObjectWidget, 然后重写createRenderObjectupdateRenderObject方法, 前者返回我们自定义的RenderBox的对象. 后者更新想要传递的属性. 然后需要我们继承RenderBox, 来扩展我们想要的功能特性.

    相关文章

      网友评论

        本文标题:Flutter自定义布局套路

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