Flutter檢視基礎簡介--Widget、Element、RenderObject

country5發表於2018-06-21

前言:Flutter官方文件裡的一句話:you build your UI out of widgets(使用Flutter開發UI介面時,都是使用Widget),然而,Widget並不是我們真正看到的檢視,背後究竟是什麼?其實Flutter Framework提供了三種檢視樹,即:Widget Element RenderObject,只不過,我們使用Flutter開發介面時,通常只和widget打交道,就如前文中所展示的Materail風格或者Cupertino(IOS風格)的各種Widget,然而Flutter介面開發是一種響應式程式設計,並且Widget都是immutable的,那麼,真正的渲染,重新整理,佈局這些問題是誰來處理呢?本文就來了解一下除了Widget,還有哪些基礎類在背後支撐Widget的快速輕量渲染;

  Widget部分:

看下官網對Widget的定義和描述:Describes the configuration for an Element. (為Element提供配置資訊),從這一句話,隱隱感覺到Widget和Element是緊密關聯著的,並且給了Element某些資訊,難道是老闆和員工的關係?老闆給員工發個指令,具體的事情都讓員工去做了?我們再嘗試著從官方文件中提取一些詳細資訊,先來看看到底什麼是Widget?
我們先來看下Widget這個類中都包含哪些屬性:
1:KEY key
2:int hasCode
3:TYPE runtimeType
Widget是使用者介面的一部分,並且是不可變的(immutable)。Widget會被inflate到Element,並由Element管理底層渲染樹。Widget本身沒有可變狀態(所有的欄位必須是final)。如果想要把可變狀態與Widget關聯起來,可以使用StatefulWidget,StatefulWidget通過使用StatefulWidget.createState方法建立State物件,並將之擴充到Element以及合併到樹中;

所以,Widget並不會直接管理狀態及渲染,而是通過State這個物件來管理狀態,下篇文章會主要講到State這個物件;

給定的Widget可以被包含在樹中(零次或多次)。一個給定的Widget可以放置在樹中多次,比如:多個TextWidget。每次將一個Widget放入樹中時,它都會被擴充到一個Element中,這也意味著多次併入樹中的Widget將會被多次擴充進對應的element。

比如在實際介面開發當中,一個UI檢視樹可能包含有多個TextWidget(Widget可能被使用多次),但是這些都叫作TextWidget的widget卻被填充(inflate)進一個個獨立的Element中;

Widget中的Key這個屬性控制一個Widget如何替換樹中的另一個Widget。如果兩個Widget的runtimeType和key屬性相等(==),則新的widget通過更新Element(即通過使用新的Widget呼叫Element.update)來替換舊的Widget。否則,如果兩個Widget的runtimeType和key屬性不相等,則舊的Element將從樹中被移除,新的Widget將被擴充到一個新的Element中,這個新的Element將被插入樹中。

這裡主要涉及到Widget的更新和移除,插入等操作,在執行此操作前,會用Widget的兩個屬性:runtimeType和key來進行對比判斷;

  Element部分

Flutter建立Element的可見樹,相對於Widget來說,是可變的,通常的Flutter介面開發中,我們不用直接操作Element,而是由框架層實現內部邏輯;就如一個UI檢視樹中,可能包含有多個TextWidget(Widget被使用多次),但是放在內部檢視樹的視角,這些TextWidget都是填充到一個個獨立的Element中;

同樣,我們先來看一下Element這個類中的屬性:

Element property表格
propertyTypeDescimplement
depthint樹根Element的深度必須大於0
int get depth => _depth;
dirtybool如果Element已經被標註成需要重建,返回true
bool get dirty => _dirty;
hashCodeint
@overrideint get hashCode => _cachedHash;
ownerBuildOwner管理Element生命週期
@overrideBuildOwner get owner => _owner;
renderObjectRenderObject如果此物件是RenderObjectElement,則渲染物件是樹中此位置處的物件。否則,這個getter將沿著樹走下去,直到找到一個RenderObjectElement。
sizeSize 省略 省略
slot 省略 省略
widgetWidget這個Element的配置資訊
@overrideWidget get widget => _widget;
runtimeType
可以從Element的屬性中看到renderObject和widget,說明Element持有這兩個類的例項;
再來看看文件中對於Element的介紹:An instantiation of a Widget at a particular location in the tree. Element是在樹中特定位置Widget的例項;
這裡進一步描述了Widget與Element之間的關係:Element是Widget的例項,也就是說,Element才是真正幹活的員工,執行老闆戰略部署;我們接著看文件中的描述:
Widget描述如何配置子樹,但由於Widget是不可變的(immutable),因此可以使用相同的Widget同時配置多個子樹。Element表示Widget配置樹中的特定位置的例項。隨著時間的推移,與給定Element關聯的Widget可能隨時會發生變化,例如,如果父Widget重建併為此位置建立新的Widget。Element構成一棵樹。大多數Element都有一個唯一的子Element,但是一些Widget(例如RenderObjectElement的子類)可以有多個子Element。
Element具有以下生命週期:
  • 框架層通過呼叫即將被用來作為Element的初始化配置資訊的Widget的Widget.createElement方法來建立Element;
  • 框架層通過呼叫mount方法來將新建立的Element新增到給定父級中給定槽點的樹上。 mount方法負責將任何Widget擴充到Widget並根據需要呼叫attachRenderObject,以將任何關聯的渲染物件附加到渲染樹上。
  • 此時,Element被視為“啟用的”,並可能出現在螢幕上。
  • 在某些情況下,父(Element)可能會更改用於配置此Element的Widget,例如因為父Element重新建立了新狀態。發生這種情況時,框架層將呼叫新的Widget的update方法。新Widget將始終具有與舊Widget相同的runtimeType和key屬性。如果父Element希望在樹中的此位置更改Widget的runtimeType或key,可以通過unmounting(解除安裝)此Element並在此位置擴充新Widget來實現。
  • 在某些時候,祖先Element可能會決定從樹中移除該Element(或中間祖先Element),祖先Element自己通過呼叫deactivateChild來完成該操作。停用中間祖先將從渲染樹中移除該Element的渲染物件,並將此Element新增到owner屬性中的非活動元素列表中,從而讓框架層呼叫deactivate方法作用在此Element上。
  • 此時,該Element被視為“無效狀態”,並且不會出現在螢幕上。一個Element可以保持”非活動"狀態,直到當前動畫幀結束。在動畫幀結束時,任何仍處於非活動狀態的Element都將被解除安裝。
  • 如果Element被重新組合到樹中(例如,因為它或其祖先之一有一個全域性鍵(global key)被重用),框架層將從owner屬性中的非活動Element列表中移除該Element,並呼叫該Element的activate方法,並重新附加Element的渲染物件到渲染樹。 (此時,Element再次被視為“活動狀態”並可能出現在螢幕上。)
  • 如果Element在當前動畫幀的末尾沒有被重新組合到樹中,則框架層將呼叫該元素的unmount方法。
  • 此時,該元素被視為“已停用”,並且將來不會併入樹中。

由此我們可知:Element存放Widget上下文,通過遍歷檢視樹,Element 同時持有 Widget 和 RenderObject;

  RenderObject部分

官網定義:An object in the render tree.渲染樹中的一個物件。從其名字,我們可以很直觀地知道,它就是負責渲染的工作;
RenderObject的屬性太多,且該類的細節涉及很多渲染知識,我們會在後面的系列中再詳細說明其工作原理,在此先繼續看下官網對其描述:RenderObject類的層次結構是渲染庫的核心。
RenderObjects有一個父級,並有一個名為parentData的插槽,其中父級RenderObject可以儲存特定於子級的資料,例如子級位置。 RenderObject類也實現了基本的佈局和繪製協議。
但是,RenderObject類沒有定義子模型(例如,節點是否有零個,一個或多個子節點)。它也沒有定義座標系(例如,子級是否位於笛卡爾座標系,極座標系等)或特定的佈局協議(例如佈局是寬度高度還是尺寸約束或者父級在子級佈置之前還是之後設定子級的大小和位置等;或者確實是否允許子級讀取他們父級的parentData插槽)。RenderBox子類引入佈局系統使用笛卡爾座標。
編寫一個RenderObject子類
在大多數情況下,RenderObject本身的子類化過度,RenderBox將是一個更好的起點。但是,如果渲染物件不想使用笛卡爾座標系,那麼它應該直接從RenderObject繼承。這允許它通過使用約束的新子類而不是使用BoxConstraints來定義自己的佈局協議,並且可能使用全新的一組物件和值來表示輸出的結果而不僅僅是一個Size。這種增加的靈活性的代價是無法依賴RenderBox的功能。例如,RenderBox實現了一個內在的尺寸調整協議,它允許您在沒有完全鋪設的情況下測量一個子級,以這樣的方式,如果該子級改變了尺寸,父級將再次佈置(考慮到子級的新尺寸)。這是一個微妙的和容易出錯的功能。
編寫RenderBox的大多數方面也適用於編寫RenderObject,因此推薦先閱讀RenderBox的相關討論。主要區別在於佈局和命中測試,因為這些是RenderBox主要專注的方面。

Layout

佈局
佈局協議從約束的子類開始。有關如何編寫Constraints子類的更多資訊,請參閱Constraints中的討論。
performLayout方法應該接受約束並應用它們。佈局演算法的輸出是設定在物件上的欄位,用於描述父物件佈局的物件幾何圖形。例如,使用RenderBox的輸出是RenderBox.size欄位。如果父級指定parentUsesSize為true,則在呼叫子級佈局時,此輸出只能由父級讀取。任何時候渲染物件上的任何變化都會影響該物件的佈局,它應該呼叫markNeedsLayout。
Flutter渲染物件樹的根是一個RenderView。這個物件有單獨的子級,它必須是一個RenderBox。因此,如果你想在渲染樹中有一個自定義的RenderObject子類,你有兩種選擇:你可能需要替換RenderView本身,或者你需要一個RenderBox作為它的子類。 (後者是更常見的情況。)
它會覆蓋performLayout方法來建立一個適合您的類的Constraints物件,並將其傳遞給該子物件的佈局方法。
渲染物件的佈局應該僅取決於其子佈局的輸出,並且只有在佈局呼叫中將parentUsesSize設定為true時才是如此。此外,如果設定為true,且要呈現子物件,則父物件必須呼叫子物件的佈局,否則在子物件更改佈局輸出時不會通知父物件。
可以設定傳輸附加資訊的渲染物件協議。 例如,在RenderBox協議中,您可以查詢您的子級的固有尺寸和基線幾何。 但是,如果這樣做了,那麼當父級在最後一個佈局階段使用它時,每當附加資訊發生變化時,子級都必須在父級上呼叫markNeedsLayout。 有關如何實現此操作的示例,請參閱RenderBox.markNeedsLayout方法。 它覆蓋了RenderObject.markNeedsLayout,以便如果父節點查詢了內部或基準資訊,則每當子節點的幾何結構發生變化時,它都會被標記為dirty。
轉載請註明出處
Email:linguowu0622@gamil.com
複製程式碼



相關文章