美文网首页
Flutter自定义流式布局控件

Flutter自定义流式布局控件

作者: 小刚_0e61 | 来源:发表于2019-02-02 11:07 被阅读0次

其实对于了解flutter的人来说,你可能已经知道flutter自身也有流式布局控件,那就是Wrap和Flow,Wrap易用而Flow更灵活,关于这两个组件的用法在这里不做介绍,可自行搜索。那为什么还要自定义流式布局控件呢?现实开发中,往往有这样的需求,流式布局中的label数量是动态的,而又不能显示过多,导致整个页面都是流控件,这样也不美观,这个时候需求就来了:最大显示3行。虽然看似是一个简单的属性,但这个时候wrap和flow就很难实现了。

进入正题,首先流式控件是一个有多个子控件的控件,它的夫容器必需可以容纳多个子控件。有点类似于Android中的ViewGroup。另外,Wrap和Flow本身不就是这样的控件吗?拿来改造不更方便吗。经过调研,发现Flutter控件源码是可以拿来直接用的,不像是Android源码使用了很多隐藏api,直接拿出来编译都不过。首先我们定义MyFlow继承MultiChildRenderObjectWidget,以获得拥有多个child控件的能力。另外,它还要有一些常用属性,padding(边距),spacing(child之间的横向间隔),runSpacing(child之间的纵向间隔),maxLine(我们想要属性,最大行数)。如下:

class MyFlow extends MultiChildRenderObjectWidget {
  final EdgeInsets padding;
  final double spacing;
  final double runSpacing;
  final int maxLine;
  MyFlow({Key key,
      this.padding =const EdgeInsets.all(0),
      this.spacing =10,
      this.runSpacing =10,
      this.maxLine =3,
      List children =const []})
:assert(padding !=null),super(key: key, children: RepaintBoundary.wrapAll(children));

  @override
  RenderObject createRenderObject(BuildContext context) {
return MyRenderFlow(
    padding:padding,
    spacing:spacing,
    runSpacing:runSpacing,
    maxLine:maxLine);
  }

@override
  void updateRenderObject(BuildContext context, IKRenderFlow renderObject) {
  renderObject
..padding =padding
..spacing =spacing
..runSpacing =runSpacing
..maxLine =maxLine;
  }
}

通过研究,我们发现继承关系MultiChildRenderObjectWidget->RenderObjectWidget->Widget,
接下来实现renderObjectWidget内的这个方法,用于创建要渲染的对象:

  RenderObject createRenderObject(BuildContext context);

接下来就要实现我们自己的RenderObject类了,流式布局主要在这里实现,上面是对外公开的组件。我们要实现的RenderObject中,要实现两个功能:
1.对children测量和实行流式布局和绘制
2.对自己大小动态计算
通过对Flow的学习我们需要继承ContainerRenderObjectMixin(提供了对children的管理功能)RenderBoxContainerDefaultsMixin(提供了对children的绘制、点击响应等功能)。


///每个child都带一个parentData,在这里可以定义想用的属性
class _MyFlowParentData extends ContainerBoxParentData<RenderBox> {
  //是否可用
  bool _dirty = false;
}

///主要实现
class MyRenderFlow extends RenderBox with
        ContainerRenderObjectMixin<RenderBox, _MyFlowParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, _MyFlowParentData> {
  EdgeInsets _padding;

  set padding(EdgeInsets padding) {
    if (padding == null) {
      return;
    }
    this._padding = padding;
  }

  double _spacing;

  set spacing(double spacing) {
    if (spacing == null) {
      return;
    }
    this._spacing = spacing;
  }

  double _runSpacing;

  set runSpacing(double runSpacing) {
    if (runSpacing == null) {
      return;
    }
    this._runSpacing = runSpacing;
  }

  int _maxLine;

  set maxLine(int maxLine) {
    if (maxLine == null) {
      return;
    }
    this._maxLine = maxLine;
  }

  MyRenderFlow(
      {EdgeInsets padding = const EdgeInsets.all(0),
      double spacing = 10,
      double runSpacing = 10,
      int maxLine = 3})
      : assert(padding != null),
        _padding = padding,
        _spacing = spacing,
        _runSpacing = runSpacing,
        _maxLine = maxLine;

  @override
  bool get isRepaintBoundary => true;

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! _MyFlowParentData)
      child.parentData = _MyFlowParentData();
  }

  //核心方法,计算每个child的offset,也就是想对于原点的偏移位置,最终算出来满足条件的要参与layout和paint的children,
  //然后根据要显示的children的高度,算出窗口高度。
  //不参与显示的child打上_dirty=ture的标记。
  double _computeIntrinsicHeightForWidth(double width) {
    int runCount = 0;
    double height = _padding.top;
    double runWidth = _padding.left;
    double runHeight = 0.0;
    int childCount = 0;
    RenderBox child = firstChild;
    while (child != null) {
      final double childWidth = child.getMaxIntrinsicWidth(double.infinity);
      final double childHeight = child.getMaxIntrinsicHeight(childWidth);
      final _MyFlowParentData childParentData = child.parentData;
      if (runWidth + childWidth + _padding.right > width) {
        if (_maxLine > 0 && runCount + 1 == _maxLine) {
          childParentData._dirty = true;
          child = childAfter(child);
          continue;
        }
        childParentData._dirty = false;
        height += runHeight;
        if (runCount > 0) {
          height += _runSpacing;
        }
        runCount += 1;
        runWidth = _padding.left;
        runHeight = 0.0;
        childCount = 0;
      }
      //更新绘制位置start
      childParentData.offset = Offset(
          runWidth + ((childCount > 0) ? _spacing : 0),
          height + ((runCount > 0) ? _runSpacing : 0));
      //更新绘制位置end
      runWidth += childWidth;
      runHeight = math.max(runHeight, childHeight);
      if (childCount > 0) {
        runWidth += _spacing;
      }
      childCount += 1;
      child = childAfter(child);
    }
    if (childCount > 0) {
      height += runHeight + _runSpacing + _padding.bottom;
    }
    return height;
  }

  //因为是纵向换行,横向固定使用父控限定的最大宽度
  double _computeIntrinsicWidthForHeight(double height) {
    return constraints.maxWidth;
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    double width = _computeIntrinsicWidthForHeight(height);
    return width;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    double width = _computeIntrinsicWidthForHeight(height);
    return width;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    double height = _computeIntrinsicHeightForWidth(width);
    return height;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    double height = _computeIntrinsicHeightForWidth(width);
    return height;
  }

  @override
  void performLayout() {
    RenderBox child = firstChild;
    if (child == null) {
      size = constraints.smallest;
      return;
    }
    size = Size(_computeIntrinsicWidthForHeight(constraints.maxHeight),
        _computeIntrinsicHeightForWidth(constraints.maxWidth));

    //布局每个child,_dirty的child自动忽略
    while (child != null) {
      final BoxConstraints innerConstraints = constraints.loosen();
      final _MyFlowParentData childParentData = child.parentData;
      if (!childParentData._dirty) {
        child.layout(innerConstraints, parentUsesSize: true);
      }
      child = childParentData.nextSibling;
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    RenderBox child = firstChild;
    //绘制每个child
    while (child != null) {
      final _MyFlowParentData childParentData = child.parentData;
      if (!childParentData._dirty) {
        context.paintChild(child, childParentData.offset + offset);
      }
      child = childParentData.nextSibling;
    }
  }

  @override
  bool hitTestChildren(HitTestResult result, {Offset position}) {
    //响应点击区域,因为布局和绘制是同样的位置 ,没有偏移,所以使用默认逻辑
    return defaultHitTestChildren(result, position: position);
  }
}

好,到这里就实现完了,通过这种思路,我们不仅可以实现流式布局,其它的行为也是一样的。对于刚接手不清楚每个控件的含义的同学,这里的技巧就是找一个行为相进的控件去模仿、改造,这样能大大加快学习的脚步。
付上效果图:


image.png

相关文章

网友评论

      本文标题:Flutter自定义流式布局控件

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