flutter布局約束原理深入解析
引言
剛開始接觸flutter的時(shí)候,Container組件是用得最多的。它就像HTML中的div一樣普遍,專門用來布局頁面的。
但是使用Container嵌套布局的時(shí)候,經(jīng)常出現(xiàn)一些令人無法理解的問題。就如下面代碼,在一個(gè)固定的容器中,子組件卻鋪滿了全屏。
/// 例一
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 300,
color: Colors.amber,
child: Container(width: 50, height: 50, color: Colors.red,),
);
}

然后要加上alignment屬性,子組件正常顯示了,但容器還是鋪滿全屏。
/// 例二
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 300,
color: Colors.amber,
alignment: Alignment.center,
child: Container(width: 50, height: 50, color: Colors.red,),
);
}

而在容器外層添加一個(gè)Scaffold組件,它就正常顯示了。
/// 例三
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: 300,
height: 300,
color: Colors.amber,
alignment: Alignment.center,
child: Container(width: 50, height: 50, color: Colors.red,),
),
);
}

這一切的怪異行為困擾了我很久,直到我深入了flutter布局的學(xué)習(xí),才漸漸解開這些疑惑。
1、flutter的widget類型
flutter的widget可以分為三類,組合類ComponentWidget、代理類ProxyWidget和繪制類RenderObjectWidget

組合類:如Container、Scaffold、MaterialApp還有一系列通過繼承StatelessWidget和StatefulWidget的類。組合類是我們開發(fā)過程中用得最多的組件。
代理類:InheritedWidget,功能型組件,它可以高效快捷的實(shí)現(xiàn)共享數(shù)據(jù)的跨組件傳遞。如常見的Theme、MediaQuery就是InheritedWidget的應(yīng)用。
繪制類:屏幕上看到的UI幾乎都會(huì)通過RenderObjectWidget實(shí)現(xiàn)。通過繼承它,可以進(jìn)行界面的布局和繪制。如Align、Padding、ConstrainedBox等都是通過繼承RenderObjectWidget,并通過重寫createRenderObject方法來創(chuàng)建RenderObject對(duì)象,實(shí)現(xiàn)最終的布局(layout)和繪制(paint)。
2、Container是個(gè)組合類
顯而易見Container繼承StatelessWidget,它是一個(gè)組合類,同時(shí)也是一個(gè)由DecoratedBox、ConstrainedBox、Transform、Padding、Align等組件組合的多功能容器。可以通過查看Container類,看出它實(shí)際就是通過不同的參數(shù)判斷,再進(jìn)行組件的層層嵌套來實(shí)現(xiàn)的。
@override
Widget build(BuildContext context) {
Widget? current = child;
if (child == null && (constraints == null || !constraints!.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
} else if (alignment != null) {
current = Align(alignment: alignment!, child: current);
}
final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null) {
current = Padding(padding: effectivePadding, child: current);
}
if (color != null) {
current = ColoredBox(color: color!, child: current);
}
if (clipBehavior != Clip.none) {
assert(decoration != null);
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.maybeOf(context),
decoration: decoration!,
),
clipBehavior: clipBehavior,
child: current,
);
}
if (decoration != null) {
current = DecoratedBox(decoration: decoration!, child: current);
}
if (foregroundDecoration != null) {
current = DecoratedBox(
decoration: foregroundDecoration!,
position: DecorationPosition.foreground,
child: current,
);
}
if (constraints != null) {
current = ConstrainedBox(constraints: constraints!, child: current);
}
if (margin != null) {
current = Padding(padding: margin!, child: current);
}
if (transform != null) {
current = Transform(transform: transform!, alignment: transformAlignment, child: current);
}
return current!;
}
組合類基本不參與ui的繪制,都是通過繪制類的組合來實(shí)現(xiàn)功能。
3、flutter布局約束
flutter中有兩種布局約束BoxConstraints盒約束和SliverConstraints線性約束,如Align、Padding、ConstrainedBox使用的是盒約束。
BoxConstraints盒約束是指flutter框架在運(yùn)行時(shí)遍歷整個(gè)組件樹,在這過程中 「向下傳遞約束,向上傳遞尺寸」,以此來確定每個(gè)組件的尺寸和大小。

BoxConstraints類由4個(gè)屬性組成,最小寬度minWidth、最大寬度maxWidth、最小高度minHeight、最大高度maxHeight。
BoxConstraints({
this.minWidth,
this.maxWidth,
this.minHeight,
this.maxHeight,
});
根據(jù)這4個(gè)屬性的變化,可以分為“緊約束(tight)”、“松約束(loose)”、“無界約束”、“有界約束”。
緊約束:最小寬(高)度和最大寬(高)度值相等,此時(shí)它是一個(gè)固定寬高的約束。
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
松約束:最小寬(高)值為0,最大寬(高)大于0,此時(shí)它是一個(gè)約束范圍。
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
無界約束:最小寬(高)和最大寬(高)值存在double.infinity(無限)。
BoxConstraints.expand({double? width, double? height})
: minWidth = width ?? double.infinity,
maxWidth = width ?? double.infinity,
minHeight = height ?? double.infinity,
maxHeight = height ?? double.infinity;
有界約束:最小寬(高)和最大寬(高)值均為固定值。
BoxConstraints(100, 300, 100, 300)
4、Container布局行為解惑
了解了BoxConstraints布局約束,回到本文最開始的問題。
/// 例一
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 300,
color: Colors.amber,
child: Container(width: 50, height: 50, color: Colors.red,),
);
}
例一中,兩個(gè)固定寬高的Container,為什么子容器鋪滿了全屏?
根據(jù)BoxConstraints布局約束,遍歷整個(gè)組件樹,最開始的root是樹的起點(diǎn),它向下傳遞的是一個(gè)緊約束。因?yàn)槭且苿?dòng)設(shè)備,root即是屏幕的大小,假設(shè)屏幕寬414、高896。于是整個(gè)布局約束如下:

這里有個(gè)問題,就是Container分明已經(jīng)設(shè)置了固定寬高,為什么無效?
因?yàn)?strong>父級(jí)向下傳遞的約束,子組件必須嚴(yán)格遵守。這里Container容器設(shè)置的寬高超出了父級(jí)的約束范圍,就會(huì)自動(dòng)被忽略,采用符合約束的值。
例一兩上Container都被鋪滿屏幕,而最底下的紅色Container疊到了最上層,所以最終顯示紅色。
/// 例二
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 300,
color: Colors.amber,
alignment: Alignment.center,
child: Container(width: 50, height: 50, color: Colors.red,),
);
}
例二也同樣可以根據(jù)布局約束求證,如下圖:

這里Container為什么是ConstrainedBox和Align組件?前面說過Container是一個(gè)組合組件,它是由多個(gè)原子組件組成的。根據(jù)例二,它是由ConstrainedBox和Align嵌套而成。
Align提供給子組件的是一個(gè)松約束,所以容器自身設(shè)置50寬高值是在合理范圍的,因此生效,屏幕上顯示的就是50像素的紅色方塊。ConstrainedBox受到的是緊約束,所以自身的300寬高被忽略,顯示的是鋪滿屏幕的黃色塊。
/// 例三
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: 300,
height: 300,
color: Colors.amber,
alignment: Alignment.center,
child: Container(width: 50, height: 50, color: Colors.red,),
),
);
}
例三中Scaffold向下傳遞的是一個(gè)松約束,所以黃色Container的寬高根據(jù)自身設(shè)置的300,在合理的范圍內(nèi),有效。Container再向下傳遞的也是松約束,最終紅色Container寬高為50。

這里還有個(gè)問題,怎么確定組件向下傳遞的是緊約束還是松約束?
這就涉及到組件的內(nèi)部實(shí)現(xiàn)了,這里通過Align舉個(gè)例。
Align是一個(gè)繪制組件,它能夠進(jìn)行界面的布局和繪制,這是因?yàn)锳lign的繼承鏈為:
Align -> SingleChildRenderObjectWidget -> RenderObjectWidget
Align需要重寫createRenderObject方法,返回RenderObject的實(shí)現(xiàn),這里Align返回的是RenderPositionedBox,所以核心內(nèi)容就在這個(gè)類中
class Align extends SingleChildRenderObjectWidget {
/// ...
@override
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.maybeOf(context),
);
}
/// ...
}
而RenderPositionedBox類中,重寫performLayout方法,該方法用于根據(jù)自身約束條件,計(jì)算出子組件的布局,再根據(jù)子組件的尺寸設(shè)置自身的尺寸,形成一個(gè)至下而上,由上到下的閉環(huán),最終實(shí)現(xiàn)界面的整個(gè)繪制。
RenderPositionedBox -> RenderAligningShiftedBox -> RenderShiftedBox -> RenderBox
class RenderPositionedBox extends RenderAligningShiftedBox {
/// ...
@override
void performLayout() {
final BoxConstraints constraints = this.constraints; // 自身的約束大小
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
/// 存在子組件
if (child != null) {
/// 開始布局子組件
child!.layout(constraints.loosen(), parentUsesSize: true);
/// 根據(jù)子組件的尺寸設(shè)置自身尺寸
size = constraints.constrain(Size(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
));
/// 計(jì)算子組件的位置
alignChild();
} else {
/// 不存在子組件
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}
/// ...
}
根據(jù)Align中performLayout方法的實(shí)現(xiàn),可以確定該組件最終會(huì)給子組件傳遞一個(gè)怎么樣的約束。
/// constraints.loosen提供的是一個(gè)松約束 child!.layout(constraints.loosen(), parentUsesSize: true);
/// loosen方法
BoxConstraints loosen() {
assert(debugAssertIsValid());
/// BoxConstraints({double minWidth = 0.0, double maxWidth = double.infinity, double minHeight = 0.0, double maxHeight = double.infinity})
return BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
);
}
其它繪制類的組件基本跟Align大同小異,只要重點(diǎn)看performLayout方法的實(shí)現(xiàn),即可判斷出組件提供的約束條件。
總結(jié)
1、flutter的widget分為,組合類、代理類和繪制類。
2、Container是一個(gè)組合類,由DecoratedBox、ConstrainedBox、Transform、Padding、Align等繪制組件組合而成。
3、flutter中有兩種布局約束BoxConstraints盒約束和SliverConstraints線性約束。
4、BoxConstraints的約束原理是: 「向下傳遞約束,向上傳遞尺寸」。
5、BoxConstraints的約束類型為:緊約束、松約束、無界約束、有界約束。
6、判斷一個(gè)繪制組件的約束行為可以通過查看performLayout方法中l(wèi)ayout傳入的約束值。
以上就是flutter布局約束原理深入解析的詳細(xì)內(nèi)容,更多關(guān)于flutter布局約束原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)濾鏡效果ColorMatrix
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)濾鏡效果ColorMatrix,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
android音樂播放簡(jiǎn)單實(shí)現(xiàn)的簡(jiǎn)單示例(MediaPlayer)
本篇文章主要介紹了android音樂播放簡(jiǎn)單實(shí)現(xiàn)的簡(jiǎn)單示例(MediaPlayer),具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
深入Android中BroadcastReceiver的兩種注冊(cè)方式(靜態(tài)和動(dòng)態(tài))詳解
這篇文章主要介紹了深入Android中BroadcastReceiver的兩種注冊(cè)方式(靜態(tài)和動(dòng)態(tài))詳解,具有一定的參考價(jià)值,有需要的可以了解一下。2016-12-12
Android?TextView的maxEms和maxLength屬性區(qū)別
這篇文章主要為大家介紹了Android?TextView的maxEms和maxLength屬性區(qū)別,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
Android實(shí)現(xiàn)取消GridView中Item選中時(shí)默認(rèn)的背景色
這篇文章主要介紹了Android實(shí)現(xiàn)取消GridView中Item選中時(shí)默認(rèn)的背景色,涉及Android GridView中Item屬性設(shè)置的相關(guān)技巧,需要的朋友可以參考下2016-02-02
android游戲載入的activity跳轉(zhuǎn)到游戲主菜單的activity具體實(shí)現(xiàn)
停止2s后由游戲載入頁面再跳轉(zhuǎn)到游戲菜單頁面,具體實(shí)現(xiàn)代碼如下,感興趣的朋友可以參考下哈2013-06-06
Android 圓角邊框的實(shí)現(xiàn)方式匯總
這篇文章主要介紹了Android 圓角邊框的實(shí)現(xiàn)方式匯總的相關(guān)資料,需要的朋友可以參考下2016-03-03
Android實(shí)現(xiàn)銀行卡號(hào)掃描識(shí)別功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)銀行卡號(hào)掃描識(shí)別功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09

