本文件描述了 Flutter 工具包的內部工作原理,這些原理使得 Flutter 的 API 成為可能。由於 Flutter Widget 是透過強大的組合方式構建的,因此使用 Flutter 構建的使用者介面擁有大量的 Widget。為了支援這種工作負載,Flutter 在佈局和 Widget 構建方面使用了亞線性演算法,以及能夠進行高效樹操作幷包含大量常數因子最佳化的資料結構。透過一些附加細節,這種設計也使得開發者能夠輕鬆地使用回撥函式建立無限滾動列表,這些回撥函式會精確地構建使用者可見的 Widget。

強大的可組合性

#

Flutter 最顯著的方面之一是其強大的可組合性。Widget 是透過組合其他 Widget 來構建的,而這些 Widget 本身又由越來越基礎的 Widget 組成。例如,Padding 是一個 Widget,而不是其他 Widget 的屬性。因此,使用 Flutter 構建的使用者介面由大量的 Widget 組成。

Widget 構建的遞迴最終會觸及 RenderObjectWidgets,這些 Widget 在底層渲染樹中建立節點。渲染樹是一種資料結構,它儲存使用者介面的幾何資訊,這些資訊在佈局期間計算,並在繪製命中測試期間使用。大多數 Flutter 開發者不會直接編寫渲染物件,而是透過 Widget 來操作渲染樹。

為了在 Widget 層實現強大的可組合性,Flutter 在 Widget 和渲染樹層都採用了許多高效的演算法和最佳化,這些內容將在接下來的小節中進行描述。

亞線性佈局

#

擁有大量 Widget 和渲染物件,效能的關鍵在於高效的演算法。至關重要的是佈局演算法的效能,佈局演算法決定了渲染物件的幾何形狀(例如,大小和位置)。一些其他工具包使用 O(N²) 或更差的佈局演算法(例如,某些約束域中的不動點迭代)。Flutter 的目標是實現初始佈局的線性效能,以及在後續更新現有佈局的常見情況下的亞線性佈局效能。通常,在佈局中花費的時間應該比渲染物件的數量增長得更慢。

Flutter 每幀執行一次佈局,並且佈局演算法採用單次遍歷。約束由父物件呼叫子物件的佈局方法向下傳遞到樹中。子物件遞迴地執行自己的佈局,然後透過從它們的佈局方法返回來將幾何資訊返回到樹中。重要的是,一旦渲染物件從其佈局方法返回,該渲染物件將不會在下一個幀的佈局之前再次被訪問[1]。這種方法將可能單獨進行的測量和佈局傳遞合併為一次遍歷,結果是,每個渲染物件在佈局期間最多被訪問兩次[2]:一次在向下遍歷樹時,一次在向上遍歷樹時。

Flutter 針對此通用協議有幾種專門化。最常見的專門化是 RenderBox,它在二維笛卡爾座標系中工作。在盒狀佈局中,約束是最小和最大寬度以及最小和最大高度。在佈局期間,子物件透過選擇這些邊界內的尺寸來確定其幾何形狀。在子物件從佈局返回後,父物件在父物件的座標系中決定子物件的位置[3]。請注意,子物件的佈局不能依賴於其位置,因為位置在子物件從佈局返回後才確定。因此,父物件可以自由地重新定位子物件,而無需重新計算其佈局。

更普遍地說,在佈局期間,唯一從父物件流向子物件的資訊是約束,而唯一從子物件流向父物件的資訊是幾何資訊。這些不變數可以減少佈局所需的工作量。

  • 如果子物件沒有將自己的佈局標記為髒,那麼只要父物件向子物件提供與子物件在上一個佈局中收到的相同的約束,子物件就可以立即從佈局返回,從而截斷遍歷。

  • 每當父物件呼叫子物件的佈局方法時,父物件會指示它是否使用從子物件返回的尺寸資訊。如果,如經常發生的那樣,父物件不使用尺寸資訊,那麼即使子物件選擇了一個新的尺寸,父物件也不需要重新計算其佈局,因為父物件保證新的尺寸將符合現有約束。

  • 嚴格約束是隻能透過一個有效幾何形狀來滿足的約束。例如,如果最小和最大寬度相等,並且最小和最大高度相等,那麼滿足這些約束的唯一尺寸是具有該寬度和高度的尺寸。如果父物件提供了嚴格約束,那麼即使父物件在其佈局中使用子物件的尺寸,當子物件重新計算其佈局時,父物件也不需要重新計算其佈局,因為子物件在沒有父物件的新約束的情況下無法改變尺寸。

  • 渲染物件可以宣告它僅使用父物件提供的約束來確定其幾何形狀。 such a declaration informs the framework that the parent of that render object does not need to recompute its layout when the child recomputes its layout *even if the constraints are not tight* and *even if the parent's layout depends on the child's size*, because the child cannot change size without new constraints from its parent. (這種宣告會告知框架,該渲染物件的父物件在子物件重新計算佈局時不需要重新計算其佈局,即使約束不嚴格並且即使父物件的佈局依賴於子物件的尺寸,因為子物件在沒有父物件的新約束的情況下無法改變尺寸。)

由於這些最佳化,當渲染物件樹包含髒節點時,佈局期間只會訪問這些節點以及它們周圍有限的子樹部分。

亞線性 Widget 構建

#

與佈局演算法類似,Flutter 的 Widget 構建演算法也是亞線性的。構建完成後,Widget 由Element 樹持有,Element 樹保留了使用者介面的邏輯結構。Element 樹是必需的,因為 Widget 本身是不可變的,這意味著(除其他外)它們無法記住它們與其他 Widget 的父子關係。Element 樹還儲存了與狀態 Widget 關聯的狀態物件。

作為對使用者輸入(或其他刺激)的響應,Element 可以變髒,例如,如果開發者在關聯的狀態物件上呼叫了 setState()。框架維護一個髒 Element 列表,並在構建階段直接跳轉到它們,跳過乾淨的 Element。在構建階段,資訊是單向地向下傳播到 Element 樹,這意味著每個 Element 在構建階段最多被訪問一次。一旦被清理,Element 就不會再次變髒,因為根據歸納法,它所有的祖先 Element 也是乾淨的[4]

由於 Widget 是不可變的,如果 Element 沒有將自己標記為髒,並且父物件用一個相同的 Widget 重建了該 Element,那麼該 Element 可以立即從構建中返回,從而截斷遍歷。此外,Element 只需要比較兩個 Widget 引用物件的身份就可以確定新 Widget 與舊 Widget 相同。開發者利用這種最佳化來實現重投影模式,其中 Widget 在其構建中包含一個儲存在其成員變數中的預構建的子 Widget。

在構建過程中,Flutter 還透過 InheritedWidgets 避免了遍歷父鏈。如果 Widget 經常遍歷其父鏈,例如為了確定當前主題顏色,那麼構建階段將變成樹深度的 O(N²) 複雜度,由於強大的組合,樹深度可能非常大。為了避免這些父鏈遍歷,框架透過在每個 Element 維護一個 InheritedWidget 的雜湊表來將資訊向下推送到 Element 樹。通常,許多 Element 會引用同一個雜湊表,該雜湊表僅在引入新 InheritedWidget 的 Element 處發生變化。

線性協調

#

與流行的看法相反,Flutter 並不採用樹差異演算法。相反,框架透過使用 O(N) 演算法獨立檢查每個 Element 的子列表來決定是否重用 Element。

  • 舊的子列表為空。
  • 兩個列表完全相同。
  • 在列表中的一個或多個位置上只發生了一次插入或刪除。
  • 如果每個列表包含一個具有相同 key 的 Widget[5],則這兩個 Widget 會匹配。

通用方法是透過比較每個 Widget 的執行時型別和 key 來匹配兩個子列表的開頭和結尾,可能在每個列表的中間找到一個非空範圍,其中包含所有未匹配的子項。然後,框架將該範圍內的子項(來自舊子列表)放入一個基於其 key 的雜湊表中。接下來,框架遍歷新子列表中的範圍,並透過 key 查詢雜湊表進行匹配。未匹配的子項將被丟棄並從頭開始重建,而匹配的子項將用它們新的 Widget 進行重建。

樹操作

#

重用 Element 對效能很重要,因為 Element 擁有兩個關鍵資料:狀態 Widget 的狀態和底層的渲染物件。當框架能夠重用 Element 時,使用者介面的該邏輯部分的 State 就會得到保留,並且先前計算的佈局資訊可以被重用,通常可以避免對整個子樹的遍歷。事實上,重用 Element 非常有價值,以至於 Flutter 支援非區域性樹突變,這些突變會保留狀態和佈局資訊。

開發者可以透過將 GlobalKey 與其中一個 Widget 相關聯來執行非區域性樹突變。每個全域性 key 在整個應用程式中都是唯一的,並且被註冊到執行緒特定的雜湊表中。在構建階段,開發者可以將帶有全域性 key 的 Widget 移動到 Element 樹中的任意位置。框架不會在該位置構建一個新的 Element,而是會檢查雜湊表,並將現有的 Element 從其先前的 Nutanix 位置重新 parent 到新的位置,從而保留整個子樹。

重新 parent 的子樹中的渲染物件能夠保留其佈局資訊,因為佈局約束是唯一從渲染樹中的父物件流向子物件的資訊。新的父物件被標記為佈局髒,因為它的子列表已更改,但是如果新父物件向子物件傳遞了與子物件從舊父物件接收到的相同的佈局約束,子物件就可以立即從佈局中返回,從而截斷遍歷。

全域性 key 和非區域性樹突變被開發者廣泛用於實現英雄過渡和導航等效果。

常數因子最佳化

#

除了這些演算法最佳化之外,實現強大的可組合性還依賴於幾個重要的常數因子最佳化。這些最佳化在上述主要演算法的葉節點處最為重要。

  • 子模型無關。 與大多數使用子列表的工具包不同,Flutter 的渲染樹不拘泥於特定的子模型。例如,RenderBox 類有一個抽象的 visitChildren() 方法,而不是一個具體的 firstChildnextSibling 介面。許多子類只支援單個子物件,直接作為成員變數儲存,而不是子列表。例如,RenderPadding 只支援單個子物件,因此它有一個更簡單的佈局方法,執行時間更短。

  • 視覺渲染樹,邏輯 Widget 樹。 在 Flutter 中,渲染樹在裝置無關的視覺座標系中執行,這意味著 x 座標值越小越靠左,即使當前的閱讀方向是從右到左。Widget 樹通常在邏輯座標中執行,即使用起始結束值,其視覺解釋取決於閱讀方向。從邏輯座標到視覺座標的轉換髮生在 Widget 樹和渲染樹之間的交接處。這種方法更有效,因為渲染樹中的佈局和繪製計算比 Widget 到渲染樹的交接更頻繁,並且可以避免重複的座標轉換。

  • 文字由專用渲染物件處理。 絕大多數渲染物件都不瞭解文字的複雜性。相反,文字由一個專門的渲染物件 RenderParagraph 來處理,它在渲染樹中是一個葉節點。開發者不是透過繼承文字感知的渲染物件,而是透過組合來將文字整合到其使用者介面中。這種模式意味著 RenderParagraph 可以在其父物件提供相同的佈局約束(這很常見,即使在樹操作期間)的情況下,避免重新計算其文字佈局。

  • 可觀察物件。 Flutter 同時使用模型觀察和響應式正規化。顯而易見,響應式正規化占主導地位,但 Flutter 對某些葉資料結構使用可觀察模型物件。例如,Animations 在值更改時會通知觀察者列表。Flutter 將這些可觀察物件從 Widget 樹傳遞到渲染樹,渲染樹直接觀察它們,並在它們更改時僅使管道的適當階段失效。例如,Animation<Color> 的更改可能只會觸發繪製階段,而不是構建和繪製階段。

總而言之,這些針對由強大組合而建立的大型樹進行累加的最佳化,對效能產生了顯著影響。

Element 和 RenderObject 樹分離

#

Flutter 中的 RenderObjectElement (Widget) 樹是同構的(嚴格來說,RenderObject 樹是 Element 樹的一個子集)。一個明顯的簡化是將這些樹合併成一棵樹。然而,在實踐中,將這些樹分開有許多好處

  • 效能。 當佈局發生變化時,只需要遍歷佈局樹的相關部分。由於組合,Element 樹通常有許多額外的節點需要跳過。

  • 清晰性。 職責的清晰分離使得 Widget 協議和渲染物件協議可以針對各自的特定需求進行專門化,從而簡化了 API 表面,從而降低了錯誤風險和測試負擔。

  • 型別安全。 渲染物件樹可以更安全,因為它可以在執行時保證子物件是合適的型別(例如,每個座標系都有自己的渲染物件型別)。組合 Widget 可以與佈局中使用的座標系無關(例如,暴露應用程式模型一部分的同一個 Widget 可以用於盒狀佈局和條狀佈局),因此在 Element 樹中,驗證渲染物件的型別需要樹遍歷。

無限滾動

#

無限滾動列表對於工具包來說一向是困難的。Flutter 使用基於構建器模式的簡單介面支援無限滾動列表,其中 ListView 使用回撥來按需構建 Widget,這些 Widget 在使用者滾動時變得可見。支援此功能需要視口感知佈局按需構建 Widget

視口感知佈局

#

與 Flutter 中的大多數事物一樣,可滾動 Widget 也是透過組合構建的。可滾動 Widget 的外部是 Viewport,它是一個“內部更大”的框,意味著它的子物件可以超出視口的邊界,並且可以滾動到檢視中。然而,視口沒有 RenderBox 子物件,而是擁有 RenderSliver 子物件,稱為條狀,它們具有視口感知的佈局協議。

條狀佈局協議與盒狀佈局協議的結構相匹配,即父物件將約束向下傳遞給子物件,並接收幾何資訊作為回報。然而,約束和幾何資料在兩個協議之間有所不同。在條狀協議中,子物件會獲得關於視口的資訊,包括剩餘可見空間量。它們返回的幾何資料能夠實現各種滾動連結效果,包括可摺疊的標題和視差。

不同的條狀以不同的方式填充視口中可用的空間。例如,一個生成子物件線性列表的條狀會按順序佈局每個子物件,直到條狀沒有更多子物件或空間用完。同樣,一個生成二維網格子物件的條狀只填充其可見的網格部分。由於它們瞭解可見空間量,因此條狀可以生成有限數量的子物件,即使它們有可能生成無限數量的子物件。

條狀可以組合以建立定製的可滾動佈局和效果。例如,單個視口可以有一個可摺疊的標題,然後是一個線性列表,最後是一個網格。這三個條狀將透過條狀佈局協議進行協作,無論這些子物件屬於標題、列表還是網格,它們都只生成可見於視口中的子物件[6]

按需構建 Widget

#

如果 Flutter 有嚴格的構建-然後-佈局-然後-繪製管道,那麼上述內容不足以實現無限滾動列表,因為關於視口可見空間量的資訊僅在佈局階段可用。沒有額外的機制,佈局階段太晚了,無法構建填充空間所需的 Widget。Flutter 透過交織管道的構建和佈局階段來解決這個問題。在佈局階段的任何時候,框架都可以按需開始構建新的 Widget,前提是這些 Widget 是當前執行佈局的渲染物件的後代

構建和佈局的交織之所以可能,完全是由於構建和佈局演算法中資訊傳播的嚴格控制。具體來說,在構建階段,資訊只能向下傳播到樹中。當渲染物件執行佈局時,佈局遍歷還沒有訪問該渲染物件下方的子樹,這意味著在該子樹中構建生成的寫入不會使到目前為止已進入佈局計算的任何資訊無效。同樣,一旦佈局從渲染物件返回,該渲染物件在該佈局期間將永遠不會再次被訪問,這意味著後續佈局計算生成的任何寫入都不能使用於構建渲染物件子樹的資訊失效。

此外,線性協調和樹操作對於在滾動期間有效地更新 Element 以及在 Element 滾動進出視口邊緣時修改渲染樹至關重要。

API 易用性

#

只有當框架能夠有效地使用時,速度快才算有意義。為了指導 Flutter 的 API 設計朝著更高的可用性發展,Flutter 在廣泛的開發者使用者體驗研究中進行了反覆測試。這些研究有時證實了預先的設計決策,有時有助於確定功能的優先順序,有時則改變了 API 設計的方向。例如,Flutter 的 API 文件非常詳盡;使用者體驗研究證實了這種文件的價值,但也特別強調了示例程式碼和說明圖的必要性。

本節討論了 Flutter API 設計中的一些為了提高可用性而做出的決策。

API 專門化以匹配開發者的思維模式

#

Flutter 的 WidgetElementRenderObject 樹中節點的基類不定義子模型。這使得每個節點都可以針對適用於該節點的子模型進行專門化。

大多數 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 Widget 和零大小的 SizedBox 子物件,很容易在行或列中新增一個空格,但發現這種模式是不必要的,因為搜尋 space 會發現 Spacer Widget,該 Widget 直接使用 ExpandedSizedBox 來實現效果。

類似地,隱藏 Widget 子樹可以透過根本不包含 Widget 子樹來輕鬆完成。然而,開發者通常期望有一個 Widget 來完成這項工作,因此 Visibility Widget 存在,將這種模式封裝在一個簡單的可重用 Widget 中。

顯式引數

#

UI 框架往往有很多屬性,以至於開發者幾乎不可能記住每個類的每個建構函式引數的語義含義。由於 Flutter 使用響應式正規化,Flutter 中的構建方法通常有很多建構函式呼叫。透過利用 Dart 對命名引數的支援,Flutter 的 API 能夠使這些構建方法保持清晰和易於理解。

此模式擴充套件到任何具有多個引數的方法,並且特別擴充套件到任何布林引數,以便方法呼叫中的孤立的 truefalse 字面量始終是自解釋的。此外,為了避免通常由 API 中的雙重否定引起的混淆,布林引數和屬性始終以正面形式命名(例如,enabled: true 而不是 disabled: false)。

規避陷阱

#

Flutter 框架中許多地方使用的一種技術是定義 API,使其不存在錯誤條件。這消除了整個類別的錯誤。

例如,插值函式允許插值的開始或結束值為 null,而不是將此定義為錯誤情況:兩個 null 值之間的插值始終為 null,從 null 值插值或插值到 null 值相當於插值到給定型別的零模擬值。這意味著偶然將 null 傳遞給插值函式的開發者不會遇到錯誤情況,而是會得到一個合理的結果。

一個更微妙的例子是 Flex 佈局演算法。這種佈局的概念是將分配給 Flex 渲染物件的空間在其子物件之間分配,因此 Flex 的大小應該是可用空間的全部。在原始設計中,提供無限空間會導致失敗:這將意味著 Flex 的大小是無限的,這是一種無用的佈局配置。相反,API 已調整,以便在為 Flex 渲染物件分配無限空間時,渲染物件的大小會調整以適應子物件的所需大小,從而減少了可能錯誤的情況。

該方法還用於避免出現允許建立不一致資料的建構函式。例如,PointerDownEvent 建構函式不允許將 PointerEventdown 屬性設定為 false(這種情況是自相矛盾的);相反,建構函式沒有 down 欄位的引數,並始終將其設定為 true

總的來說,該方法是定義輸入域中所有值的有效解釋。最簡單的例子是 Color 建構函式。它不接受四個整數,一個代表紅色,一個代表綠色,一個代表藍色,一個代表 Alpha,每個都可能超出範圍,而是採用單個整數值,並定義每個位的含義(例如,最低八位定義紅色分量),以便任何輸入值都是有效的顏色值。

一個更復雜的例子是 paintImage() 函式。該函式接受十一個引數,其中一些引數的輸入域相當寬,但它們被仔細設計成大部分是相互正交的,因此無效組合非常少。

主動報告錯誤情況

#

並非所有錯誤條件都可以透過設計消除。對於剩餘的錯誤,在除錯版本中,Flutter 通常會盡早捕獲錯誤並立即報告。斷言被廣泛使用。建構函式引數會被詳細地進行健全性檢查。生命週期會被監控,當檢測到不一致時,會立即丟擲異常。

在某些情況下,這會被推向極致:例如,在執行單元測試時,無論測試還在做什麼,每個佈局過的 RenderBox 子類都會檢查其內在尺寸方法是否滿足內在尺寸約定。這有助於捕獲 API 中可能未被使用的錯誤。

當丟擲異常時,它們會包含儘可能多的可用資訊。Flutter 的一些錯誤訊息會主動探測相關的堆疊跟蹤,以確定實際錯誤的可能位置。另一些會遍歷相關的樹來確定錯誤資料的來源。最常見的錯誤包括詳細說明,有時還包括避免錯誤的示例程式碼,或指向進一步文件的連結。

響應式正規化

#

可變樹 API 存在一個二元訪問模式:建立樹的原始狀態通常使用一組非常不同的操作,而後續更新則使用另一組。Flutter 的渲染層使用此正規化,因為它是維護持久樹的有效方法,這對於高效佈局和繪製至關重要。然而,這意味著直接與渲染層互動充其量很笨拙,最壞情況則容易出錯。

Flutter 的 Widget 層引入了一種使用響應式正規化[7]的組合機制來操作底層渲染樹。此 API 透過將樹建立和樹突變步驟合併為單個樹描述(構建)步驟來抽象出樹操作,在該步驟中,在系統狀態每次更改後,由開發者描述使用者介面的新配置,框架計算出反映此新配置所需的樹突變序列。

插值

#

由於 Flutter 框架鼓勵開發者描述與當前應用程式狀態匹配的介面配置,因此存在一種機制可以在這些配置之間進行隱式動畫。

例如,假設在狀態 S1 中,介面由一個圓組成,而在狀態 S2 中,它由一個正方形組成。如果沒有動畫機制,狀態更改將導致一個刺眼的介面變化。隱式動畫允許圓在幾幀內平滑地變為正方形。

每個可以隱式動畫的特徵都有一個有狀態的 Widget,它會記錄輸入的當前值,並在輸入值更改時開始一個動畫序列,在指定的持續時間內從當前值過渡到新值。

這是使用 lerp(線性插值)函式以及不可變物件實現的。每個狀態(例如,圓和正方形)都表示為一個不可變物件,該物件配置有適當的設定(顏色、筆觸寬度等)並知道如何自行繪製。當需要繪製動畫的中間步驟時,開始和結束值將傳遞給適當的 lerp 函式,以及一個表示動畫中點的 *t* 值,其中 0.0 表示開始,1.0 表示結束[8],該函式返回一個表示中間階段的第三個不可變物件。

對於從圓到正方形的過渡,lerp 函式將返回一個表示“圓角正方形”的物件,其半徑由 *t* 值匯出的分數描述,顏色透過顏色 lerp 函式插值,筆觸寬度透過雙精度 lerp 函式插值。該物件實現了與圓和正方形相同的介面,然後可以在請求時繪製自身。

這種技術允許狀態機制、狀態到配置的對映、動畫機制、插值機制以及與如何繪製每一幀相關的特定邏輯完全彼此分離。

這種方法具有廣泛的適用性。在 Flutter 中,可以插值基本型別,如 ColorShape,但也可以插值更復雜的型別,如 DecorationTextStyleTheme。這些通常由可以自身插值的元件構成,並且對更復雜的物件進行插值通常與遞迴地插值描述複雜物件的_所有_值一樣簡單。

一些可插值物件由類層次結構定義。例如,形狀由 ShapeBorder 介面表示,存在各種形狀,包括 BeveledRectangleBorderBoxBorderCircleBorderRoundedRectangleBorderStadiumBorder。單個 lerp 函式無法預測所有可能的型別,因此介面定義了 lerpFromlerpTo 方法,靜態 lerp 方法會委託給它們。當被告知從形狀 A 插值到形狀 B 時,首先會詢問 B 是否可以 lerpFrom A,如果不能,則會詢問 A 是否可以 lerpTo B。(如果兩者都不可能,則函式在 *t* 值小於 0.5 時返回 A,否則返回 B。)

這允許類層次結構被任意擴充套件,後期新增的類能夠在其與先前已知的_值之間進行插值。

在某些情況下,插值本身無法用任何可用類來描述,並且定義了一個私有類來描述中間階段。例如,在 CircleBorderRoundedRectangleBorder 之間進行插值時就是這種情況。

這種機制還有一個額外的優點:它可以處理從中間階段到新值的插值。例如,在圓到正方形過渡的中間,形狀可以再次改變,導致動畫需要插值到三角形。只要三角形類可以 lerpFrom 圓角正方形中間類,就可以無縫執行過渡。

結論

#

Flutter 的口號“一切皆 Widget”圍繞著透過組合 Widget 來構建使用者介面,而 Widget 又由逐漸更基礎的 Widget 組成。這種強大的組合結果是大量的 Widget,需要精心設計的演算法和資料結構來高效處理。透過一些額外的設計,這些資料結構還可以使開發者輕鬆建立無限滾動列表,這些列表在 Widget 可見時按需構建。


腳註


  1. 至少對於佈局而言。它可能需要為繪製、必要時構建可訪問性樹以及必要時進行命中測試而再次訪問。 ↩︎

  2. 當然,現實情況要複雜一些。一些佈局涉及內在尺寸或基線測量,這確實需要對相關子樹進行額外的訪問(透過積極快取來減輕最壞情況下的二次方效能)。然而,這些情況出奇地罕見。特別是,對於常見的收縮包裝情況,不需要內在尺寸。 ↩︎

  3. 嚴格來說,子物件的位置不是其 RenderBox 幾何形狀的一部分,因此不必在佈局期間計算。許多渲染物件在相對於自己的原點將單個子物件隱式定位在 0,0 處,這根本不需要計算或儲存。一些渲染物件避免在最後一刻計算其子物件的位置(例如,在繪製階段),以完全避免計算,前提是它們之後不會被繪製。 ↩︎

  4. 此規則存在一個例外。如按需構建 Widget部分所述,某些 Widget 可以由於佈局約束的變化而被重建。如果在同一個幀中,一個 Widget 因不相關的原因變髒,並且同時受到佈局約束變化的影響,它將被更新兩次。這種冗餘構建僅限於 Widget 本身,並且不會影響其後代。 ↩︎

  5. Key 是一個可選的、不透明的物件,它與 Widget 相關聯,其相等運算子用於影響協調演算法。 ↩︎

  6. 為了可訪問性,併為應用程式在 Widget 構建到螢幕顯示之間提供幾毫秒的額外時間,視口會為可見 Widget 前後幾百畫素建立(但不繪製)Widget。 ↩︎

  7. 這種方法最早由 Facebook 的 React 庫普及。 ↩︎

  8. 實際上,*t* 值允許超出 0.0-1.0 的範圍,對於某些曲線也是如此。例如,“彈性”曲線會短暫地過度,以表示彈跳效果。插值邏輯通常可以根據需要推斷到開始或結束值之外。對於某些型別,例如插值顏色時,*t* 值實際上會被限制在 0.0-1.0 的範圍內。 ↩︎