Flutter自定义流式布局控件

其实对于了解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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352