本篇將解析 Flutter 中自定義佈局的原理,並帶你深刻實戰自定義佈局的流程,利用兩種自定義佈局的實現方式,完成以下圖所示的界面效果,看完這一篇你將能夠更輕鬆的對 Flutter 隨心所欲。git
前文:github
在以前的篇章咱們講過 Widget
、Element
和 RenderObject
之間的關係,所謂的 自定義佈局,事實上就是自定義 RenderObject
內 child
的大小和位置 ,而在這點上和其餘框架不一樣的是,在 Flutter 中佈局的核心並非嵌套堆疊,Flutter 佈局的核心是在於 Canvas
,咱們所使用的 Widget
,僅僅是爲了簡化 RenderObject
的操做。數組
在《9、 深刻繪製原理》的測試繪製 中咱們知道, 對於 Flutter 而言,整個屏幕都是一塊畫布,咱們經過各類
Offset
和Rect
肯定了位置,而後經過Canvas
繪製 UI,而整個屏幕區域都是繪製目標,若是在child
中咱們 「不按照套路出牌」 ,咱們甚至能夠無論parent
的大小和位置隨意繪製。bash
瞭解基本概念後,咱們知道 自定義 Widget
佈局的核心在於自定義 RenderObject
,而在官方默認提供的佈局控件裏,大部分的佈局控件都是經過繼承 MultiChildRenderObjectWidget
實現,那麼通常狀況下自定義佈局時,咱們須要作什麼呢?框架
如上圖所示,通常狀況下實現自定義佈局,咱們會經過繼承 MultiChildRenderObjectWidget
和 RenderBox
這兩個 abstract
類實現,而 MultiChildRenderObjectElement
則負責關聯起它們, 除了此以外,還有有幾個關鍵的類 : ContainerRenderObjectMixin
、 RenderBoxContainerDefaultsMixin
和 ContainerBoxParentData
。less
RenderBox
咱們知道是 RenderObject
的子類封裝,也是咱們自定義 RenderObject
時常常須要繼承的,那麼其餘的類分別是什麼含義呢?ide
故名思義,這是一個 mixin
類,ContainerRenderObjectMixin
的做用,主要是維護提供了一個雙鏈表的 children RenderObject
。佈局
經過在 RenderBox
裏混入 ContainerRenderObjectMixin
, 咱們就能夠獲得一個雙鏈表的 children ,方便在咱們佈局時,能夠正向或者反向去獲取和管理 RenderObject
們 。post
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
是 BoxParentData
的子類,主要是關聯了 ContainerDefaultsMixin
和 BoxParentData
,BoxParentData
是 RenderBox
繪製時所需的位置類。
經過 ContainerBoxParentData
,咱們能夠將 RenderBox
須要的 BoxParentData
和上面的 ContainerParentDataMixin
組合起來,事實上咱們獲得的 children 雙鏈表就是以 ParentData
的形式呈現出來的。
abstract class ContainerBoxParentData<ChildType extends RenderObject> extends BoxParentData with ContainerParentDataMixin<ChildType> { }
複製代碼
MultiChildRenderObjectWidget
的實現很簡單 ,它僅僅只是繼承了 RenderObjectWidget
,而後提供了 children
數組,並建立了 MultiChildRenderObjectElement
。
上面的
RenderObjectWidget
顧名思義,它是提供RenderObject
的Widget
,那有不存在RenderObject
的Widget
嗎?有的,好比咱們常見的
StatefulWidget
、StatelessWidget
、Container
等,它們的Element
都是ComponentElement
,ComponentElement
僅僅起到容器的做用,而它的get renderObject
須要來自它的child
。
前面的篇章咱們說過 Element
是 BuildContext
的實現, 內部通常持有 Widget
、RenderObject
並做爲兩者溝通的橋樑,那麼 MultiChildRenderObjectElement
就是咱們自定義佈局時的橋樑了, 以下代碼所示,MultiChildRenderObjectElement
主要實現了以下接口,其主要功能是對內部 children
的 RenderObject
,實現了插入、移除、訪問、更新等邏輯:
/// 下面三個方法都是利用 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
最終將咱們自定義的 RenderBox
和 Widget
關聯起來。
上述主要描述了 MultiChildRenderObjectWidget
、 MultiChildRenderObjectElement
和其餘三個輔助類ContainerRenderObjectMixin
、 RenderBoxContainerDefaultsMixin
和 ContainerBoxParentData
之間的關係。
瞭解幾個關鍵類以後,咱們看通常狀況下,實現自定義佈局的簡化流程是:
ParentData
繼承 ContainerBoxParentData
。RenderBox
,同時混入 ContainerRenderObjectMixin
和 RenderBoxContainerDefaultsMixin
實現自定義RenderObject
。MultiChildRenderObjectWidget
,實現 createRenderObject
和 updateRenderObject
方法,關聯咱們自定義的 RenderBox
。RenderBox
的 performLayout
和 setupParentData
方法,實現自定義佈局。固然咱們能夠利用官方的 CustomMultiChildLayout
實現自定義佈局,這個後面也會講到,如今讓咱們先從基礎開始, 而上述流程中混入的 ContainerRenderObjectMixin
和 RenderBoxContainerDefaultsMixin
,在 RenderFlex
、RenderWrap
、RenderStack
等官方實現的佈局裏,也都會混入它們。
自定義佈局就是在 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
,並混入 ContainerRenderObjectMixin
和 RenderBoxContainerDefaultsMixin
實現 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);
}
複製代碼
以下代碼所示,接下來主要看 RenderCloudWidget
中override performLayout
中的實現,這裏咱們只放關鍵代碼:
ContainerRenderObjectMixin
鏈表中的 firstChild
,而後從頭到位讀取整個鏈表。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.parentData
的 offset
,來控制其位置。
最後經過 CloudWidget
加載咱們的 RenderCloudWidget
便可, 固然完整代碼還須要結合 FittedBox
與 RotatedBox
簡化完成,具體可見 :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;
}
}
複製代碼
最後咱們總結,實現自定義佈局的流程就是,實現自定義 RenderBox
中 performLayout
child 的 offset
。
CustomMultiChildLayout
是 Flutter 爲咱們封裝的簡化自定義佈局實現,它的內部一樣是經過 MultiChildRenderObjectWidget
實現,可是它爲咱們封裝了 RenderCustomMultiChildLayoutBox
和 MultiChildLayoutParentData
,並經過 MultiChildLayoutDelegate
暴露出須要自定義的地方。
使用 CustomMultiChildLayout
你只須要繼承 MultiChildLayoutDelegate
,並實現以下方法便可:
void performLayout(Size size);
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
複製代碼
經過繼承 MultiChildLayoutDelegate
,而且實現 performLayout
方法,咱們能夠快速自定義咱們須要的控件,固然便捷的封裝也表明了靈活性的喪失,能夠看到 performLayout
方法中只有佈局自身的 Size
參數,因此完成上圖需求時,咱們還須要 child 的大小和位置 ,也就是 childSize
和 childId
。
childSize
相信你們都能故名思義,那 childId
是什麼呢?
這就要從 MultiChildLayoutDelegate
的實現提及,在 MultiChildLayoutDelegate
內部會有一個 Map<Object, RenderBox> _idToChild;
對象,這個 Map
對象保存着 Object id
和 RenderBox
的映射關係,而在 MultiChildLayoutDelegate
中獲取 RenderBox
都須要經過 id
獲取。
_idToChild
這個 Map
是在 RenderBox performLayout
時,在 delegate._callPerformLayout
方法內建立的,建立後所用的 id
爲 MultiChildLayoutParentData
中的 id, 而 MultiChildLayoutParentData
的 id ,能夠經過 LayoutId
嵌套時自定義指定賦值。
而完成上述佈局,咱們須要知道每一個 child 的 index ,因此咱們能夠把 index 做爲 id 設置給每一個 child 的 LayoutId
。
因此咱們能夠經過 LayoutId
指定 id 爲數字 index , 同時告知 delegate ,這樣咱們就知道 child 順序和位置啦。
這個 id 是
Object
類型 ,因此你懂得,你能夠賦予不少屬性進去。
以下代碼所示,這樣在自定義的 CircleLayoutDelegate
中,就知道每一個控件的 index
位置,也就是知道了,圓形佈局中每一個 item 須要的位置。
咱們只須要經過 index
,計算出 child 所在的角度,而後利用 layoutChild
和 positionChild
對每一個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 的方式作二次封裝,這樣的自定義佈局會更行規範可控。
自此,第十六篇終於結束了!(///▽///)