本文基於1.12.13+hotfix.8版本原始碼分析。
0、大綱
- RenderBox的用法
- 通過RenderObjectWidget把RenderBox塞進介面
1、RenderBox
在flutter中,我們最常接觸的,莫過於各種各樣的widget了,但是,實際負責渲染的RenderObject是很少接觸的(它們之間的關聯可以看看閒魚的這篇文章:https://www.yuque.com/xytech/flutter/tge705)。而作為一名天天向上的程式設計師,我們自然要去學習一下它的原理,做到知其然且知其所以然。本文會先來看看RenderBox的用法,以此拋磚引玉,便於後面繼續深入flutter的繪製原理。
首先,RenderBox是RenderObject的子類,它將座標系轉換成我們熟知的笛卡爾座標系,更便於使用。使用RenderBox進行繪製(本文暫時不講child),我們需要做三件事:
(1)測量
第一步,我們需要確定檢視大小,並賦值給父類的size屬性。測量有兩種情況,第一種是size由自身決定,第二種是由parent決定。
首先,由自身決定size的情況,需要在performLayout方法中完成測量,通過父類的constraints可得到滿足約束的值:
@override
void performLayout() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(200),
);
}
第二種情況,size由parent決定,這種情況下檢視大小應該完全通過parent提供的constraints測量,不存在其它因素。這種情況下,只要parent的約束不發生變化,就不會重新測量。
這種情況需要重寫sizedByParent並返回true,然後在performResize中完成測量,並且performLayout中不能對size賦值! 感興趣的同學也可以兩邊都寫,看看會發生什麼:)。
@override
void performLayout() {}
@override
void performResize() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(200),
);
}
@override
bool get sizedByParent => true;
(2)繪製
RenderBox的繪製與android原生的view繪製非常相似,同樣是Paint+Canvas的組合,而且api也非常接近,會非常容易上手。
@override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = _color
..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(
0,
0,
size.width,
size.height,
),
paint);
}
這樣是不是就萬事大吉了呢?如果通過上面的程式碼進行繪製,你會發現,不管在外層怎麼設定位置,繪製出來的矩形都是固定在螢幕左上角的!怎麼回事?
這裡就是flutter中繪製與android的最大不同:在這裡繪製的座標系是全域性座標系,即原點在螢幕左上角,而非檢視左上角。
細心的同學可能已經發現,paint方法中還有一個offset引數,這就是經過parent的約束後,當前檢視的偏移量,繪製時應該將它考慮進去:
@override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = _color
..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(
offset.dx,
offset.dy,
offset.dx + size.width,
offset.dy + size.height,
),
paint);
}
(3)更新
在flutter中,是由Widget的配置發生變更而引起的rebuild,而這就是我們要實現的第三步:當檢視屬性發生變更時,標記重新佈局或重新繪製,當螢幕重新整理時就會做相應的重新整理。
這裡涉及到兩個方法:markNeedsLayout、markNeedsPaint。顧名思義,前者標記重佈局,後者標記重繪。
我們需要做的,就是根據屬性的影響範圍,在更新屬性時,呼叫合適的標記方法,例如color變化時呼叫markNeedsPaint,width變化時呼叫markNeedsLayout。另外,兩者都需要更新的情況下,呼叫markNeedsLayout即可,不需要兩個方法都調。
set width(double width) {
if (width != _width) {
_width = width;
markNeedsLayout();
}
}
set color(Color color) {
if (color != _color) {
_color = color;
markNeedsPaint();
}
}
2、RenderObjectWidget
(1)簡介
上面講了一大堆RenderBox的用法,但是,這玩意兒怎麼用到我們熟知的Widget裡面去?
按照正常流程,我們得實現一個Element和一個Widget,然後在Widget中建立Element,在Element中建立和更新RenderObject,另外還得管理一大堆狀態,處理非常繁瑣。所幸flutter為我們封裝了這一套邏輯,即RenderObjectWidget。
相信對flutter有所瞭解的同學都對StatelessWidget和StatefulWidget不陌生,但其實,StatelessWidget和StatefulWidget僅負責屬性、生命週期等的管理,在它們的build方法實現中都會建立RenderObjectWidget,通過它來實現與RenderObject的關聯。
舉個例子,我們經常使用的Image是個StatefulWidget,對應的state的build方法中實際返回了一個RawImage物件,而這個RawImage是繼承自LeafRenderObjectWidget的,這正是RenderObjectWidget的一個子類;再比如Text,它build方法中建立的RichText是繼承自MultiChildRenderObjectWidget,這同樣是RenderObjectWidget的一個子類。
我們再看看RenderObjectWidget頂部的註釋即可明白:
RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
which wrap [RenderObject]s, which provide the actual rendering of the
application.
大概意思就是RenderObject才是實際負責渲染應用的,而RenderObjectWidget提供包裝了RenderObject的配置,方便我們使用。
另外,flutter還分別實現了幾個子類,進一步封裝了RenderObjectWidget,它們分別是LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。其中,LeafRenderObjectWidget是葉節點,不含子Widget;SingleChildRenderObjectWidget僅有一個child;而MultiChildRenderObjectWidget則是含有children列表。這幾個子類根據child的情況分別建立了對應的Element,所以通過這幾個子類,我們只需要關注RenderObject的建立和更新。
(2)用法
以最簡單的LeafRenderObjectWidget為例,我們需要實現createRenderObject、updateRenderObject兩個方法:
class CustomRenderWidget extends LeafRenderObjectWidget {
CustomRenderWidget({
this.width = 0,
this.height = 0,
this.color,
});
final double width;
final double height;
final Color color;
@override
RenderObject createRenderObject(BuildContext context) {
return CustomRenderBox(width, height, color);
}
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) {
CustomRenderBox renderBox = renderObject as CustomRenderBox;
renderBox
..width = width
..height = height
..color = color;
}
}