前言
Flutter原生框架提供了MaterialDesign和Cupertino兩種風格的UI,預設支援了非常多 的樣式,不過想做個性化的控制元件仍然需要我們進行自定義。
Flutter像android一樣也提供了一套畫圖API,下面我們就自己動手做一個簡單的Demo,熟悉下自定義Widget的流程,然後探究下介面繪製的原理。
UI層級框架
我們都知道,Flutter的UI框架有三級結構:Widget,Element,RenderObject。Element作為中間層負責維護整個佈局的建立和更新,Widget和Element一一對應,但Element並不一定都會持有一個RenderObejct。
這裡我選擇比較簡單的LeafRenderObjectElement
型別自定義了一個RenderObject,效果圖如下:
其中一個是RenderObejct
實現,另一個是CustomPaint
。
自定義RenderObject實現過程
需要重寫Widget,Element,RenderObject三部分:
SixStarWidget
class SixStarWidget extends LeafRenderObjectWidget {
final Color _paintColor;
final double _starSize;
SixStarWidget(this._paintColor, this._starSize);
/// 在其父Widget對應的Element的updateChild方法中呼叫
@override
LeafRenderObjectElement createElement() {
return SixStarElement(this);
}
/// 在mount方法中呼叫
@override
RenderObject createRenderObject(BuildContext context) {
return SixStarObject(_paintColor, _starSize);
}
/// 在widget重建時會執行此方法
/// 這裡的renderObject是複用的,如果這裡不更新RenderObject, 那麼UI不會改變
@override
void updateRenderObject(BuildContext context, SixStarObject renderObject) {
renderObject
..paintColor = _paintColor
..starSize = _starSize;
}
}
複製程式碼
SixStarElement
/// 葉子節點
class SixStarElement extends LeafRenderObjectElement {
SixStarElement(LeafRenderObjectWidget widget) : super(widget);
}
複製程式碼
SixStarObject
根據RenderObject
的註釋,RenderObject
沒有定義座標系以及各類佈局規則,自行實現佈局繪製較為複雜,而RenderBox
定義了與android相同的笛卡爾直角座標系以及佈局所依賴的其他多種規則。除非不想使用直角座標系,應該用RenderBox
替換RenderObject
。
所以這裡我們還是乖乖聽話,直接選擇從RenderBox
入手,程式碼請戳這裡檢視
UI更新時的處理流程
為什麼重寫上面的方法就可以實現佈局的更新呢?還有,Flutter可以實現響應式更新UI的原理是什麼呢?既然setState不是開啟介面重新整理的直接動作,那什麼時候才會真正開始重新整理UI呢?
答案就是Vsync垂直同步訊號到來時。以獲取到Vsync訊號為分界線,UI重新整理流程分為beforeDrawFrame和beginDrawFrame兩部分,下面整體介紹一下:
在重新整理頁面setState時(beforeDrawFrame)
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
if (result is Future) {
throw FlutterError...
}
_element.markNeedsBuild();
}
複製程式碼
其中StatefulElement.markNeedsBuild()
會把element標記為_dirty = true,然後呼叫BuildOwner.scheduleBuildFor(this)
,把element新增到dirty列表中:
void scheduleBuildFor(Element element) {
_dirtyElements.add(element);
element._inDirtyList = true;
}
複製程式碼
記住這個類BuildOwner,它是承接前後兩個流程的橋樑。 beforeDrawFrame這一部分很簡單,只做了把element新增到dirtyList一件事。
下一個Vsync訊號到達後(beginDrawFrame)
Vsync訊號到達後,flutter-engine層會自動呼叫到framework層的WidgetsBinding.drawFrame
:
void drawFrame() {
...
buildOwner.buildScope(renderViewElement); // 關鍵點1
super.drawFrame(); // 關鍵點2
...
}
複製程式碼
關鍵點1
BuildOwner.buildScope(RenderViewElement)
方法會遍歷_dirtyElements執行element.rebuild
(這裡的RenderViewElement就是根節點RootRenderObjectElement,它在runApp中被構建出來):
void buildScope(Element context, [VoidCallback callback]) {
if (callback == null && _dirtyElements.isEmpty) return;
int dirtyCount = _dirtyElements.length;
int index = 0;
try {
while (index < dirtyCount) {
_dirtyElements[index].rebuild(); // 執行了element.performRebuild()
index += 1;
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
}
}
複製程式碼
這裡的performRebuild
方法在上面整理的Element
依賴圖中的兩個Element子類被分別重寫:
-
RenderObjectElement
會執行updateRenderObject
,而ComponentElement
中會執行updateChild
,這個方法是Flutter佈局構建的核心,也就是我們平時所說的view-diff演算法所在,它的總策略如下:. newWidget == null newWidget != null child == null Returns null. Returns new [Element]. child != null Old child is removed, returns null. Old child updated if possible, returns child or new [Element]. /// 其中child是此Element持有的舊的子element,newWidget是通過Stateless、StatefulWidget的build方法構建出的 Element updateChild(Element child, Widget newWidget, dynamic newSlot) { ... // 第一列的兩種情況 if (newWidget == null) { if (child != null) { // 移除child: child.detachRenderObject() deactivateChild(child); } return null; } // 第二列第二排情況: 都不為null, 需要更新 if (child != null) { // 如果新舊為同一個widget時, 更新下位置即可 if (child.widget == newWidget) { if (child.slot != newSlot) { // 如果持有MultiChild的Element內child的位置有變化時更新位置 updateSlotForChild(child, newSlot); } return child; } // 通過runtimeType和key來判斷(這裡就是為什麼要給widget新增key的原因) if (Widget.canUpdate(child.widget, newWidget)) { if (child.slot != newSlot) { updateSlotForChild(child, newSlot); } // element更新widget, 由兩個子類重寫: // RenderObjectElement會在子類中執行 !!updateRenderObject!! // ComponentElement會在子類中執行rebuild方法,最終呼叫到子Element的updateRenderObject child.update(newWidget); ... return child; } // 如果不是上面的兩種情況,說明不能更新,就把舊的子element放入_inactiveElements,然後從render tree移除 deactivateChild(child); assert(child._parent == null); } // 第二列第一排情況: old == null, new != null // 呼叫element.mount方法,兩個子類的表現為: // RenderObjectElement會在子類中執行 !!createRenderObject!! // ComponentElement仍會執行rebuild方法 return inflateWidget(newWidget, newSlot); } 複製程式碼
-
看到在上面流程中會在多個位置執行
widget.updateRenderObject
,這個方法我們很眼熟,之前SixStarWidget
裡重寫過。在這個方法中,我們更新了RenderObject的相關屬性,在RenderObject內部的setter方法中呼叫了以下方法:- 在 markNeedsPaint() 時會把RenderObject自身新增到 PipelineOwner 的
_nodesNeedingPaint
列表 - 在 markNeedsLayout()時會把RenderObject自身新增到 PipelineOwner 的
_nodesNeedingLayout
列表
- 在 markNeedsPaint() 時會把RenderObject自身新增到 PipelineOwner 的
現在我們的UI更新資料已經來到了PipelineOwner
。
關鍵點2
關鍵點2處WidgetsBinding
類內的super.drawFrame()
是執行的mixin RenderBinding
混入類的方法:
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 遍歷_nodesNeedingLayout列表,執行performLayout()
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint(); // 遍歷_nodesNeedingPaint列表,執行到paint(context, offset)
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
複製程式碼
這就是Flutter的整個重新整理流程,補充一張流程圖
CustomPaint
CustomPaint也可以做到類似效果,這種方式也是重寫了三層結構,不過進行了封裝:
Widget
就是CustomPaint
自身,它繼承了SingleChildRenderObjectWidget
Element
是SingleChildRenderObjectElement
RenderObject
是RenderCustomPaint
,它繼承了RenderProxyBox extends RenderBox with xx
。
CustomPaint
的一個很大優勢在於它是一個會自動重建的Widget,所以不用像RenderObject一樣要考慮維護引數更新、處理繁瑣的標記dirty等。一般情況下,使用CustomPaint
自定義widget是更好地選擇。