Flutter 工作原理
本文件解釋了使 Flutter API 正常工作的 Flutter 工具包內部工作原理。由於 Flutter widget 是以積極組合的形式建立的,所以使用 Flutter 建立的使用者介面含有大量 widget。為了支撐這些負載,Flutter 使用了次線性演算法來佈局和建立 widget,這些資料結構使樹形結構最佳化更加高效,並且具有很多常數因子最佳化。透過一些額外的機制,該設計也允許開發者利用回呼(用於建立使用者可見的 widget)來輕鬆建立無限捲動清單。
積極可組合性
組合性是 Flutter 最為出眾的一個屬性。widget 透過組合其他 widget 的方式進行建立,並且這些 widget 自身由更基礎的 widget 建立。比如,Padding
是一個 widget 而非其他 widget 的屬性。因此,使用 Flutter 建立的使用者介面是由多個 widget 組成的。
widget 遞迴建立的底層是 RenderObjectwidget,它將在渲染樹的底部建立子節點。渲染樹是一種儲存使用者介面幾何訊息的資料結構,該幾何訊息在 佈局 期間計算並在 繪製 及 命中測試 期間使用。大多數 Flutter 開發者無需直接建立這些物件,而是使用 widget 來操縱渲染樹。
為了支援 widget 層的積極可組合性, Flutter 在 widget 和樹渲染層使用了大量的高效演算法和最佳化措施,這些將在下面小節中進行介紹。
次線性佈局
使用大量 widget 及渲染物件並保持高效能的關鍵是使用高效的演算法。其中最重要的是確定渲染物件幾何空間(比如大小和位置)的佈局演算法的效能。其他一些工具包使用 O(N²) 或更糟糕的佈局演算法(例如,約束域中的不動點迭代)。
Flutter 的目標在於佈局初始化的線性效能,及一般情況下更新現有佈局的次線性佈局效能
。通常情況下,佈局所花費的時間應該比對象渲染要多得多。
Flutter 對每一幀執行一次佈局操作,且佈局演算法僅在一次傳遞中完成。 約束訊息透過父節點呼叫每個子節點的佈局方法向下傳遞。子節點遞迴執行自身的佈局操作,並在它們的佈局方法中回傳幾何訊息以便將其新增到渲染樹中。需要注意的是,一旦渲染物件從佈局中回傳,該物件將不會被再次訪問 1,直到下一幀佈局的執行。該策略將可能存在的單獨測量和佈局傳遞合併為單次傳遞,因此,每個渲染物件在佈局過程中最多被訪問兩次 2:一次在樹的向下傳遞過程中,一次在樹的向上傳遞過程中。
針對這個通用協定,Flutter 擁有多種實現。最常用的是 RenderBox
,它以二維的笛卡爾坐標進行運算。在盒子佈局中,約束是最小及最大寬高。在佈局過程中,子節點透過選擇這些邊界內的大小來確定其幾何訊息。子節點在佈局中回傳後,由父節點確定該子節點在父座標系中的位置 3。注意,子節點的佈局並不取決於它的位置,這是因為它的位置直到它從佈局中回傳後才確定。因此父節點可以在無需重新計算子節點佈局的情況下重新定位子節點的位置訊息。
更廣泛地講,在佈局期間,從父節點流向子節點的唯一訊息是約束訊息,從子節點流向父節點的唯一訊息是幾何訊息。透過這些不變量可減少佈局期間所需的工作量:
-
如果父節點對子節點使用與上一次佈局中相同的約束,且子節點沒有將自己的佈局標記為髒,那麼該節點可立即從佈局中回傳,以切斷佈局的向下傳遞。
-
當父節點呼叫子節點的佈局方法時,父節點會表明它是否使用從子節點回傳的大小訊息。如果父節點經常不使用此訊息,即使子節點重新選擇了大小,父節點依舊無需重新計算其佈局,這是因為父節點需要保證新的大小符合現有約束。
-
嚴格約束是指恰好由一個有效幾何滿足的約束。比如,如果最小最大寬度彼此相等,且最小最大高度彼此相等,那麼滿足這些約束的唯一大小便是具有該寬度及高度的大小。如果父節點提供了嚴格約束,即便父節點在佈局中使用了子節點的大小,在子節點重新計算佈局時,父節點的佈局也無需重新計算,這是因為子節點在沒有父節點新約束的情況下無法更改其大小。
-
渲染物件可以宣告僅使用父節點提供的約束來確定其幾何訊息。此類宣告通知框架: 即便約束為非嚴格約束,以及父節點的佈局取決於子節點的大小, 該渲染物件父節點的佈局在子節點的佈局重新計算時仍無需重新計算,這是因為子節點在沒有父節點新約束的情況下無法更改其大小。
這些最佳化措施的效果是,當渲染物件包含髒節點時,在佈局過程中,只有這些節點以及它們周圍子樹的有限節點才允許被訪問。
次線性 widget 建立
Flutter 使用類似於佈局的次線性演算法來建立 widget。widget 建立完成後,它們將被保留了使用者頁面邏輯結構的 element 樹 儲存。 Element 樹是必要的,因為 widget 自身是不可變的,這意味著(其他情況除外)它們無法記住與其他 widget 的父或子關係。 Element 還儲存了與 Stateful widget 相關聯的 state 物件。
由於使用者輸入(或來自其他地方的回應),比如開發者在關聯的 state 物件上呼叫了 setState()
方法,element 可能會變髒。框架維護了一個髒 element 清單,使得 建立 過程可跳過乾淨的 element,直接跳轉到髒的 element。建立過程中,訊息在 element 樹中向下 單向 傳遞,這意味著該階段中每個 element 最多會被訪問一次。一個 element 一旦被清洗,它將不會再次變髒,這是因為透過歸納,它所有的祖先 element 也都是乾淨的 4。
由於 widget 是不可變的,因此父節點使用相同的 widget 來重新建立 element,如果 element 沒有將自己標記為髒,那麼該 element 可立即從建立中回傳,以切斷建立的向下傳遞。另外,element 只需比較兩個 widget 所引用的物件標識來確定新 widget 與舊 widget 是否相同。開發者可利用該最佳化實現投影模式,即 widget 包含了被儲存為成員變數、在建立過程中預先建立的子 widget
建立過程中,Flutter 同時使用 Inheritedwidgets
來避免父鏈的遍歷。如果 widget 經常遍歷它們的父鏈,比如確定當前的主題顏色,那麼建立階段樹的深底將變為 O(N²),由於 Flutter 的積極可組合性,其數量可能非常巨大。為了避免這些父鏈的遍歷,框架透過在每個 element 上維護一個 Inheritedwidget
雜湊表來向下傳遞
element 樹中的訊息。通常情況下,多個 element 引用相同的雜湊表,並且該表僅在 element 引入新的 Inheritedwidget
時改變。
線性協調
不同於傳統做法,Flutter 沒有使用樹差異比較演算法。相反,框架透過使用 O(N) 演算法獨立地檢查每個 element 的子節點來決定是否重用該 element。子清單協調演算法針對以下情況進行了最佳化:
-
舊的子清單為空。
-
兩個清單完全相同。
-
在清單的某個位置插入或刪除一個或多個 widget。
-
如果新舊清單都包含相同 key 5 的 widget,那麼這兩個 widget 就會被認為是相同的。
通常的做法是從新舊子清單的頭部和尾部開始對每一個 widget 的執行時型別和 key 進行對應,這樣就可能找到在兩個清單中間所有不對應子節點的(非空)範圍。然後框架將舊子清單中該範圍內的子項根據它的 key 放入一個雜湊表中。接下來,框架將會遍歷新的子清單以尋找該範圍內能夠對應雜湊表中的 key的子項。無法對應的子項將會被丟棄並從頭開始重建,對應到的子項則使用它們新的 widget 進行重建。
樹結構最佳化
重用 element 對效能非常重要,這是因為 element 擁有兩份關鍵資料:Stateful widget 的狀態物件及底層的渲染物件。當框架能夠重用 element 時,使用者介面的邏輯狀態訊息是不變的,並且可以重用之前計算的佈局訊息,這通常可以避免遍歷整棵子樹。事實上,重用 element 是非常有價值的,因為 Flutter 支援 全域 樹更新,以此保留狀態和佈局訊息。
開發者可透過將 GlobalKey
與其中一個 widget 相關聯來實施全域樹更新。每個全域 key 在整個應用中都是唯一的,並使用特定於執行緒的雜湊表進行註冊。在建立過程中,開發者可以使用全域 key 將 widget 移動到 element 樹的任意位置。框架將不會在該位置上重新建立 element,而是檢查雜湊表並將現有的 element 從之前的位置移動到新的位置,從而保留整棵子樹。
重新建立的子樹中的渲染物件能夠保留它們的佈局訊息,這是因為佈局約束是渲染樹從父節點傳遞到子節點的唯一訊息。子清單發生變化後,父節點將會被標記為髒,但如果新的父節點傳遞給子節點的佈局約束與該子節點從舊的父節點接收到的相同,那麼子節點可立即從佈局中回傳,從而切斷佈局的向下傳遞。
開發者廣泛使用全域 key 和全域樹更新來實現 hero transition 及導向等效果。
恆定因子最佳化
除了上述演算法最佳化,實現積極可組合還需依賴幾個重要的恆定因子最佳化。這些最佳化對於上面所討論的主要演算法是非常重要的。
-
子模型無關。與大多數使用子清單的工具包不同, Flutter 渲染樹不會記住一個特定的子模型。比如,類
RenderBox
存在一個抽象的visitChildren()
方法,而非具體的 firstChild 和 nextSibling 介面。許多子類別僅支援直接作為其成員變數的單個子項,而非子項清單。比如,由於RenderPadding
僅支援單個子節點,因此它擁有一個更為簡單、高效的佈局方法。 -
視覺渲染樹、widget 邏輯樹。在 Flutter 中,渲染樹在與裝置無關的視覺座標系中執行,這意味著即使 x 軸的讀取方向是從右到左,其左側的值依舊小於右側。Widget 樹通常在邏輯坐標中執行,這意味著擁有 開始 和 結束 值的視覺解釋取決於讀取方向。邏輯坐標到視覺坐標的轉換是在 widget 樹和渲染樹之間的切換中完成的。這種方法更為高效的原因是,渲染樹中的佈局和繪製計算比 widget 到渲染樹的切換更加頻繁,並且可以避免重複的坐標轉換。
-
透過專門的渲染物件處理文字。大多數渲染物件都不清楚文字的複雜性。相反,文字是由專門的渲染物件
RenderParagraph
進行處理,它是渲染樹中的一個葉子節點。開發者使用組合形式將文字併入到使用者介面中,而非使用文字感知渲染物件進行子類別化。該模式意味著RenderParagraph
可避免文字佈局在父節點提供相同佈局約束下的重複計算,這是非常常見的,即使在樹最佳化期間也是如此。 -
可觀察物件。 Flutter 使用模型觀察及回應設計模式。顯而易見,回應模式占主導地位,但 Flutter 在某些葉子節點的資料結構上使用了可觀察物件。比如
Animation
會在值發生變化時通知觀察者清單。 Flutter 將這些可觀察物件從 widget 樹轉移到渲染樹中,渲染樹直接監聽這些物件,並在它們改變時僅重繪通道的相關階段。比如,更改Animation<Color>
可能只觸發繪製階段,而非整個建立和繪製階段。
總的來說,這些最佳化對透過積極組合方式產生的大型樹結構的效能產生了重大影響。
Element 樹和 RenderObject 樹的分離
Flutter 中 RenderObject
樹和 Element
(Widget) 樹是同構的(嚴格來說,RenderObject
樹是 Element
樹的子集)。顯然,可以簡化這些樹合併成一棵樹。但在 Flutter 中我們並沒有這樣做,因為在實踐中,將這些樹分開有許多好處:
-
效能。 當佈局改變時,只需要遍歷佈局相關的樹。由於組合 widget 的原因,Element 樹通常具有許多必須跳過的額外節點。
-
清晰。 更清晰的分離,使得 Widget 協定和 RenderObject 協定能夠各自專注特定的需求,簡化了 API,從而降低 bug 風險和測試負擔。
-
型別安全。 渲染 RenderObject 樹會更安全,因為它可以在執行時保證子節點有合適的型別(例如,每個座標系都有自己的 RenderObject 型別)。組合 widget 可以不考慮佈局時使用的座標系(例如,同一個 widget 可以分別在盒子佈局和 sliver 佈局中使用),因此在 Element 樹中,驗證 RenderObject 的型別需要將樹遍歷。
無限捲動
對於工具包來說,實現無限捲動清單是非常困難的。Flutter
支援基於 構造器 模式實現的簡單無限捲動清單介面,其中 ListView
使用回呼按需建立 widget,即它們只在捲動過程中才對使用者可見。該功能需要 視窗感知佈局 及 按需建立 widget 的支援。
視窗感知佈局
同 Flutter 中的大多數東西一樣,可捲動的 widget 是基於組合模式建立的。可捲動 widget 的外部是一個 Viewport
,這是一個擁有更大內部空間的盒子,這意味著它的子節點可以超出視窗口的邊界並捲動到可視區域中。但是,視窗口沒有 RenderBox
子節點,而是擁有被稱為 sliver,實現了視窗感知協定的RenderSliver
子節點。
sliver 佈局協定中父節點向下傳遞給子節點的約束訊息及接收到的幾何訊息的結構與盒子佈局相同。但約束和幾何資料在兩個協定之間不同。在 sliver 協定中,子節點接收到的是關於視窗口的訊息,這其中包含剩餘的可見空間量。它們回傳的幾何資料支援各種捲動連結效果,包括可摺疊標題及視差。
不同的 sliver 以不同的方式填充視窗口中的可用空間。比如,生成線性子清單的 sliver 按順序排列每個子節點,直到 sliver 中無任何子節點或可用空間。同理,生成二維子節點網格的 sliver 僅填充網格中的可見區域。由於它們知道還有多大的可見空間,sliver 可以生成有限的子節點,即使它們可能生成無限的子節點。
可組合 sliver 來建立特定的捲動佈局和效果。比如,單個視窗口可以有一個摺疊標題、一個線性清單和一個網格。所有這些 sliver 將按照 sliver 佈局協定進行協作,只生成那些在視窗口實際可見的子節點,而不管這些子節點是否屬於標題、清單或網格6。
按需建立 widget
如果 Flutter 擁有一個嚴格的從建立到佈局,再到繪製的通道,那麼前面的內容將不足以實現無限捲動清單,這是因為只有在佈局階段才能透過視窗口獲取可用的空間訊息。如果沒有額外的機制,在佈局階段建立用於填充空間的 widget 已經太遲了。 Flutter 使用將通道的建立與佈局交叉在一起的方式來解決這個問題。在佈局階段的任意時刻,只要這些 widget 是當前佈局的渲染物件的子節點,框架就可以按需建立新的 widget。
只有嚴格控制建立及佈局中訊息傳播的演算法,才能實現建立和佈局的交叉執行。也就是說,在建立過程中,訊息只能沿建立樹向下傳遞。當渲染物件進行佈局時,佈局遍歷過程中並沒有訪問該渲染物件的子樹,這意味透過子樹建立的寫入無法使到目前為止已進入佈局計算過程的任何訊息失效。無獨有偶,一旦佈局從渲染物件中回傳,在當前佈局過程中,該渲染物件將永遠不會被再次訪問,這意味後續佈局計算生成的任何寫入都不會使用於建立渲染物件的子樹的訊息失效。
此外,線性協調及樹結構最佳化對於在捲動過程中有效更新 element,以及當 element 在視窗口邊緣捲動進出檢視期間修改渲染樹至關重要。
人機工程 API
速度只有在框架能夠被有效使用時才有意義。為了引導設計更高可用性的 Flutter API, Flutter 已經在與開發者進行的廣泛使用者體驗研究中進行了反覆測試。這些研究有時證實了已有的設計決策,有時有助於引導功能的優先級,有時會改變 API 的設計方向。比如,Flutter 的 API 文件很多,使用者體驗的研究不僅證實了這些文件的價值,也同時強調了範例程式碼及說明性圖表的重要性。
本節將要討論 Flutter API 設計中為提高可用性所做的一些決策。
與開發者思維模式相對應的專項 API
Flutter 中 widget
、Element
和 RenderObject
的基底類別節點不定義子類別模型。該機制允許每個節點對適用於該節點的子模型進行定製化。
大多數 widget
物件都有一個子 widget
物件,因此它只暴露了一個 child
引數。一些 widget 支援任意數量的子節點,並暴露了一個獲取子節點清單的 children
引數。有些 widget 無任何子節點、不保留記憶體且無任何引數。同樣的,RenderObjects
暴露特定於子模型的 API。
RenderImage
是一個沒有子節點的葉子節點。
RenderPadding
只持有一個子節點,因此它有一個指向單個子節點的指標儲存空間。
RenderFlex
接受任意數量的子節點,並透過連結串列對其進行管理。
在一些罕見情況下,將使用更復雜的子類別模型。渲染物件 RenderTable
的建構式函式需要使用二維陣列來儲存子節點,所以該類別暴露了用於控制行和列數量的 getter 及 setter 方法,還有一些可以用 x、y 軸坐標來替換單個子節點的特殊方法,可透過提供一個新的子節點陣列來新增新行,並用單個陣列及列的個數來替換整個子節點清單。該物件並不像大多數渲染物件那樣使用連結串列,而是使用可索引陣列來實現。
Chip
widget 和 InputDecoration
物件具有與其控制中的插槽相對應的欄位。如果一個通用子模型將強制語義定義在子清單之上,比如將第一個子節點定義為字首,第二個子節點定義為字尾,那麼專用子模型允許使用特有的命名屬性。
這種靈活性允許樹中的每個子節點以其最常用的方式操作它的角色。很少有人想要在表格中插入一個單元格,從而導致其他所有單元格被環繞;同樣的,很少有人想要透過索引而不是透過引用從 flex 行中刪除子項。
RenderParagraph
物件是最極端的情況:它有一個完全不同型別的子節點,TextSpan
。在 RenderParagraph
的邊界,RenderObject
樹會被轉換為 TextSpan
樹。
專門用於滿足開發者期望的 API 的一切方法不僅適用於子模型。
專門存在一些瑣碎的 widget,以便開發者在尋找問題解決方案時能夠發現並使用它們。一旦知道如何使用 Expanded
和大小為零的 SizedBox
子部件,就可以輕鬆地為行或列新增空格,但你會發現這種模式是沒有必要的,因為搜尋 space
所找到的 Spacer
,它是直接使用 Expanded
和 SizedBox
來達到同樣的效果的。
同理,可以透過在建立過程中不包含 widget 子樹來輕鬆隱藏 widget 子樹。但開發者通常希望有一個 widget 來執行該操作,因此 Visibility
的存在便是將此模式封裝在一個簡單的可重用 widget 中。
明確的引數
UI 框架往往擁有大量的屬性,因此很少有開發者能夠記住每個類別的每個建構式函式引數的作用。由於 Flutter 使用回應式程式設計正規化,因此在 Flutter 中,建立方法通常會對建構式函式進行多次呼叫。透過利用 Dart 的命名引數,Flutter 中的 API 能夠使這些建立方法保持清晰易懂。
該模式已被擴充套件到任何具有多個引數(尤其是具有 boolean 型別引數)的方法,因此獨立的 true
或 false
值在方法呼叫中總是自我描述的。此外,為避免 API 中通常由雙重否定所造成的困惑,
boolean 型別的引數和屬性始終以肯定的形式命名(比如,使用 enabled: true
而非 disabled: false
)。
引數陷阱
在 Flutter 框架中被大量使用的一項技術是定義不存在錯誤條件的 API。這樣可以避免考慮整個錯誤類別。
比如內嵌函式允許內嵌的一端或兩端為空,而不是將其定義為錯誤:兩個空值之間的內嵌永遠為空,並且從空值或空值內嵌等效於對指定型別進行零模擬內嵌。這意味著不小心將 null 傳遞給內嵌函式的開發者不會遇到錯誤,而是會得到一個合理結果。
一個更加微妙的例子是 Flex
佈局演算法。該佈局給予 flex 渲染物件的空間被它的子節點所劃分。因此 flex 的大小應該是整個可用空間。在最初的設計中提供無限空間將導致失敗:這意味著 flex 應該是無限大且無用的佈局設定。然而,透過對 API 的改造,在為 flex 物件提供無限空間時,渲染物件會調整自身大小來滿足所需子節點的大小,從而減少可能出現的錯誤次數。
該方法也可用於避免使用允許建立不符合邏輯的資料的建構式函式。例如,PointerDownEvent
的建構式函式不允許將 PointerEvent
的 down
屬性設定為
false
(這種情況是自相矛盾的);相反,建構式函式沒有關於欄位 down
的引數,且將值始終設定為 true
。
一般情況下,該方法用於為輸入域中的所有值定義有效的解釋。最簡單的例子是 Color
的建構式函式。相對於接受四個整型引數(分別用於表示紅色、綠色、藍色和 alpha),其中任何一個都可能超出範圍,它的預設建構式函式僅接受一個整數值,並定義每位的含義(例如,低八位代表紅色),以便任何輸入都是有效的顏色值。
一個更復雜的例子是 paintImage()
函式。該函式需要 11 個引數,其中一些具有相當寬泛的輸入域,但它們都經過精心設計且大部分都能夠彼此相交,因此很少出現無效組合。
積極報告錯誤
並非所有的錯誤都能被設計出來。對於那些遺漏的錯誤,在 debug 版本中,Flutter 通常會嘗試儘早捕獲並立即報告。它使用了大量的斷言,對建構式函式引數進行了詳細的完整性檢查,並監視其生命週期,一旦檢測到不一致,它們會立即引發異常。
這在某些情況下是極端情況:比如,在執行單元測試時,無論測試用例正在做什麼,每個 RenderBox
子類別都會主動地檢查其內部大小調整方法是否滿足內部大小調整契約。這有助於捕獲可能無法執行的 API 錯誤。
當異常丟擲時,它們會包含儘可能多的訊息。 Flutter 中的一些錯誤會主動探測相關的堆疊跟蹤訊息,以確定實際錯誤最可能發生的位置。其他錯誤則透過相關樹來確定壞資料的來源。最常見的錯誤包含詳細說明(在某些情況下會包含避免錯誤的範例程式碼),或指向其他文件的連結。
回應式
可變的基於樹結構的 API 受二元訪問模式的影響:建立樹的原始狀態通常使用與後續更新完全不同的操作集。Flutter 的渲染層使用了這種正規化,因為它是維護持久樹的有效方法,是高效佈局和繪製的關鍵所在。但這也意味著,與渲染層的直接互動是十分笨拙的,甚至極其容易出錯。
Flutter 在 widget 層引入了一個使用回應式來操作底層渲染樹的組合機制7。該 API 透過將樹的建立和更新步驟整合到一個單一的樹結構描述(建立)中,從而將樹操作抽象出來,這包括:每次系統狀態更新之後,開發者用於描述使用者介面的新設定;框架對於新設定所需要進行的一系列樹更新計算。
內嵌
由於 Flutter 鼓勵開發者描述與當前應用狀態相對應的介面設定,因此存在一種在這些設定之間執行隱式的動畫機制。
例如,假設介面在狀態 S1 由一個圓形組成,在狀態 S2 時由一個正方形組成。如果沒有動畫機制,狀態更改將導致不和諧的介面更改。隱式動畫則允許介面在幾個幀的時間裡由圓形平滑地轉場到正方形。
每個可執行隱式動畫的屬性都包含一個 Stateful widget,它用於記錄輸入的當前值,並在輸入值改變時開始執行動畫序列,並在指定的持續時間內從當前值轉換為新值。
這是使用不可變物件的 lerp
(線性內嵌)函式來實現的。每個狀態(這裡為圓形和正方形)代表一個設定中包含恰當設定(比如顏色、筆劃寬度等)且知道如何繪製自己的不可變物件。在動畫繪製中間步驟時,開始和結束值連同表示動畫中點的 t 值一併傳遞給 lerp
函式。其中 0.0 代表開始 start
,1.0 代表結束 end
8,並且該方法回傳表示中間階段的第三個不可變物件。
對於從圓形到正方形的轉換,lerp
函式將回傳一個圓角正方形物件,其半徑被描述為從 t 值匯出的分數,使用 lerp
函式進行內嵌計算的顏色,以及使用 lerp
函式進行雙倍內嵌計算的筆劃寬度。該物件與圓形、正方形一樣具有相同的介面實現,並且可以在請求時進行自我繪製。
該技術允許狀態機、狀態到設定的對映、動畫和內嵌機制以及與如何繪製每一楨完全分離的特定邏輯。
在 Flutter 中,該機制得到了廣泛應用,無論是像 Color
和 Shape
這樣的基本型別,還是像 Decoration
,TextStyle
或 Theme
這樣更為複雜的型別,都是可以進行內嵌處理的。它們通常是由可插入元件構成的,並且插入更復雜的物件通常就像遞迴插入描述複雜物件的所有值一樣簡單。
一些內嵌物件由類層次結構定義。比如,形狀由 ShapeBorder
介面表示,並且存在多種形狀型別,包括:
BeveledRectangleBorder
、BoxBorder
、CircleBorder
、RoundedRectangleBorder
和 StadiumBorder
。單一的 lerp
函式並不能瞭解所有可能的型別訊息,因此介面定義了 lerpFrom
和 lerpTo
方法以替代靜態的 lerp
方法。當被告知從形狀 A 切換到 B 時,將首選詢問 B 是否 lerpFrom
A,如其答案為否,則詢問 A 是否可以 lerpTo
B
(如兩者的答案均為否,如果 t
的值小於 0.5 則回傳 A,否則回傳 B)。
這允許類層次結構的任意擴充套件,後續新增的能夠在先前已知值與它們之間進行內嵌處理。
在某些情況下,內嵌本身不能被任何可用的類描述,並且定義一個私有類來描述中間狀態。比如在 CircleBorder
和 RoundedRectangleBorder
之間進行內嵌時就是如此。
該機制的另外一個優點是:它可以處理從中間態到新值的內嵌。比如,在圓形到正方形轉場的中途,形狀可能再次改變,導致動畫需要內嵌到一個三角形。只要該三角形類是 lerpFrom
圓形到正方形的中間類,就可以無縫進行轉換。
結論
Flutter 一切都是 widget 的口號是圍繞著透過組合 widget 來建立使用者介面, widget 又由更為基礎的 widget 構成。這種積極組合的結果是需要精心設計的演算法和數據結構才能有效處理大量的 widget。透過一些額外的機制,這些資料結構還能使開發者輕鬆建立無限捲動清單,以便在 widget 可見時進行按需建立。
腳注:
1 至少對於佈局來說。它可能會重新審視繪製、在必要時建立輔助功能樹、以及必要時的命中測試。
2 現實情況當然更復雜一些。有些佈局涉及內部維度及基線測量,這涉及到相關子樹的額外遍歷(在最壞的情況下,使用積極快取來降低潛在的二次效能)。但是,這些情況非常罕見。特別是在常見的 shrink-wrapping 情況下,根本不需要內部尺寸。
3 嚴格來說,子節點的位置不是其 RenderBox 幾何體的一部分,因此無需在佈局期間進行實際計算。許多渲染物件隱式地將它們的單個子節點相對於它們自身的原點定位在 0,0 處,這根本不需要進行計算或儲存。一些渲染物件避免計算它們子節點的位置直到最後可能需要的時刻(比如,在繪製過程中),以避免以後沒有被繪製時的計算。
4 該規則有一個例外。正如 按需建立 widget 中所描述的,由於佈局約束的變化,一些 widget 可以被重建。如果 widget 在同一幀中因與此無關的原因被標記為髒,同時也由於它受佈局約束的影響,該 widget 將會被建立兩次。該次冗餘建立僅限於 widget 自身,並不會影響其後代節點。
5 鍵是一個可選的與 widget 相關聯的不透明物件,它的相等運算子用於影響協調演算法。
6 對於可訪問性,並在 widget 建立及在視窗顯示的過程中為應用提供幾毫米的時間,視窗口會在可見 widget 的前後為幾百個畫素建立(但不進行繪製)widget。
7 該方法首次在 Facebook 的 React 框架中得到了廣泛使用。
8 實際上,允許 t 值超過 0.0-1.0 的範圍,這同樣適用於某些曲線。比如 elastic 緩動曲線透過短暫的過沖來表示彈跳效應。內嵌邏輯通常可以在適當情況下推算出起始或結束點。對於某些型別,比如在插入顏色時,t 值被有效地固定到 0.0-1.0 的範圍。