Flutter 完整開發實戰詳解(十6、詳解自定義佈局實戰)

本篇將解析 Flutter 中自定義佈局的原理,並帶你深刻實戰自定義佈局的流程,利用兩種自定義佈局的實現方式,完成以下圖所示的界面效果,看完這一篇你將能夠更輕鬆的對 Flutter 隨心所欲。git

前文:github

1、前言

在以前的篇章咱們講過 WidgetElementRenderObject 之間的關係,所謂的 自定義佈局,事實上就是自定義 RenderObjectchild 的大小和位置 ,而在這點上和其餘框架不一樣的是,在 Flutter 中佈局的核心並非嵌套堆疊,Flutter 佈局的核心是在於 Canvas ,咱們所使用的 Widget ,僅僅是爲了簡化 RenderObject 的操做。數組

《9、 深刻繪製原理》的測試繪製 中咱們知道, 對於 Flutter 而言,整個屏幕都是一塊畫布,咱們經過各類 OffsetRect 肯定了位置,而後經過 Canvas 繪製 UI,而整個屏幕區域都是繪製目標,若是在 child 中咱們 「不按照套路出牌」 ,咱們甚至能夠無論 parent 的大小和位置隨意繪製。bash

2、MultiChildRenderObjectWidget

瞭解基本概念後,咱們知道 自定義 Widget 佈局的核心在於自定義 RenderObject ,而在官方默認提供的佈局控件裏,大部分的佈局控件都是經過繼承 MultiChildRenderObjectWidget 實現,那麼通常狀況下自定義佈局時,咱們須要作什麼呢?框架

如上圖所示,通常狀況下實現自定義佈局,咱們會經過繼承 MultiChildRenderObjectWidgetRenderBox 這兩個 abstract 類實現,而 MultiChildRenderObjectElement 則負責關聯起它們, 除了此以外,還有有幾個關鍵的類 : ContainerRenderObjectMixinRenderBoxContainerDefaultsMixinContainerBoxParentDataless

RenderBox 咱們知道是 RenderObject 的子類封裝,也是咱們自定義 RenderObject 時常常須要繼承的,那麼其餘的類分別是什麼含義呢?ide

一、ContainerRenderObjectMixin

故名思義,這是一個 mixin 類,ContainerRenderObjectMixin 的做用,主要是維護提供了一個雙鏈表的 children RenderObject佈局

經過在 RenderBox 裏混入 ContainerRenderObjectMixin , 咱們就能夠獲得一個雙鏈表的 children ,方便在咱們佈局時,能夠正向或者反向去獲取和管理 RenderObject 們 。post

二、RenderBoxContainerDefaultsMixin

RenderBoxContainerDefaultsMixin 主要是對 ContainerRenderObjectMixin 的拓展,是對 ContainerRenderObjectMixin 內的 children 提供經常使用的默認行爲和管理,接口以下所示:學習

/// 計算返回第一個 child 的基線 ,經常使用於 child 的位置順序有關
	double defaultComputeDistanceToFirstActualBaseline(TextBaseline baseline)
	
	/// 計算返回全部 child 中最小的基線,經常使用於 child 的位置順序無關
	double defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline)
	
	/// 觸摸碰撞測試
	bool defaultHitTestChildren(BoxHitTestResult result, { Offset position })
	
	/// 默認繪製
	void defaultPaint(PaintingContext context, Offset offset)
	
	/// 以數組方式返回 child 鏈表
	List<ChildType> getChildrenAsList()

複製代碼

三、ContainerBoxParentData

ContainerBoxParentDataBoxParentData 的子類,主要是關聯了 ContainerDefaultsMixinBoxParentDataBoxParentDataRenderBox 繪製時所需的位置類。

經過 ContainerBoxParentData ,咱們能夠將 RenderBox 須要的 BoxParentData 和上面的 ContainerParentDataMixin 組合起來,事實上咱們獲得的 children 雙鏈表就是以 ParentData 的形式呈現出來的。

abstract class ContainerBoxParentData<ChildType extends RenderObject> extends BoxParentData with ContainerParentDataMixin<ChildType> { }
複製代碼

四、MultiChildRenderObjectWidget

MultiChildRenderObjectWidget 的實現很簡單 ,它僅僅只是繼承了 RenderObjectWidget,而後提供了 children 數組,並建立了 MultiChildRenderObjectElement

上面的 RenderObjectWidget 顧名思義,它是提供 RenderObjectWidget ,那有不存在 RenderObjectWidget 嗎?

有的,好比咱們常見的 StatefulWidgetStatelessWidgetContainer 等,它們的 Element 都是 ComponentElementComponentElement 僅僅起到容器的做用,而它的 get renderObject 須要來自它的 child

五、MultiChildRenderObjectElement

前面的篇章咱們說過 ElementBuildContext 的實現, 內部通常持有 WidgetRenderObject 並做爲兩者溝通的橋樑,那麼 MultiChildRenderObjectElement 就是咱們自定義佈局時的橋樑了, 以下代碼所示,MultiChildRenderObjectElement 主要實現了以下接口,其主要功能是對內部 childrenRenderObject ,實現了插入、移除、訪問、更新等邏輯:

/// 下面三個方法都是利用 ContainerRenderObjectMixin 的 insert/move/remove 去操做
	/// ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject> 
	void insertChildRenderObject(RenderObject child, Element slot) 
	void moveChildRenderObject(RenderObject child, dynamic slot)         
	void removeChildRenderObject(RenderObject child)
	
	/// visitChildren 是經過 Element 中的 ElementVisitor 去迭代的
	/// 通常在 RenderObject get renderObject 會調用
	void visitChildren(ElementVisitor visitor)
	
	/// 添加忽略child _forgottenChildren.add(child);
	void forgetChild(Element child) 
	
	/// 經過 inflateWidget , 把 children 中 List<Widget> 對應的 List<Element>
	void mount(Element parent, dynamic newSlot)
	
	/// 經過 updateChildren 方法去更新獲得  List<Element>
	void update(MultiChildRenderObjectWidget newWidget)
	
複製代碼

因此 MultiChildRenderObjectElement 利用 ContainerRenderObjectMixin 最終將咱們自定義的 RenderBoxWidget 關聯起來。

六、自定義流程

上述主要描述了 MultiChildRenderObjectWidgetMultiChildRenderObjectElement 和其餘三個輔助類ContainerRenderObjectMixinRenderBoxContainerDefaultsMixinContainerBoxParentData 之間的關係。

瞭解幾個關鍵類以後,咱們看通常狀況下,實現自定義佈局的簡化流程是:

  • 一、自定義 ParentData 繼承 ContainerBoxParentData
  • 二、繼承 RenderBox ,同時混入 ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin 實現自定義RenderObject
  • 三、繼承 MultiChildRenderObjectWidget,實現 createRenderObjectupdateRenderObject 方法,關聯咱們自定義的 RenderBox
  • 四、override RenderBoxperformLayoutsetupParentData 方法,實現自定義佈局。

固然咱們能夠利用官方的 CustomMultiChildLayout 實現自定義佈局,這個後面也會講到,如今讓咱們先從基礎開始, 而上述流程中混入的 ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin ,在 RenderFlexRenderWrapRenderStack 等官方實現的佈局裏,也都會混入它們。

3、自定義佈局

自定義佈局就是在 performLayout 中實現的 child.layout 大小和 child.ParentData.offset 位置的賦值。

首先咱們要實現相似如圖效果,咱們須要自定義 RenderCloudParentData 繼承 ContainerBoxParentData ,用於記錄寬高和內容區域 :

class RenderCloudParentData extends ContainerBoxParentData<RenderBox> {
  double width;
  double height;

  Rect get content => Rect.fromLTWH(
        offset.dx,
        offset.dy,
        width,
        height,
      );
}

複製代碼

而後自定義 RenderCloudWidget 繼承 RenderBox ,並混入 ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin 實現 RenderBox 自定義的簡化。

class RenderCloudWidget extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, RenderCloudParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, RenderCloudParentData> {
  RenderCloudWidget({
    List<RenderBox> children,
    Overflow overflow = Overflow.visible,
    double ratio,
  })  : _ratio = ratio,
        _overflow = overflow {
   ///添加全部 child 
    addAll(children);
  }
複製代碼

以下代碼所示,接下來主要看 RenderCloudWidgetoverride performLayout 中的實現,這裏咱們只放關鍵代碼:

  • 一、咱們首先拿到 ContainerRenderObjectMixin 鏈表中的 firstChild ,而後從頭到位讀取整個鏈表。
  • 二、對於每一個 child 首先經過 child.layout 設置他們的大小,而後記錄下大小以後。
  • 三、以容器控件的中心爲起點,從內到外設置佈局,這是設置的時候,須要經過記錄的 Rect 判斷是否會重複,每次佈局都須要計算位置,直到當前 child 不在重複區域內。
  • 四、獲得最終佈局內大小,而後設置總體居中。
///設置爲咱們的數據
@override
void setupParentData(RenderBox child) {
  if (child.parentData is! RenderCloudParentData)
    child.parentData = RenderCloudParentData();
}

@override
  void performLayout() {
    ///默認不須要裁剪
    _needClip = false;

    ///沒有 childCount 不玩
    if (childCount == 0) {
      size = constraints.smallest;
      return;
    }

    ///初始化區域
    var recordRect = Rect.zero;
    var previousChildRect = Rect.zero;

    RenderBox child = firstChild;

    while (child != null) {
      var curIndex = -1;

      ///提出數據
      final RenderCloudParentData childParentData = child.parentData;

      child.layout(constraints, parentUsesSize: true);

      var childSize = child.size;

      ///記錄大小
      childParentData.width = childSize.width;
      childParentData.height = childSize.height;

      do {
        ///設置 xy 軸的比例
        var rX = ratio >= 1 ? ratio : 1.0;
        var rY = ratio <= 1 ? ratio : 1.0;

        ///調整位置
        var step = 0.02 * _mathPi;
        var rotation = 0.0;
        var angle = curIndex * step;
        var angleRadius = 5 + 5 * angle;
        var x = rX * angleRadius * math.cos(angle + rotation);
        var y = rY * angleRadius * math.sin(angle + rotation);
        var position = Offset(x, y);

        ///計算獲得絕對偏移
        var childOffset = position - Alignment.center.alongSize(childSize);

        ++curIndex;

        ///設置爲遏制
        childParentData.offset = childOffset;

        ///判處是否交疊
      } while (overlaps(childParentData));

      ///記錄區域
      previousChildRect = childParentData.content;
      recordRect = recordRect.expandToInclude(previousChildRect);

      ///下一個
      child = childParentData.nextSibling;
    }

    ///調整佈局大小
    size = constraints
        .tighten(
          height: recordRect.height,
          width: recordRect.width,
        )
        .smallest;

    ///居中
    var contentCenter = size.center(Offset.zero);
    var recordRectCenter = recordRect.center;
    var transCenter = contentCenter - recordRectCenter;
    child = firstChild;
    while (child != null) {
      final RenderCloudParentData childParentData = child.parentData;
      childParentData.offset += transCenter;
      child = childParentData.nextSibling;
    }

    ///超過了嘛?
    _needClip =
        size.width < recordRect.width || size.height < recordRect.height;
  }
複製代碼

其實看完代碼能夠發現,關鍵就在於你怎麼設置 child.parentDataoffset ,來控制其位置。

最後經過 CloudWidget 加載咱們的 RenderCloudWidget 便可, 固然完整代碼還須要結合 FittedBoxRotatedBox 簡化完成,具體可見 :GSYFlutterDemo

class CloudWidget extends MultiChildRenderObjectWidget {
  final Overflow overflow;
  final double ratio;

  CloudWidget({
    Key key,
    this.ratio = 1,
    this.overflow = Overflow.clip,
    List<Widget> children = const <Widget>[],
  }) : super(key: key, children: children);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderCloudWidget(
      ratio: ratio,
      overflow: overflow,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, RenderCloudWidget renderObject) {
    renderObject
      ..ratio = ratio
      ..overflow = overflow;
  }
}
複製代碼

最後咱們總結,實現自定義佈局的流程就是,實現自定義 RenderBoxperformLayout child 的 offset

4、CustomMultiChildLayout

CustomMultiChildLayout 是 Flutter 爲咱們封裝的簡化自定義佈局實現,它的內部一樣是經過 MultiChildRenderObjectWidget 實現,可是它爲咱們封裝了 RenderCustomMultiChildLayoutBoxMultiChildLayoutParentData ,並經過 MultiChildLayoutDelegate 暴露出須要自定義的地方。

使用 CustomMultiChildLayout 你只須要繼承 MultiChildLayoutDelegate ,並實現以下方法便可:

void performLayout(Size size);

  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);

複製代碼

經過繼承 MultiChildLayoutDelegate,而且實現 performLayout 方法,咱們能夠快速自定義咱們須要的控件,固然便捷的封裝也表明了靈活性的喪失,能夠看到 performLayout 方法中只有佈局自身的 Size 參數,因此完成上圖需求時,咱們還須要 child 的大小和位置 ,也就是 childSizechildId

childSize 相信你們都能故名思義,那 childId 是什麼呢?

這就要從 MultiChildLayoutDelegate 的實現提及,MultiChildLayoutDelegate 內部會有一個 Map<Object, RenderBox> _idToChild; 對象,這個 Map 對象保存着 Object idRenderBox 的映射關係,而在 MultiChildLayoutDelegate 中獲取 RenderBox 都須要經過 id 獲取。

_idToChild 這個 Map 是在 RenderBox performLayout 時,在 delegate._callPerformLayout 方法內建立的,建立後所用的 idMultiChildLayoutParentData 中的 id, MultiChildLayoutParentData 的 id ,能夠經過 LayoutId 嵌套時自定義指定賦值。

而完成上述佈局,咱們須要知道每一個 child 的 index ,因此咱們能夠把 index 做爲 id 設置給每一個 child 的 LayoutId

因此咱們能夠經過 LayoutId 指定 id 爲數字 index , 同時告知 delegate ,這樣咱們就知道 child 順序和位置啦。

這個 id 是 Object 類型 ,因此你懂得,你能夠賦予不少屬性進去。

以下代碼所示,這樣在自定義的 CircleLayoutDelegate 中,就知道每一個控件的 index 位置,也就是知道了,圓形佈局中每一個 item 須要的位置。

咱們只須要經過 index ,計算出 child 所在的角度,而後利用 layoutChildpositionChild 對每一個item進行佈局便可,完整代碼:GSYFlutterDemo

///自定義實現圓形佈局
class CircleLayoutDelegate extends MultiChildLayoutDelegate {
  final List<String> customLayoutId;

  final Offset center;

  Size childSize;

  CircleLayoutDelegate(this.customLayoutId,
      {this.center = Offset.zero, this.childSize});

  @override
  void performLayout(Size size) {
    for (var item in customLayoutId) {
      if (hasChild(item)) {
        double r = 100;

        int index = int.parse(item);

        double step = 360 / customLayoutId.length;

        double hd = (2 * math.pi / 360) * step * index;

        var x = center.dx + math.sin(hd) * r;

        var y = center.dy - math.cos(hd) * r;

        childSize ??= Size(size.width / customLayoutId.length,
            size.height / customLayoutId.length);

        ///設置 child 大小
        layoutChild(item, BoxConstraints.loose(childSize));

        final double centerX = childSize.width / 2.0;

        final double centerY = childSize.height / 2.0;

        var result = new Offset(x - centerX, y - centerY);

        ///設置 child 位置
        positionChild(item, result);
      }
    }
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
}
複製代碼

總的來講,第二種實現方式相對簡單,可是也喪失了必定的靈活性,可自定義控制程度更低,可是也更加規範與間接,同時咱們本身實現 RenderBox 時,也能夠用相似的 delegate 的方式作二次封裝,這樣的自定義佈局會更行規範可控。

自此,第十六篇終於結束了!(///▽///)

資源推薦

文章

《Flutter完整開發實戰詳解系列》

《移動端跨平臺開發的深度解析》

相關文章
相關標籤/搜索