Flutter 內部原理
從 Flutter 的創始工程師之一那裡瞭解 Flutter 的內部工作原理。
本文件描述了 Flutter 工具包的內部工作原理,這些原理使得 Flutter 的 API 成為可能。由於 Flutter 小部件是使用積極的組合構建的,因此使用 Flutter 構建的使用者介面包含大量的小部件。為了支援這種工作負載,Flutter 使用亞線性演算法進行佈局和小部件構建,以及使樹外科手術高效且具有許多常數因子最佳化的資料結構。透過一些額外的細節,這種設計也使得開發者能夠使用回撥來建立無限滾動列表,這些回撥構建使用者可見的那些小部件。
積極的可組合性
#Flutter 最具特色的方面之一是其積極的可組合性。小部件是透過組合其他小部件來構建的,而這些小部件本身又是透過逐步更基本的小部件構建的。例如,Padding 是一個小部件,而不是其他小部件的屬性。因此,使用 Flutter 構建的使用者介面包含許多、許多小部件。
小部件構建遞迴最終以 RenderObjectWidgets 結束,這些小部件是建立底層渲染樹中的節點的小部件。渲染樹是一種資料結構,用於儲存使用者介面的幾何資訊,這些資訊在佈局期間計算,並在繪製和命中測試期間使用。大多數 Flutter 開發者不會直接編寫渲染物件,而是使用小部件來操作渲染樹。
為了支援小部件層面的積極可組合性,Flutter 在小部件和渲染樹層面使用許多高效的演算法和最佳化,這些演算法和最佳化在以下子部分中描述。
亞線性佈局
#對於大量的小部件和渲染物件,良好的效能的關鍵在於高效的演算法。至關重要的是佈局的效能,該演算法確定渲染物件的幾何形狀(例如,大小和位置)。一些工具包使用 O(N²) 或更差的佈局演算法(例如,某些約束域中的定點迭代)。Flutter 旨在實現初始佈局的線性效能,並在後續更新現有佈局的常見情況下實現亞線性佈局效能。通常,在佈局中花費的時間應該比渲染物件的數量增長得更慢。
Flutter 每幀執行一次佈局,佈局演算法以單次傳遞的方式工作。約束由父物件透過呼叫子物件的佈局方法向下傳遞。子物件遞迴地執行自己的佈局,然後透過從其佈局方法返回來返回幾何資訊。重要的是,一旦渲染物件從其佈局方法返回,該渲染物件將不會在下次幀的佈局之前再次被訪問1。這種方法將原本可能分開的測量和佈局傳遞合併為一次傳遞,並且,因此,每個渲染物件在佈局期間最多被訪問兩次2:一次向下遍歷樹,一次向上遍歷樹。
Flutter 有此通用協議的幾個專業化。最常見的專業化是 RenderBox,它在二維笛卡爾座標系中執行。在盒子佈局中,約束是最小和最大寬度以及最小和最大高度。在佈局期間,子物件透過在這些邊界內選擇大小來確定其幾何形狀。子物件從佈局返回後,父物件決定子物件在其座標系中的位置3。請注意,子物件的佈局不能依賴於其位置,因為位置是在子物件從佈局返回後才確定的。因此,父物件可以在不需要重新計算其佈局的情況下重新定位子物件。
更一般地說,在佈局期間,從父物件流向子物件的唯一資訊是約束,從子物件流向父物件的唯一資訊是幾何資訊。這些不變性可以減少佈局期間所需的工作量
-
如果子物件沒有將自己的佈局標記為髒,只要父物件向子物件提供與子物件在上次佈局期間收到的相同的約束,子物件就可以立即從佈局返回,從而切斷遍歷。
-
每當父物件呼叫子物件的佈局方法時,父物件都會指示它是否使用從子物件返回的大小資訊。如果父物件通常不使用大小資訊(這很常見),那麼如果子物件選擇新的大小,父物件就不需要重新計算其佈局,因為父物件保證新的大小將符合現有的約束。
-
嚴格的約束是可以由唯一有效的幾何形狀滿足的約束。例如,如果最小和最大寬度彼此相等,並且最小和最大高度彼此相等,那麼滿足這些約束的唯一大小就是具有該寬度和高度的大小。如果父物件提供嚴格的約束,那麼即使父物件的佈局依賴於子物件的大小,每當子物件重新計算其佈局時,父物件也不需要重新計算其佈局,因為子物件不能在沒有來自其父物件的新的約束的情況下更改大小。
-
渲染物件可以宣告它僅使用來自父物件的約束來確定其幾何形狀。此宣告告知框架,即使約束不嚴格並且父物件的佈局依賴於子物件的大小,當子物件重新計算其佈局時,該渲染物件的父物件也不需要重新計算其佈局,因為子物件不能在沒有來自其父物件的新的約束的情況下更改大小。
由於這些最佳化,當渲染物件樹包含髒節點時,只有這些節點和圍繞它們的子樹的有限部分會在佈局期間被訪問。
亞線性小部件構建
#與佈局演算法類似,Flutter 的小部件構建演算法也是亞線性的。構建後,小部件由元素樹儲存,該樹保留使用者介面的邏輯結構。元素樹是必要的,因為小部件本身是不可變的,這意味著(在其他方面),它們無法記住與其他小部件的父級或子級關係。元素樹還儲存與有狀態小部件關聯的狀態物件。
響應於使用者輸入(或其他刺激),一個元素可以變為髒,例如,如果開發者在關聯的狀態物件上呼叫 setState()。框架維護一個髒元素的列表,並在構建階段直接跳轉到它們,跳過乾淨的元素。在構建階段,資訊單向地向下遍歷元素樹,這意味著每個元素在構建階段最多被訪問一次。一旦清理乾淨,一個元素就不能再次變髒,因為,透過歸納法,其所有祖先元素也是乾淨的4。
由於小部件是不可變的,如果一個元素沒有將自身標記為髒,只要父物件使用相同的widget重新構建該元素,該元素就可以立即從構建返回,從而切斷遍歷。此外,該元素只需要比較兩個widget引用的物件標識,就可以確定新的widget與舊的widget相同。開發者利用這種最佳化來實現重投影模式,在這種模式下,一個widget包含一個預構建的子widget儲存在其成員變數中。
在構建期間,Flutter 還使用 InheritedWidgets 避免遍歷父鏈。如果小部件經常遍歷其父鏈,例如確定當前的主題顏色,那麼構建階段將變為樹深度的 O(N²),由於積極的可組合性,這可能非常大。為了避免這些父鏈遍歷,框架透過在每個元素處維護 InheritedWidget 的雜湊表來向下推送資訊。通常,許多元素將引用相同的雜湊表,該雜湊表僅在引入新的 InheritedWidget 的元素處發生變化。
線性協調
#與流行的觀點相反,Flutter 不採用樹差異演算法。相反,框架透過使用 O(N) 演算法獨立檢查每個元素的子列表來決定是否重用元素。子列表協調演算法針對以下情況進行了最佳化
- 舊的子列表為空。
- 兩個列表相同。
- 列表中有一個或多個小部件在列表中的精確位置被插入或刪除。
- 如果每個列表都包含具有相同 key5 的小部件,則將匹配這兩個小部件。
通常的方法是透過比較每個小部件的執行時型別和 key,在兩個子列表的開頭和結尾匹配,從而可能在每個列表的中間找到包含所有不匹配子項的非空範圍。然後,框架將舊子列表範圍中的子項放入基於其 key 的雜湊表中。接下來,框架遍歷新子列表範圍並按 key 在雜湊表中查詢匹配項。未匹配的子項將被丟棄並從頭開始重建,而匹配的子項將使用其新的小部件進行重建。
樹外科手術
#重用元素對於效能至關重要,因為元素擁有兩個關鍵資料:有狀態小部件的狀態和底層的渲染物件。當框架能夠重用一個元素時,該邏輯部分的使用者介面的狀態將被保留,並且先前計算的佈局資訊可以被重用,通常可以避免整個子樹的遍歷。事實上,重用元素非常重要,以至於 Flutter 支援在保留狀態和佈局資訊的非區域性樹突變。
開發者可以透過將一個 GlobalKey 與他們的某個 widget 關聯,來執行非本地樹突變。每個全域性鍵在整個應用程式中都是唯一的,並註冊到一個執行緒特定的雜湊表中。在構建階段,開發者可以將具有全域性鍵的 widget 移動到元素樹中的任意位置。框架不會在該位置構建一個新的元素,而是會檢查雜湊表,並將現有元素從其先前位置重新定位到其新位置,從而保留整個子樹。
重新定位的子樹中的渲染物件能夠保留其佈局資訊,因為佈局約束是渲染樹中從父物件流向子物件唯一的的資訊。新的父物件會被標記為佈局已更改,因為其子列表已經改變,但如果新的父物件將相同的佈局約束傳遞給子物件,就像子物件從其舊父物件接收到的那樣,子物件可以立即從佈局中返回,從而切斷遍歷。
全域性鍵和非本地樹突變被開發者廣泛用於實現諸如英雄過渡和導航之類的效果。
常數因子最佳化
#除了這些演算法最佳化之外,實現積極的可組合性還依賴於幾個重要的常數因子最佳化。這些最佳化對於上述主要演算法討論的葉子節點來說最為重要。
-
與子模型無關。 與大多數使用子列表的工具包不同,Flutter 的渲染樹並不致力於特定的子模型。例如,
RenderBox類有一個抽象的visitChildren()方法,而不是具體的firstChild和nextSibling介面。許多子類僅支援單個子物件,直接作為成員變數持有,而不是子物件列表。例如,RenderPadding僅支援單個子物件,因此具有更簡單的佈局方法,執行時間更短。 -
視覺渲染樹,邏輯 widget 樹。 在 Flutter 中,渲染樹在裝置無關的視覺座標系中執行,這意味著 x 座標中的較小值始終朝左,即使當前的閱讀方向是從右到左。widget 樹通常在邏輯座標系中執行,這意味著使用開始和結束值,其視覺解釋取決於閱讀方向。從邏輯座標到視覺座標的轉換是在 widget 樹和渲染樹之間的交接過程中完成的。這種方法效率更高,因為渲染樹中的佈局和繪製計算比 widget 到渲染樹的交接更頻繁,並且可以避免重複的座標轉換。
-
文字由專門的渲染物件處理。 大多數渲染物件都不知道文字的複雜性。相反,文字由專門的渲染物件
RenderParagraph處理,它是渲染樹中的葉子節點。開發者透過組合將文字合併到他們的使用者介面中,而不是子類化一個文字感知的渲染物件。這種模式意味著,只要其父物件提供相同的佈局約束,RenderParagraph就可以避免重新計算其文字佈局,即使在樹突變期間也是如此。 -
可觀察物件。 Flutter 使用模型觀察和反應式範例。顯然,反應式範例占主導地位,但 Flutter 對一些葉子資料結構使用可觀察模型物件。例如,
Animations 在其值發生變化時會通知觀察者列表。Flutter 將這些可觀察物件從 widget 樹傳遞到渲染樹,渲染樹直接觀察它們,並在它們發生變化時僅使管道的適當階段失效。例如,對Animation<Color>的更改可能僅觸發繪製階段,而不是構建和繪製階段。
綜上所述,並且加總於積極組合建立的大型樹,這些最佳化對效能具有實質性的影響。
元素樹和 RenderObject 樹的分離
#Flutter 中的 RenderObject 和 Element (Widget) 樹是同構的(嚴格來說,RenderObject 樹是 Element 樹的子集)。一個顯而易見的簡化是將這些樹合併成一棵樹。然而,在實踐中,將這些樹分開有很多好處
-
效能。 當佈局發生變化時,只需要遍歷佈局樹的相關部分。由於組合,元素樹通常有許多額外的節點,這些節點必須被跳過。
-
清晰度。 更清晰的關注點分離允許 widget 協議和渲染物件協議各自專門針對其特定需求,從而簡化 API 表面,降低錯誤風險和測試負擔。
-
型別安全。 渲染物件樹可以更具型別安全性,因為它可以在執行時保證子物件將是適當的型別(每個座標系,例如,都有其自己型別的渲染物件)。組合 widget 可以不知道佈局期間使用的座標系(例如,暴露應用程式一部分模型的相同 widget 可以在框佈局和切片佈局中使用),因此在元素樹中,驗證渲染物件的型別需要遍歷樹。
無限滾動
#無限滾動列表對於工具包來說是出了名的難題。Flutter 使用基於builder 模式的簡單介面支援無限滾動列表,其中 ListView 使用回撥函式按需構建 widget,因為它們在滾動期間對使用者可見。支援此功能需要視口感知佈局和按需構建 widget。
視口感知佈局
#與 Flutter 中的大多數事物一樣,可滾動 widget 是使用組合構建的。可滾動 widget 的外部是一個 Viewport,它是一個“內部更大”的框,這意味著它的子物件可以超出視口的邊界,並且可以滾動到檢視中。但是,與其擁有 RenderBox 子物件,視口具有 RenderSliver 子物件,稱為切片,它們具有視口感知佈局協議。
切片佈局協議與框佈局協議的結構相匹配,即父物件將約束傳遞給其子物件,並接收幾何體作為回報。但是,兩種協議之間的約束和幾何體資料不同。在切片協議中,子物件會獲得有關視口的資訊,包括剩餘的可見空間量。它們返回的幾何體資料可以實現各種與滾動相關的效果,包括可摺疊的標題和視差效果。
不同的切片以不同的方式填充視口中可用的空間。例如,生成線性子物件列表的切片會按順序佈局每個子物件,直到切片用完子物件或用完空間。類似地,生成二維子物件網格的切片僅填充其網格的可見部分。由於它們知道有多少空間可見,切片即使它們有可能生成無限數量的子物件,也可以生成有限數量的子物件。
切片可以組合起來建立定製的可滾動佈局和效果。例如,單個視口可以有一個可摺疊的標題,然後是一個線性列表,然後是一個網格。所有三個切片將透過切片佈局協議進行協作,僅生成實際透過視口可見的子物件,無論這些子物件屬於標題、列表還是網格6。
按需構建小部件
#如果 Flutter 具有嚴格的構建-然後-佈局-然後-繪製 管道,那麼上述內容不足以實現無限滾動列表,因為有關透過視口可見空間的量的資訊僅在佈局階段可用。如果沒有額外的機制,佈局階段太晚了,無法構建必要的填充空間的 widget。Flutter 透過交錯管道的構建和佈局階段來解決此問題。在佈局階段的任何時候,框架都可以開始按需構建新的 widget,只要這些 widget 是當前執行佈局的渲染物件的後代。
構建和佈局的交錯是可能的,這僅僅是因為構建和佈局演算法中資訊傳播的嚴格控制。具體來說,在構建階段,資訊只能向下樹傳播。當渲染物件執行佈局時,佈局遍歷尚未訪問該渲染物件下方的子樹,這意味著在子樹中生成的寫入不會使迄今為止進入佈局計算的資訊失效。同樣,一旦佈局從渲染物件返回,該渲染物件將永遠不會在此佈局期間再次被訪問,這意味著後續佈局計算生成的寫入不會使用於構建渲染物件子樹的資訊失效。
此外,線性調和和樹突變對於在滾動期間有效地更新元素以及在元素在視口邊緣滾動進出檢視時修改渲染樹至關重要。
API 人體工程學
#僅僅速度快,如果框架實際上可以有效地使用,那才重要。為了指導 Flutter 的 API 設計朝著更大的可用性發展,Flutter 一直在與開發人員進行廣泛的 UX 研究中反覆測試。這些研究有時會確認預先存在的決策,有時會幫助確定功能的優先順序,有時會改變 API 設計的方向。例如,Flutter 的 API 有大量文件;UX 研究證實了這些文件的價值,但也強調了對示例程式碼和說明性圖表的特定需求。
本節討論了 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 widget 和零尺寸的 SizedBox 子物件可以輕鬆地在行或列中新增空格,但發現這種模式是不必要的,因為搜尋 space 會發現 Spacer widget,它直接使用 Expanded 和 SizedBox 來實現該效果。
類似地,隱藏一個 widget 子樹可以透過根本不在 build 中包含該 widget 子樹來輕鬆完成。然而,開發人員通常期望有一個 widget 來執行此操作,因此 Visibility widget 存在於此,將這種模式包裝在一個簡單的可重用 widget 中。
顯式引數
#UI 框架往往有很多屬性,以至於開發人員很難記住每個類中每個建構函式引數的語義含義。由於 Flutter 使用響應式範例,因此 Flutter 中的 build 方法通常包含許多建構函式呼叫。透過利用 Dart 對命名引數的支援,Flutter 的 API 能夠使這些 build 方法清晰易懂。
這種模式擴充套件到具有多個引數的任何方法,特別是擴充套件到任何布林引數,以便方法呼叫中的孤立的 true 或 false 字面量始終是自文件化的。此外,為了避免 API 中雙重否定通常造成的混淆,布林引數和屬性始終以肯定形式命名(例如,enabled: true 而不是 disabled: false)。
消除潛在問題
#Flutter 框架中許多地方使用的一種技術是定義 API,使其不存在錯誤條件。這消除了整個類別的錯誤考慮。
例如,插值函式允許插值的兩端或單端為 null,而不是將其定義為錯誤情況:在兩個 null 值之間插值始終為 null,從 null 值或到 null 值插值等效於插值到給定型別的零模擬值。這意味著意外將 null 傳遞給插值函式的開發人員不會遇到錯誤情況,而是會得到合理的結果。
一個更微妙的例子是在 Flex 佈局演算法中。此佈局的概念是,給定 flex 渲染物件的空間在其子物件之間分配,因此 flex 的大小應該是可用空間的全部。在原始設計中,提供無限空間會導致失敗:它意味著 flex 應該被無限地調整大小,這是一種無用的佈局配置。相反,API 被調整為當無限空間分配給 flex 渲染物件時,渲染物件將其自身調整為適合子物件所需的大小,從而減少了可能的錯誤情況數量。
這種方法也用於避免允許建立不一致資料的建構函式。例如,PointerDownEvent 建構函式不允許將 PointerEvent 的 down 屬性設定為 false(這是一種自相矛盾的情況);相反,建構函式沒有 down 欄位的引數,並且始終將其設定為 true。
通常,該方法是為輸入域中的所有值定義有效的解釋。最簡單的例子是 Color 建構函式。與其採用四個整數,每個整數分別代表紅色、綠色、藍色和 alpha,而每個整數都可能超出範圍,不如預設建構函式採用單個整數值,並定義每個位的含義(例如,底部八位定義紅色元件),以便任何輸入值都是有效的顏色值。
一個更復雜的例子是 paintImage() 函式。此函式接受十一個引數,其中一些引數具有相當寬的輸入域,但它們經過精心設計,彼此大部分是正交的,因此無效組合很少。
積極報告錯誤情況
#並非所有錯誤條件都可以設計出來。對於那些仍然存在的錯誤,在除錯構建中,Flutter 通常會嘗試在早期捕獲錯誤並立即報告它們。廣泛使用了斷言。建構函式引數會進行詳細的檢查。生命週期受到監控,並且當檢測到不一致時,會立即丟擲異常。
在某些情況下,這被推向了極端:例如,當執行單元測試時,無論測試正在做什麼,每個 RenderBox 子類都會積極檢查其內部大小調整方法是否滿足內部大小調整契約。這有助於捕獲可能不會被其他方式使用的 API 中的錯誤。
當丟擲異常時,它們會包含儘可能多的資訊。Flutter 的一些錯誤訊息會主動探測相關的堆疊跟蹤,以確定實際錯誤的可能位置。其他訊息會遍歷相關樹,以確定錯誤資料的來源。最常見的錯誤包括詳細的說明,在某些情況下,包括避免該錯誤的示例程式碼或指向進一步文件的連結。
響應式範例
#基於樹的可變 API 存在一種二分訪問模式:建立樹的原始狀態通常使用與後續更新非常不同的操作集。Flutter 的渲染層使用這種範例,因為它是一種維護持久樹的有效方法,這對於高效的佈局和繪製至關重要。然而,這意味著直接與渲染層互動最多隻能勉強可行,並且最壞情況下容易出錯。
Flutter 的 widget 層引入了一種使用響應式範例7的組合機制,用於操作底層的渲染樹。此 API 透過將樹建立和樹修改步驟合併到一個樹描述(build)步驟中來抽象樹操作,在該步驟中,在每次系統狀態更改後,開發人員描述使用者介面的新配置,並且框架計算必要的樹修改序列以反映此新配置。
插值
#由於 Flutter 的框架鼓勵開發人員描述與當前應用程式狀態匹配的介面配置,因此存在一種機制可以隱式地在這些配置之間進行動畫處理。
例如,假設在狀態 S1 中,介面由一個圓組成,但在狀態 S2 中,它由一個正方形組成。如果沒有動畫機制,狀態更改將導致介面發生劇烈的變化。隱式動畫允許圓在幾個幀中平滑地變成正方形。
每個可以隱式動畫處理的特性都有一個有狀態的 widget,它會記錄輸入當前值,並在輸入值發生更改時開始動畫序列,從當前值過渡到指定持續時間的新值。
這是使用 lerp(線性插值)函式實現的,該函式使用不可變物件。每個狀態(在本例中,圓和正方形)都表示為一個配置了適當設定(顏色、筆畫寬度等)並知道如何繪製自身的不可變物件。當需要繪製動畫過程中的中間步驟時,起始值和結束值會傳遞給適當的 lerp 函式,以及表示動畫中點的 t 值,其中 0.0 表示 start,1.0 表示 end8,該函式返回表示中間階段的第三個不可變物件。
對於圓到正方形的過渡,lerp 函式將返回一個表示“圓角矩形”的物件,其半徑描述為從 t 值派生的分數,顏色使用 lerp 函式進行插值,筆畫寬度使用 lerp 函式進行插值。該物件,它實現了與圓和正方形相同的介面,在請求繪製自身時能夠繪製自身。
這種技術允許狀態機制、將狀態對映到配置、動畫機制、插值機制以及與如何繪製每個幀相關的特定邏輯完全彼此分離。
這種方法適用範圍廣泛。在 Flutter 中,基本型別(如 Color 和 Shape)可以進行插值,但更復雜的型別(如 Decoration、TextStyle 或 Theme)也可以進行插值。這些通常由可以自身進行插值的元件構建,並且插值更復雜的物件通常就像遞迴地插值描述複雜物件的所有值一樣簡單。
一些可插值物件由類層次結構定義。例如,形狀由 ShapeBorder 介面表示,並且存在各種形狀,包括 BeveledRectangleBorder、BoxBorder、CircleBorder、RoundedRectangleBorder 和 StadiumBorder。單個 lerp 函式無法預測所有可能的型別,因此該介面定義了 lerpFrom 和 lerpTo 方法,靜態 lerp 方法會委派給這些方法。當被告知從形狀 A 插值到形狀 B 時,首先詢問 B 是否可以從 A lerpFrom,然後,如果不能,則相反地詢問 A 是否可以 lerpTo B。(如果兩者都不可能,則該函式從 t 值小於 0.5 時返回 A,否則返回 B。)
這允許類層次結構任意擴充套件,後續新增可以插值到先前已知的值和自身之間。
在某些情況下,插值本身無法由任何可用類描述,並且定義了一個私有類來描述中間階段。例如,在 CircleBorder 和 RoundedRectangleBorder 之間插值時就是這種情況。
這種機制還有一個額外的優勢:它可以處理從中間階段到新值的插值。例如,在圓到正方形過渡的一半過程中,形狀可以再次更改,導致動畫需要插值到三角形。只要三角形類可以從圓角矩形中間類 lerpFrom,就可以無縫地執行過渡。
結論
#Flutter 的口號“一切都是 widget”圍繞著透過組合 widget 來構建使用者介面,而這些 widget 又由逐漸更基本的 widget 組成。這種積極的組合導致了大量的 widget,需要精心設計算法和資料結構才能有效地處理這些 widget。透過一些額外的設計,這些資料結構也使開發人員能夠建立在需要時按需構建 widget 的無限滾動列表。
腳註
-
至少對於佈局而言。如果需要,它可能會重新審視用於繪製、構建可訪問性樹和進行命中測試的情況。↩
-
當然,現實情況要複雜一些。有些佈局涉及固有尺寸或基線測量,這確實需要遍歷相關的子樹(使用積極的快取來緩解最壞情況下的二次效能問題)。然而,這些情況出乎意料地少見。特別是,固有尺寸對於常見的自適應尺寸包裝的情況並非必需。↩
-
從技術上講,子元件的位置不屬於其 RenderBox 幾何體的一部分,因此實際上不需要在佈局期間計算。許多渲染物件隱式地將單個子元件定位在其自身的原點處,座標為 0,0,這不需要任何計算或儲存。一些渲染物件會盡可能推遲計算子元件的位置(例如,在繪製階段),以避免在之後未繪製時進行計算。↩
-
存在一個例外。如 按需構建小部件 部分所述,某些小部件可能會因佈局約束的變化而重建。如果某個小部件在同一幀中由於無關原因標記為“髒”,並且又受到佈局約束變化的影響,它將被更新兩次。這種冗餘構建僅限於該小部件本身,不會影響其子元件。↩
-
Key 是一個不透明物件,可選地與小部件關聯,其相等運算子用於影響調和演算法。↩
-
為了提高可訪問性,併為應用程式在小部件構建和螢幕上顯示之間提供額外的幾毫秒時間,視口會在可見小部件之前和之後建立(但不繪製)幾百畫素的小部件。↩
-
這種方法最初是由 Facebook 的 React 庫推廣的。↩
-
在實踐中,t 值允許超出 0.0-1.0 的範圍,並且對於某些曲線確實會超出。例如,“彈性”曲線會短暫地超調,以表示彈跳效果。插值邏輯通常可以根據需要超出起點或終點。對於某些型別,例如插值顏色時,t 值實際上會被限制在 0.0-1.0 的範圍內。↩