釋出時間:2019年8月21日
照片:Mathew Schwartz on Unsplash。
Flutter Anatomy是一系列關於什麼讓Flutter...Flutter的文章。我們試圖深入瞭解Flutter如何工作,以更好地瞭解框架的一些偉大功能。
在這篇文章中,我們開始看Flutter如何使用獨特的方法來計算螢幕佈局,這有助於Flutter的速度和流暢的UI渲染。這也是讓Flutter能夠使用非常簡單的Widget組成模型,即使是複雜的螢幕也是如此。
對於圖形使用者介面框架來說,佈局是決定使用者介面元素的大小和位置的活動。尺寸和位置被稱為幾何。 對於我們經常在移動應用中看到的流體和相對佈局,計算UI元素的幾何形狀變得很困難。佈局經理通常必須在UI元素的層次結構中進行幾次傳遞,以計算父元素及其子元素的尺寸和位置--這被稱為多傳遞佈局。
另一方面,Flutter使用線性(以及在可能的情況下使用子線性)佈局。但這意味著什麼?
簡單地說,它意味著Flutter中的佈局是通過UI Widgets樹來計算每一個UI元素的幾何形狀的一個通道(向下和向上)。(這並不總是可能的,我們將在以後的文章中討論)。
一個重要的含義是,Widget樹的子集可以被更新,而不必計算整個螢幕的佈局。這種優化就是子線型佈局的意思。
如果你熟悉瀏覽器的重繪和迴流,Flutter的方法應該已經看起來是一個大規模的優化了。
這是如何工作的呢?
核心概念是父Widget對允許子Widget的大小進行限制。基於這些約束,子Widgets將其計算出的大小傳回給父Widget。最後,父Widget決定子Widget的位置。
當然也有例外,但我們會在後面的文章中講到。
讓我們用一個簡單的圖來視覺化。
小元件樹中的約束和尺寸
藍色的父節點約束黃色的子節點,而黃色的子節點又約束其子節點。這決定了子節點的最大和最小尺寸。然後,小元件根據這些約束計算出的大小會傳回樹上。
約束是什麼樣子的?
約束是子節點允許的最大和最小高度和寬度的簡單組合。我們將在另一篇文章中更詳細地研究約束)。
限制條件
假設上面樹中的藍色widget寬為100.0,高為100.0,那麼黃色的子widget的寬度和高度都可以最小為0.0,最大為100.0。
請注意,我們在Flutter中並沒有設定X/Y位置,儘管對於某些widget來說,一些子部件的定位是可能的,例如,使用Positionied widget與Stack Widget 。
讓我們通過建立一個受其父體約束的子部件,來看看這種基於約束的方法在行動。 我們建立一個寬度為100.0、高度為100.0的容器(父容器)。父容器的子容器將是另一個容器,但希望比其父容器更寬--我們設定寬度為200.0,高度為100.0。
子容器的首選寬度和高度將違反其父容器所規定的約束。父容器會告訴子容器,它的最大寬度和高度只能是100.0。基於Flutter佈局演算法,子代將其大小限制在w=100.0,h=100.0。
讓我們根據這個簡單的例子來深入瞭解一些程式碼。
class SimpleParentChildExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
width: 100.0,
height: 100.0,
child: Container(
color: Colors.yellow,
height: 100.0,
width: 200.0,
));
}
複製程式碼
這段程式碼很瑣碎,執行應用後,可以看到我們期望看到的東西--黃色的子Widget與父Widget大小相同。
簡單Flutter佈局
要想了解更多的情況,我們來看看渲染樹。
渲染樹(快速繞行)
等等,這個 "渲染樹 "是什麼東西?這個'渲染樹'是什麼東西?我們先繞道來了解一下這是什麼。
在Flutter中,渲染樹是計算Widget樹佈局的結果。換句話說,渲染樹包含了描述如何繪製一個Widget的低階UI物件。在本文中,我們感興趣的是,每個UI物件(稱為RenderObject)都有一個幾何體--即將在螢幕上繪製的東西的大小和位置。
讓我們再來看看我們簡單UI的渲染樹。
Container Widget實際上是由兩個Widgets組成的,這就是為什麼你會看到每個Container的RenderConstrainedBox和RenderDecoratedBox。為了更清楚,我將渲染物件分別用藍色(父容器)和黃色(子容器)標記。
你可以去Container Widget的原始碼中看一看,看看它是如何工作的 bit.ly/2PqGvQq 。
與我們理解Flutter中的Layout有關的是三個綠色箭頭。這些箭頭告訴我們以下內容。
- 黃色子Widget的BoxConstraints與藍色父Widget的BoxConstraints相匹配(即w=100.0,h=100.0)。
- 渲染物件的大小是w=100.0,h=100.0--這符合父Widget所規定的BoxContraints,而不是我們在黃色子Widget中設定的200.0的寬度。換句話說,沒有隱藏的溢位)。
- 'extraConstraints'屬性保留了我們為子Widget設定的首選寬度和高度的記錄。
這裡是一個簡單的視覺化的情況。
簡單佈局父約束
那又怎樣?
這種方法的簡單性掩蓋了它的重要性。讓我們來研究一些重要的意義。
1/ 複雜螢幕的高效佈局
有很多巢狀的Widgets的螢幕可以有效地佈局。即使您的螢幕變得複雜,Flutter可以(在大多數情況下)通過所有Widgets的一次傳遞來計算佈局,並再次返回。
這就是為什麼您可以在您的佈局中使用大量的Widgets,並且仍然可以看到快速流暢的螢幕。
2/ 螢幕更新時的部分佈局
螢幕被更新以反映新的狀態(例如顯示新的資料)或顯示動畫或響應輸入。當這種情況發生時,Flutter不必每次都計算螢幕的整個佈局--只計算已經改變的螢幕部分。
這種優化就是所謂的子線性佈局。它是可能的,因為父Widget的約束不受子Widget中發生的動畫或其他更新的影響。因此Flutter不需要再次重新計算父部件的佈局。這就是所謂的中繼邊界。
與瀏覽器中的迴流和重繪相比,DOM樹深處的變化可能會導致一路到根部的變化。HTML開發者需要考慮到這一點,而Flutter開發者則不需要--嗯......這幾乎是真的,請看下一點。
我們將在後面的文章中更詳細地研究這個問題,也會研究使區域性佈局效率低下的異常情況。
3/ 混合無狀態和有狀態的Widgets
在Flutter的佈局機制中,擁有Stateless和Stateful Widgets的方法是有意義的。
Stateless Widgets可以提供佈局約束,對於一個螢幕來說是不會改變的。Stateful Widgets可以改變Widget的資料或者一些視覺約束,比如AnimatedWidget。
螢幕的部分佈局意味著StatefulWidgets可以非常高效。然而要看到這個好處,我們必須避免將StatefulWidgets作為我們螢幕的大部分的父節點。
參見 bit.ly/2VBRRYm ,瞭解如何優化Stateful Widgets中子代佈局的技巧。
其他UI框架有何不同?
快速看看其他UI框架如何處理佈局是很有趣的。這有助於我們看到Flutter的方法與你之前可能看到的有些不同。
讓我們在三個不同的框架中畫出我們簡單的2框佈局。
HTML
在瀏覽器中,父代的大小自然是要適合子代的。在本例中,我們可以看到父DIV被推出來,寬度為200px。當然也可以將溢位樣式屬性設定為 "隱藏",這樣子DIV就被剪掉了,視覺上是100px×100px。
<html>
<body>
<div style="width: 100px; height: 100px; background-color: blue;">
<div style="width: 200px; height: 100px; background-color: yellow"></div>
</div>
</body>
</html>
複製程式碼
這種佈局看起來如下。
簡單的HTML佈局
iOS
在iOS中,UIViews可以 "包含 "其他UIVIew作為子檢視,這並不完全是父子關係。關鍵的區別在於,這些檢視居住在不同的層中,而子檢視位於其父檢視之上。這意味著子檢視的寬度將達到200。另一個重要的區別是,UIViews有位置--在這種情況下,約束用於使用錨約束來定位一個View與另一個View的相對位置。
class ViewController: UIViewController {
lazy var yellowSquare: UIView = {
let square = UIView(frame: .zero)
square.backgroundColor = .yellow
square.translatesAutoresizingMaskIntoConstraints = false
return square
}()
lazy var blueSquare: UIView = {
let square = UIView(frame: .zero)
square.backgroundColor = .blue
square.translatesAutoresizingMaskIntoConstraints = false
return square
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
blueSquare.addSubview(yellowSquare)
blueSquare.addConstraints([
yellowSquare.topAnchor.constraint(equalTo: blueSquare.topAnchor),
yellowSquare.bottomAnchor.constraint(equalTo: blueSquare.bottomAnchor),
yellowSquare.leadingAnchor.constraint(equalTo: blueSquare.leadingAnchor),
yellowSquare.trailingAnchor.constraint(equalTo: blueSquare.trailingAnchor),
yellowSquare.widthAnchor.constraint(equalToConstant: 200),
yellowSquare.heightAnchor.constraint(equalToConstant: 100)
])
view.addSubview(blueSquare)
view.addConstraints([
blueSquare.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
blueSquare.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50)
])
}
}
複製程式碼
佈局看起來如下。
iOS上的簡單佈局
安卓系統
馬上就到了
TL;DR
Flutter中的螢幕佈局方法很簡單但很強大。父Widgets對子Widgets實施約束,所以通過Widget樹的一次傳遞就足以計算佈局和定位。
本文將通過一個非常簡單的例子來展示Flutter中的關鍵佈局概念,以及這與其他UI框架有何不同。
在接下來的文章中,我們將深入研究一些佈局機制,以及如何使用這些知識來改進你的Flutter UIs。