
老孟導讀:此篇文章非常詳細的講解了 Flutter 布局系統的作業原理,
翻譯自:https://itnext.io/flutter-layout-system-overview-c70bbe9ba909?source=bookmarks---------17------------------

最近,我決定專注于Flutter基礎知識,這次,我試圖更好地理解“布局系統的作業原理”,并回答以下問題:
- 我的小部件的尺寸看起來不合適,怎么回事?
- 我只想將Widget放置在特定位置,但是沒有任何屬性可以控制它,為什么呢?
- 我一直看到諸如BoxConstraints,RenderBox和Size之類的術語,它們之間有什么關系?
- 對布局系統如何作業有一個大概的了解?
本文并不意味著對以上所有內容進行深入而詳細的描述,但是,我們將對最重要的內容進行很好的概述,力圖將一切可視化,
“兩個階段” 布局系統和約束
首先,小部件是Flutter SDK的構建塊,但它們不負責將其自身繪制到螢屏中,每個小部件都與負責此操作的RenderBox物件相關聯,這些框是2D直角坐標系,其大小表示為距原點的偏移,每個RenderBox還將與一個BoxConstraints物件相關聯,該物件包含四個值:最大|最小寬度和最大|最小高度, RenderBox可以選擇具有所需的任何大小,但它必須遵守這些值/約束,小部件的大小/位置完全取決于這些RenderBox的屬性,
原文:The same way Widgets build a Widget three, RenderBoxes make a render three.
我覺得three可能寫錯了,應該是tree,譯文:以同樣的方式小部件生成 組件樹,RenderBoxes生成渲染樹,

我們可以將Flutter的布局系統視為兩階段系統,在第一個階段中,framework 以遞回地方式沿著渲染樹 把BoxConstraints傳遞給子組件,它為父組件提供了一種方式來調節/增強子組件的尺寸,并根據需要更新這些限制,換句話說,這是負責傳播約束資訊的階段,讓每個人知道其最大/最小值,
完成后,第二階段開始,這次,每個RenderBox都將其選擇的大小傳遞回其父物件,父級收集所有子級的大小,然后使用此幾何資訊將每個子級正確定位在自己的笛卡爾系統中,這個階段負責確定大小和位置,在此階段,父組件知道每個子組件的大小以及他們的位置,
那么,這到底意味著什么?
這意味著父組件有責任定義/限制/約束子組件的尺寸,并相對于其坐標系進行定位,換句話說,小部件可以選擇其大小,但是它必須始終遵守從其父級收到的約束,此外,小部件不知道其在螢屏上的位置,但其父級知道,
如果您對小部件的大小或位置有疑問,請嘗試查看(更新)其父組件,
Example
好的,讓我們將所有內容可視化,嘗試通過示例了解正在發生的事情,但是在此之前,以下是一些在除錯約束時可能有用的術語,
下面的術語未翻譯,因為這些術語本身比譯文更好理解:
- If *max(w|h) = min (w|h)*, that is *tightly* constrained.
- If *min(w|h) = 0*, we have a *loose* constraint.
- If *max(w|h) != infinite*, the constraint is *bounded.*
- If *max(w|h) = infinite*, the constraint is *unbounded.*
- If *min(w|h) = infinite*, is just said to be *infinite*

我們將使用的是初始應用模板的修改版本,通常,您可以通過兩種簡單的方法來檢查視窗小部件RenderBox及其屬性:
-
通過代碼執行:我們可以使用LayoutBuilder在布局系統第一階段攔截BoxConstraints傳播,并檢查約束,然后,在第二階段完成后,我們使用鍵來獲取小部件的RenderBox并能夠檢查Size,Position,
-
或使用DevTools視窗小部件檢查器

import 'package:flutter/material.dart';
GlobalKey _keyMyApp = GlobalKey();
GlobalKey _keyMaterialApp = GlobalKey();
GlobalKey _keyHomePage = GlobalKey();
GlobalKey _keyScaffold = GlobalKey();
GlobalKey _keyAppbar = GlobalKey();
GlobalKey _keyCenter = GlobalKey();
GlobalKey _keyFAB = GlobalKey();
GlobalKey _keyText = GlobalKey();
void printConstraint(String name, BoxConstraints c) {
print(
'CONSTRAINT of $name: min(w=${c.minWidth.toInt()},h=${c.minHeight.toInt()}) max(w=${c.maxWidth.toInt()},h=${c.maxHeight.toInt()})',
);
}
void printSizes() {
printSize('MyApp', _keyMyApp);
printSize('MaterialApp', _keyMaterialApp);
printSize('HomePage', _keyHomePage);
printSize('Scaffold', _keyScaffold);
printSize('Appbar', _keyAppbar);
printSize('Center', _keyCenter);
printSize('Text', _keyText);
printSize('FAB', _keyFAB);
}
void printSize(String name, GlobalKey key) {
final RenderBox renderBox = key.currentContext.findRenderObject();
final size = renderBox.size;
print("SIZE of $name: w=${size.width.toInt()},h=${size.height.toInt()}");
}
void printPositions() {
printPosition('MyApp', _keyMyApp);
printPosition('MaterialApp', _keyMaterialApp);
printPosition('HomePage', _keyHomePage);
printPosition('Scaffold', _keyScaffold);
printPosition('Appbar', _keyAppbar);
printPosition('Center', _keyCenter);
printPosition('Text', _keyText);
printPosition('FAB', _keyFAB);
}
void printPosition(String name, GlobalKey key) {
final RenderBox renderBox = key.currentContext.findRenderObject();
final position = renderBox.localToGlobal(Offset.zero);
print("POSITION of $name: $position ");
}
void main() {
runApp(LayoutBuilder(
builder: (context, constraints) {
printConstraint('MyApp', constraints);
return MyApp();
},
));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
key: _keyMyApp,
builder: (context, constraints) {
printConstraint('MaterialApp', constraints);
return MaterialApp(
key: _keyMaterialApp,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: LayoutBuilder(
builder: (context, constraints) {
printConstraint('HomePage', constraints);
return HomePage(
key: _keyHomePage,
title: 'Flutter Demo Home Page',
);
},
),
);
},
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
void _afterLayout(_) {
printSizes();
printPositions();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
printConstraint('Scaffold', constraints);
return Scaffold(
backgroundColor: Colors.purple,
key: _keyScaffold,
appBar: AppBar(
key: _keyAppbar,
title: Text(widget.title),
),
body: LayoutBuilder(
builder: (context, constraints) {
printConstraint('Center', constraints);
return Center(
key: _keyCenter,
child: LayoutBuilder(builder: (context, constraints) {
printConstraint('Text', constraints);
return Text(
'You have pushed the button this many times:',
key: _keyText,
style: TextStyle(color: Colors.white),
);
}),
);
},
),
floatingActionButton: LayoutBuilder(
builder: (context, constraints) {
printConstraint('FAB', constraints);
return FloatingActionButton(
key: _keyFAB,
onPressed: printSizes,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
),
);
});
}
}

讓我們一步一步來看看發生了什么(在這里我們將忽略LayoutBuilders),

在我們的示例中發生的第一件事是執行runApp(..),此函式檢查螢屏當前大小(在我們的示例中為392:759),然后創建一個BoxConstraints物件,其中包含將發送到我們的第一個小部件(MyApp)的約束,注意,max | min的寬度和高度都相等;因此,runApp使用了嚴格的約束-通過這樣做,MyApp除了選擇螢屏上的可用空間外,在選擇其大小時將別無選擇,

然后將約束向下傳播到Widget樹, MyApp,MaterialApp,HomePage和Scaffold都被告知相同的嚴格約束,因此,所有人將被迫填滿整個螢屏,每個小部件都有機會向其子項通知不同的BoxConstraints(仍然尊重已收到的子項),但是,在這種情況下,他們選擇不這樣做,

現在事情開始變得越來越有趣,Scaffold告知AppBar有關必須使用的BoxConstraints的資訊,但是,這一次,它使用了寬松的約束(min h = 0),它使AppBar有機會選擇所需的任何高度,但仍必須使用width = 390,
AppBar是一種特殊的小部件,稱為PreferredSizeWidget,這種型別的小部件不會對其子級施加任何約束,如果嘗試使用LayoutBuilder獲取Title的約束,則會出現錯誤,而是,AppBar以首選/默認大小回應Scaffold:高度= 80,寬度= 392(受接收到的約束的約束)
獲得AppBar的大小后,Scaffold繼續下一個子項:Center

好的,這里發生了很多事情,讓我們嘗試了解:
- Scaffold告知Center其約束,讓其選擇在 0 < width < 392 和 0 < height < 697 中選擇,請注意,最大高度為759(螢屏最大高度)減去80(AppBar選擇的高度),
- Center轉到其子組件“Text”,轉發相同的約束,
- Text選擇一個足以顯示其資料的大小(279:16),然后回復Center,
- 借助手上的幾何資訊(大小),Center可以在其笛卡爾系統內正確定位文本,作為父母,Center有權選擇其子組件位置,在這種情況下,它決定將其居中,
流程繼續:

- 然后,Center為自己選擇一個大小,而不是僅選擇一個“足夠”的大小(如“Text”一樣),而是決定盡可能大,因此受到了限制,
- Scaffold收到Center所需的尺寸,并且流程繼續向其最后一個孩子:FAB
- FAB收到約束,然后將其首選大小回傳給Scaffold(56:56)
- 最后,Scaffold還具有將每個孩子都放置在其笛卡爾系統內所需的所有幾何資訊,
最后,對Scaffold以上的所有小部件重復該程序:

- Size資訊繼續沿渲染樹傳播,
- 每個小部件都使用此資訊將每個孩子放置在笛卡爾系統內,
- Scaffold回復HomePage,HomePage回復MaterialApp,MaterialApp回復MyApp,直到最后再次到達Main,
- Main獲取此“最終”視窗小部件,并將其最終系結到螢屏中,
RenderBox樹最終系結在螢屏上,我們有一個正在運行的應用程式,
有趣的事情要記住
- 小部件不知道其在螢屏上的位置;它的父組件才知道,
- 小部件可以選擇想要的大小,但必須根據其父級的限制,
- 約束向下傳播,而大小向上傳播,
- 嘗試了解約束條件,它們可能在以后有用,
我希望所有這些都可以幫助您更好地了解Flutter布局系統的作業方式,
交流
老孟Flutter博客地址(330個控制元件用法):http://laomengit.com
歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:
![]() |
![]() |
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/301.html
標籤:Dart


