Flutter自定義流式布局控件

其實(shí)對(duì)于了解flutter的人來(lái)說(shuō),你可能已經(jīng)知道flutter自身也有流式布局控件,那就是Wrap和Flow,Wrap易用而Flow更靈活,關(guān)于這兩個(gè)組件的用法在這里不做介紹,可自行搜索。那為什么還要自定義流式布局控件呢?現(xiàn)實(shí)開(kāi)發(fā)中,往往有這樣的需求,流式布局中的label數(shù)量是動(dòng)態(tài)的,而又不能顯示過(guò)多,導(dǎo)致整個(gè)頁(yè)面都是流控件,這樣也不美觀,這個(gè)時(shí)候需求就來(lái)了:最大顯示3行。雖然看似是一個(gè)簡(jiǎn)單的屬性,但這個(gè)時(shí)候wrap和flow就很難實(shí)現(xiàn)了。

進(jìn)入正題,首先流式控件是一個(gè)有多個(gè)子控件的控件,它的夫容器必需可以容納多個(gè)子控件。有點(diǎn)類似于Android中的ViewGroup。另外,Wrap和Flow本身不就是這樣的控件嗎?拿來(lái)改造不更方便嗎。經(jīng)過(guò)調(diào)研,發(fā)現(xiàn)Flutter控件源碼是可以拿來(lái)直接用的,不像是Android源碼使用了很多隱藏api,直接拿出來(lái)編譯都不過(guò)。首先我們定義MyFlow繼承MultiChildRenderObjectWidget,以獲得擁有多個(gè)child控件的能力。另外,它還要有一些常用屬性,padding(邊距),spacing(child之間的橫向間隔),runSpacing(child之間的縱向間隔),maxLine(我們想要屬性,最大行數(shù))。如下:

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;
  }
}

通過(guò)研究,我們發(fā)現(xiàn)繼承關(guān)系MultiChildRenderObjectWidget->RenderObjectWidget->Widget,
接下來(lái)實(shí)現(xiàn)renderObjectWidget內(nèi)的這個(gè)方法,用于創(chuàng)建要渲染的對(duì)象:

  RenderObject createRenderObject(BuildContext context);

接下來(lái)就要實(shí)現(xiàn)我們自己的RenderObject類了,流式布局主要在這里實(shí)現(xiàn),上面是對(duì)外公開(kāi)的組件。我們要實(shí)現(xiàn)的RenderObject中,要實(shí)現(xiàn)兩個(gè)功能:
1.對(duì)children測(cè)量和實(shí)行流式布局和繪制
2.對(duì)自己大小動(dòng)態(tài)計(jì)算
通過(guò)對(duì)Flow的學(xué)習(xí)我們需要繼承ContainerRenderObjectMixin(提供了對(duì)children的管理功能)RenderBoxContainerDefaultsMixin(提供了對(duì)children的繪制、點(diǎn)擊響應(yīng)等功能)。


///每個(gè)child都帶一個(gè)parentData,在這里可以定義想用的屬性
class _MyFlowParentData extends ContainerBoxParentData<RenderBox> {
  //是否可用
  bool _dirty = false;
}

///主要實(shí)現(xiàn)
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();
  }

  //核心方法,計(jì)算每個(gè)child的offset,也就是想對(duì)于原點(diǎn)的偏移位置,最終算出來(lái)滿足條件的要參與layout和paint的children,
  //然后根據(jù)要顯示的children的高度,算出窗口高度。
  //不參與顯示的child打上_dirty=ture的標(biāo)記。
  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;
  }

  //因?yàn)槭强v向換行,橫向固定使用父控限定的最大寬度
  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));

    //布局每個(gè)child,_dirty的child自動(dòng)忽略
    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;
    //繪制每個(gè)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}) {
    //響應(yīng)點(diǎn)擊區(qū)域,因?yàn)椴季趾屠L制是同樣的位置 ,沒(méi)有偏移,所以使用默認(rèn)邏輯
    return defaultHitTestChildren(result, position: position);
  }
}

好,到這里就實(shí)現(xiàn)完了,通過(guò)這種思路,我們不僅可以實(shí)現(xiàn)流式布局,其它的行為也是一樣的。對(duì)于剛接手不清楚每個(gè)控件的含義的同學(xué),這里的技巧就是找一個(gè)行為相進(jìn)的控件去模仿、改造,這樣能大大加快學(xué)習(xí)的腳步。
付上效果圖:


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容