我會用三篇文章來講透 Flutter ConstraintLayout(約束布局)沽瘦,讓你用起來能夠得心應手袍祖。分別是《Flutter ConstraintLayout 完全指南》堵泽、《Flutter ConstraintLayout 原理解析》物咳、《Flutter ConstraintLayout 最佳實踐》憔四。今天是第一篇伐憾。
https://github.com/hackware1993/Flutter_ConstraintLayout
前言
這是一份 Flutter ConstraintLayout 的完全指南,基于當前最新的 1.6.3-stable 版本秒际。由于現(xiàn)在的 API 已經(jīng)穩(wěn)定悬赏,所以本文可能會長期適用狡汉,后期僅會有很小的變動。
如果你有 Android ConstraintLayout 的經(jīng)驗闽颇,那你能上手更快盾戴,但也希望你能認真看完全文,因為聲明式 UI 下的用法和 XML 里的用法截然不同兵多。并且它有很多 Android ConstraintLayout 所不具有的優(yōu)秀特性尖啡。如果你是一名 iOSer,那就更應該看完了中鼠,據(jù)我的了解可婶,不論是 Android 下的 ConstraintLayout沿癞,還是 Flutter ConstraintLayout援雇,它們都和 AutoLayout 有很大的不同。
約束布局的本質(zhì)
任何布局控件的核心都在于計算子控件的大小和位置椎扬。而約束布局的核心在于使用約束來計算子控件的大小和位置惫搏,而約束的本質(zhì)是對齊。即指定子控件的上蚕涤、下筐赔、左、右揖铜、基線分別和哪些子控件(或 parent)的哪些位置去對齊茴丰。這直接決定了子控件的位置,并可能決定子控件的大刑煜拧(如果子控件大小指定為 matchConstraint 的話)贿肩。
用過約束布局的人幾乎都回不去了,主要是因為它有兩大殺手锏龄寞,一是它能讓布局層次更加扁平化汰规,這樣能帶來渲染性能的提升。二是強大靈活的布局(排版)能力能讓我們更快的開發(fā) UI物邑。
我認為后者更重要溜哮,因為這種布局能力是一種所見即所寫的能力,什么意思呢色解?意思是說設計同事在做設計的時候不會去考慮技術實現(xiàn)的細節(jié)茂嗓,他們只關心哪個元素在哪個元素的下面,哪個元素在哪個元素的右邊等等科阎。對于他們來講述吸,沒有嵌套這種說法。他們認為所有的元素都在一個平面內(nèi)萧恕,他們做設計的時候只是在一個平面內(nèi)去拖動刚梭、調(diào)整一些元素而已肠阱。
然而對于開發(fā)者來講,事情就不一樣了朴读。
當不使用約束布局時屹徘,開發(fā)者必須要考慮實現(xiàn)方案,即怎么樣結(jié)合 Row衅金、Column噪伊、Stack 或自定義控件去達到設計師的效果。這個考慮的過程直接拖慢了開發(fā)進度氮唯。此外對于某些場景鉴吹,開發(fā)需要的工作量也會進一步拖慢開發(fā)進度,而這些場景使用約束布局可能是分分鐘就能搞定的事惩琉。
當使用約束布局時豆励。所有的子元素都在一個平面內(nèi)。我們幾乎可以完全遵照視覺稿來布局瞒渠,甚至不需要思考良蒸。哪個元素在哪個元素的下面,我們就把它約束在哪個元素的下面伍玖。哪個元素在哪個元素的右邊嫩痰,我們就把它約束在哪個元素的右邊。這會大大地提高開發(fā)效率窍箍。
約束布局會在性能串纺、靈活性、開發(fā)速度椰棘、可維護性方面全面超越傳統(tǒng)嵌套寫法纺棺,不信你可以試試。
基本用法
在 Flutter 中晰搀,父元素想給子元素施加布局參數(shù)五辽,標準的做法是使用 ParentDataWidget 將子元素包起來。例如 Stack 中的子元素有時要用 Positioned 包住以定位它們外恕。ParentDataWidget 機制就跟 Android 中的 LayoutParams 是一個意思杆逗,它的原理很簡單,本文不做過多介紹鳞疲,我后面會再寫一篇文章深入剖析它(連同 ParentData 機制)罪郊。總之想給子元素施加 LayoutParams尚洽,就用 ParentDataWidget 包裹它悔橄,不同的布局會提供不同類型的 ParentDataWidget。Flutter ConstraintLayout 也提供了 ParentDataWidget,名為 Constrained癣疟。用法如下:
ConstraintLayout(
children: [
Constrained(
child: Container(
color: Colors.yellow,
),
constraint: Constraint(
size: matchParent,
),
)
],
),
所有給子元素施加的約束都存儲在 Constraint 中挣柬。除了內(nèi)置的 Helper Widget(Guideline、Barrier)以外睛挚,所有其他 Widget 都需要使用 Constrained 包裹邪蛔。當然這顯得有點麻煩,因此我提供了基于擴展函數(shù)的簡便寫法扎狱,并推薦大家這么寫:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
centerTo: parent,
)
平均下來 applyConstraint 中一般只需要兩行代碼侧到,一行指定元素大小,一行指定元素位置淤击。
基本約束(對齊屬性)
Flutter ConstraintLayout 提供了兩套約束系統(tǒng)匠抗,一套是基本約束,一套是圖釘定位(Pinned Position)污抬。所有的基本約束如下:
- left
- toLeft
- toCenter(默認偏移量為 0.5汞贸,代表中心)
- toRight
- right
- toLeft
- toCenter(默認偏移量為 0.5,代表中心)
- toRight
- top
- toTop
- toCenter(默認偏移量為 0.5,代表中心)
- toBottom
- bottom
- toTop
- toCenter(默認偏移量為 0.5,代表中心)
- toBottom
- baseline
- toTop
- toCenter(默認偏移量為 0.5恼布,代表中心)
- toBaseline
- toBottom
示例:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
left: parent.left,
top: title.bottom,
)
這些約束具有自解釋能力攒射,不做過多介紹。它們起到的作用是讓子元素的哪個位置去和其它子元素(或 parent)的哪個位置去對齊猎唁。
相比 Android 這里額外多了 toCenter:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
left: parent.center,
right: parent.center.bias(0.8),
)
center 可以指定偏移量咒劲,默認為 0.5,為 0 時效果和 left 等同诫隅,為 1 時效果和 right 等同腐魂。
任何子元素在橫向和縱向都必須至少有一個約束。這樣才能定位它們逐纬。如果你在某一方向施加了兩個約束蛔屹,那起到的效果就是居中。
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
left: parent.left,
right: parent.right,
top: parent.center,
)
如果你把左右或上下都約束到同一個位置兔毒,那子元素就會相對于這個位置居中。
Container(
color: Colors.yellow,
).applyConstraint(
id: anchor,
size: 200,
centerTo: parent,
),
Container(
color: Colors.green,
).applyConstraint(
size: 100,
left: anchor.right,
right: anchor.right,
top: anchor.bottom,
)
值得注意的是育叁,如果將子元素的寬或高設置為 matchParent,則不能再設置基本約束芍殖。因為內(nèi)部會自動設置:
if (width == matchParent) {
left = parent.left;
right = parent.right;
}
if (height == matchParent) {
top = parent.top;
bottom = parent.bottom;
baseline = null;
}
子元素大小設置
有 3 個屬性來設置子元素的大小豪嗽,分別是 width、height、size龟梦。它們可以取值為:
- matchParent: 撐滿父布局
- wrapContent(默認值): 自己有多大就撐多大隐锭,可設置最小或最大大小
- matchConstraint: 大小由約束決定
- fixed_size (>= 0): 固定大小
當 width 和 height 相同時,給 size 賦值就行了计贰,這樣能少寫一行代碼成榜。內(nèi)部會做如下轉(zhuǎn)換:
if (size != null) {
width = size!;
height = size!;
}
每一種取值需要的約束數(shù)量是不同的,比如取值為 matchConstraint 時蹦玫,某一方向上必須要設置 2 個約束(完整約束)赎婚,相關的規(guī)則如下:
int getMinimalConstraintCount(double size) {
if (size == matchParent) {
return 0; // 不能再設置約束
} else if (size == wrapContent || size >= 0) {
return 1; // 至少要設置 1 個約束
} else {
return 2; // matchConstraint,必須要設置 2 個約束
}
}
當子元素的寬或高為 wrapContent(默認) 時樱溉,可使用 minWidth挣输、maxWidth、minHeight福贞、maxHeight 設置最小撩嚼、最大寬高。默認的最小值為 0挖帘,最大值為 matchParent完丽。即默認情況下子元素的寬高不能超過 parent 的寬高,你可以賦其它值來突破這個限制拇舀。
id 與相對 id
如果子元素需要和某個子元素的某個位置對齊逻族,可以給后者指定一個 id。先聲明骄崩,再賦值和引用:
// 聲明
ConstraintId anchor = ConstraintId('anchor');
Container(
color: Colors.yellow,
).applyConstraint(
id: anchor, // 賦值
);
Container(
color: Colors.green,
).applyConstraint(
left: anchor.right, // 引用
top: anchor.bottom, // 引用
);
這里需要保證字符串的唯一性聘鳞。一般將 id 聲明為 StatelessWidget 或 State 的成員變量,但也可即時聲明:
Container(
color: Colors.yellow,
).applyConstraint(
id: cId('yellowArea'),
size: 200,
centerTo: parent,
),
Container(
color: Colors.green,
).applyConstraint(
size: 100,
topLeftTo: cId('yellowArea'),
)
這里的 id 都是絕對 id要拂,與之對應的是相對 id:
- rId(3) 代表第三個子元素抠璃,以此類推
- rId(-1) 代表最后一個子元素
- rId(-2) 代表倒數(shù)第二個子元素,以此類推
- sId(-1) 代表上一個兄弟元素脱惰,以此類推
- sId(1) 代表下一個兄弟元素搏嗡,以此類推
比如上例可以改造為:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
centerTo: parent,
),
Container(
color: Colors.green,
).applyConstraint(
size: 100,
topLeftTo: rId(0), // 引用第 0 個子元素
)
或
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
centerTo: parent,
),
Container(
color: Colors.green,
).applyConstraint(
size: 100,
topLeftTo: sId(-1), // 引用上一個兄弟元素
)
相對 id 主要是為懶癌患者設計的,因為命名是個麻煩事拉一。如果已經(jīng)為子元素定義了絕對 id采盒,則不能再使用相對 id 來引用他們。
包裝約束
包裝約束是為了簡化使用而設計的舅踪,顧名思義它是對基本約束的包裝纽甘,它在運行時會轉(zhuǎn)化為基本約束,比如以下代碼:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
topLeftTo: parent,
),
等價于:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
left: parent.left,
top: parent.top,
),
再比如:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
centerTo: parent,
)
等價于:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
left: parent.left,
top: parent.top,
right: parent.right,
bottom: parent.bottom,
)
一共有 27 個包裝約束抽碌,分別是:
- topLeftTo
- topCenterTo
- topRightTo
- centerLeftTo
- centerTo
- centerRightTo
- bottomLeftTo
- bottomCenterTo
- bottomRightTo
- centerHorizontalTo
- centerVerticalTo
- outTopLeftTo
- outTopCenterTo
- outTopRightTo
- outCenterLeftTo
- outCenterRightTo
- outBottomLeftTo
- outBottomCenterTo
- outBottomRightTo
- centerTopLeftTo
- centerTopCenterTo
- centerTopRightTo
- centerCenterLeftTo
- centerCenterRightTo
- centerBottomLeftTo
- centerBottomCenterTo
- centerBottomRightTo
其中一部分是自解釋的悍赢,另一部分可參考下圖:
或者進入 Flutter ConstraintLayout 在線示例(https://constraintlayout.flutterfirst.cn)去查看决瞳。
clickPadding
快速擴大子元素的點擊區(qū)域而無需改變子元素的實際大小。這意味著你可以完全遵照視覺稿來布局左权,而不用為了考慮點擊區(qū)域而做額外的事情皮胡,這會提升一定的開發(fā)效率。用法如下:
Container(
color: Colors.red,
).applyConstraint(
size: 200,
centerTo: parent,
clickPadding: const EdgeInsets.all(10), // 四周都擴大 10 dp赏迟,每個邊都可分別擴大
)
深色區(qū)域為子元素的實際大小屡贺,淺色區(qū)域為擴大后的點擊區(qū)域。淺色區(qū)域內(nèi)的觸摸事件會映射到深色區(qū)域锌杀。
這也意味著子元素之間可以在不增加嵌套的情況下共享點擊區(qū)域甩栈,有時可能需要結(jié)合 e-index(后面會講到) 使用。
可見性控制
visibility 屬性用來控制子元素的可見性糕再,可取值為 visible量没、invisible、gone突想。這個其實沒什么可講的殴蹄,大家都懂。對于 gone 來講猾担,有時可能更好的辦法是使用條件表達式來阻止 Widget 的創(chuàng)建袭灯。用 gone 的好處是可以保留狀態(tài)(如果該 Widget 是 StatefulWidget 的話)。
有一點值得一提的是在 Flutter 里 RenderObject 任何時候都必須被 layout绑嘹,但可以不 paint稽荧。因此 gone 的 Widget 也會被 layout,只不過它會縮小成一個點圾叼。依賴它的其他 Widget 的 goneMargin 會生效蛤克。gone 和 invisible 的 Widget 都不會被 paint。
margin
有三個屬性來設置 margin夷蚊,分別是 margin、goneMargin髓介、percentageMargin惕鼓。
margin 和 goneMargin 都可以為負數(shù),使用方法為:
margin: const EdgeInsets.only(left: 10),
當依賴的元素的可見性為 gone 或者其某一邊的實際大小為 0 時唐础,goneMargin 就會生效箱歧,否則 margin 會生效,即便其自身的可見性為 gone一膨。也就是說呀邢,當元素自身可見性為 gone 時,它自身的 margin 仍然會生效豹绪,因為它被 layout 了价淌。這和 Android 是不同的。
percentageMargin 是為了支持 Guideline 而開發(fā)的一個內(nèi)部功能,現(xiàn)把它暴露出來蝉衣,興許對你有用括尸。其默認為 false,如果設置為 true病毡,則 margin濒翻、goneMargin 的值只能在 [-1, 1] 的范圍內(nèi)±材ぃ基準是 parent 的寬或高有送。
zIndex
搞過前端的應該都知道這個屬性,它即視圖層級僧家,值越大子元素就越顯示在上層雀摘。默認值是子元素的下標。一般來講啸臀,越顯示在上層就越先接收點擊事件届宠。
如果兩個子元素的 zIndex 相同,則下標越大乘粒,越顯示在上層豌注。
translate
當需要對子元素進行平移時,除了可以使用 Flutter 自帶的 Transform Widget灯萍,還可以使用約束布局內(nèi)置的平移功能:
Container(
color: Colors.yellow,
).applyConstraint(
size: 200,
centerTo: parent,
id: anchor,
),
Container(
color: Colors.green,
).applyConstraint(
size: 100,
topLeftTo: anchor,
translate: const Offset(100, 100),
)
默認情況下轧铁,平移只會移動自身,那些依賴自己的元素不會跟著被平移旦棉,如果也想讓他們跟著移動齿风,請將 translateConstraint 設置為 true。
和 margin 一樣绑洛,平移也支持 percentageTranslate救斑,但基準是自身的寬或高。
百分比布局
當大小被設置為 matchConstraint 時真屯,就會啟用百分比布局脸候,默認的百分比是 1(100%)。相關的屬性是 widthPercent绑蔫,heightPercent运沦,widthPercentageAnchor,heightPercentageAnchor配深。
PercentageAnchor 的取值有兩種携添,parent 和 constraint(默認),當取值為前者時篓叶,代表以 parent 的寬或高為基準烈掠,因此只需要在對應方向添加一個約束即可羞秤。當取值為后者時,代表以約束的區(qū)間寬度為基準向叉,因此需要在對應的方向添加兩個約束(完整約束)锥腻。示例代碼如下:
Container(
color: Colors.redAccent,
alignment: Alignment.center,
child: const Text('width: 50% of parent\nheight: 200'),
).applyConstraint(
width: matchConstraint,
height: 200,
widthPercent: 0.5,
bottomCenterTo: parent,
),
Container(
color: Colors.blue,
alignment: Alignment.center,
child: const Text('width: 300\nheight: 30% of parent'),
).applyConstraint(
width: 300,
height: matchConstraint,
heightPercent: 0.3,
topLeftTo: parent,
heightPercentageAnchor: PercentageAnchor.parent,
)
偏移
當某一方向有兩個約束時(完整約束),可使用 horizontalBias母谎、verticalBias 調(diào)整偏移量瘦黑,范圍為 [0, 1],默認值為 0.5奇唤,代表居中幸斥。比如給上例中的紅色區(qū)域添加 horizontalBias: 0 時,它就跑到最左邊了咬扇。
布局回調(diào)
如果你想做一些布局甲葬、繪制監(jiān)聽,那就使用 layoutCallback懈贺、paintCallback 吧经窖,它們的定義如下:
typedef OnLayoutCallback = void Function(RenderBox renderBox, Rect rect);
typedef OnPaintCallback = void Function(RenderBox renderBox, Offset offset,
Size size, Offset? anchorPoint, double? angle);
這兩個回調(diào)是施加給局部的某個子元素的,而不是全局的 ConstraintLayout梭灿。
等比例布局
當需要等比例布局時画侣,除了可以使用 Flutter 自帶的 FractionallySizedBox,還可以使用約束布局提供的等比例布局功能堡妒。相關的屬性是:
- widthHeightRatio: 1 / 3,
- ratioBaseOnWidth: true, (默認值是 null配乱,代表自動推斷,未確定邊的大小會根據(jù)確定邊的大小和 widthHeightRatio
計算出來皮迟。未確定邊的大小必須設置為 matchConstraint搬泥,確定邊的大小可以為 matchParent,固定大蟹帷(>=0)忿檩,matchConstraint)
示例如下:
Container(
color: Colors.yellow,
).applyConstraint(
width: 200,
height: matchConstraint,
widthHeightRatio: 2 / 1,
centerTo: parent,
)
請不要把百分比布局和等比例布局搞混了,前者是寬高由外部決定爆阶,后者是寬高由自身決定休溶。
eIndex
eIndex 是事件分發(fā)順序,它的默認值是 zIndex扰她。一般很少用到它。比如以下場景就需要用到它:
圖片中的 ListView item 布局追求了一層嵌套芭碍,白色圓角區(qū)域(一個 Container)和其他元素是平級的且位于最底層(zIndex 最型揭邸),但點擊整個 item 需要跳轉(zhuǎn)新頁面窖壕。因此這里把點擊事件設置到 Container忧勿,并讓它的 eIndex 變大(比如賦值 1000)杉女。這樣就能在不增加嵌套的情況下整體響應事件了。
但這種追求一層嵌套的寫法并不是在所有情況下都適用鸳吸,比如按下 ListView item 時 item 內(nèi)的背景熏挎、文本要變色,就必須得增加嵌套了晌砾。
柵欄(屏障)Barrier
搞過 Android 的都知道這個坎拐,它和 Android 中的柵欄完全一樣。目的是為了在幾個子元素之間生成一條虛擬的屏障养匈,然后別的元素可以相對于這個屏障去布局哼勇,示例如下:
Container(
color: const Color(0xFF005BBB),
).applyConstraint(
id: leftChild,
size: 200,
topLeftTo: parent,
),
Container(
color: const Color(0xFFFFD500),
).applyConstraint(
id: rightChild,
width: 200,
height: matchConstraint,
centerRightTo: parent,
heightPercent: 0.5,
verticalBias: 0,
),
Barrier(
id: barrier,
direction: BarrierDirection.bottom, // 方向
referencedIds: [leftChild, rightChild], // 引用的子元素的 id,此處的 id 不能為相對 id
),
const Text(
'Align to barrier',
style: TextStyle(
fontSize: 40,
color: Colors.blue,
),
).applyConstraint(
centerHorizontalTo: parent,
top: barrier.bottom,
)
引導線 Guideline
這個也和 Android 中的 Guideline 完全一樣呕乎。示例如下:
Container(
color: const Color(0xFF005BBB),
).applyConstraint(
width: matchParent,
height: matchConstraint,
top: parent.top,
bottom: guideline.top,
),
Guideline(
id: guideline,
horizontal: true, // 方向积担,true 為水平,false 為垂直
guidelinePercent: 0.5,
),
Container(
color: const Color(0xFFFFD500),
).applyConstraint(
width: matchParent,
height: matchConstraint,
top: guideline.bottom,
bottom: parent.bottom,
),
const Text(
'Align to Guideline',
style: TextStyle(
fontSize: 40,
color: Colors.white,
),
textAlign: TextAlign.center,
).applyConstraint(
centerHorizontalTo: parent,
bottom: guideline.bottom,
)
Guideline 有四個屬性可以設置猬仁,分別是 horizontal帝璧、guidelinePercent、guidelineBegin湿刽、guidelineEnd的烁。后三個屬性都是相對于 parent 而言。
圖釘定位
Flutter ConstraintLayout 提供了兩套約束系統(tǒng)叭爱,一套是基本約束撮躁,一套是圖釘定位(Pinned Position)。提供圖釘定位主要是為了讓布局更靈活一些买雾。設想一下把曼,要想定位一個元素,除了給它指定橫向漓穿、縱向?qū)R到哪里以外嗤军,我認為還有一種辦法是讓它的哪個位置釘在哪里。把一個東西釘在哪里晃危,從邏輯上來講會產(chǎn)生兩個孔叙赚,一個孔穿過元素自身,一個孔穿過目標位置僚饭。因此圖釘定位的 API 主要是用來描述兩個孔的位置震叮,示例如下:
Container(
color: Colors.yellow,
).applyConstraint(
id: anchor,
size: 200,
centerTo: parent,
),
Container(
color: Colors.cyan,
).applyConstraint(
size: 100,
pinnedInfo: PinnedInfo(
anchor,
Anchor(0.2, AnchorType.percent, 0.2, AnchorType.percent),
Anchor(1, AnchorType.percent, 1, AnchorType.percent),
),
),
Container(
decoration: const BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
).applyConstraint(
size: 10,
centerBottomRightTo: anchor,
)
例子中的意思是青色區(qū)域橫豎 20% 的點釘在黃色區(qū)域的右下角,紅點即為孔的位置鳍鸵。PinnedInfo 類完整的構(gòu)造函數(shù)如下:
PinnedInfo(
this.targetId,
this.selfAnchor,
this.targetAnchor, {
this.angle = 0.0,
});
targetId 和 targetAncnor 描述了目標孔的位置苇瓣,selfAnchor 描述了自身孔的位置。一個物體被圖釘釘住后偿乖,它就有了個轉(zhuǎn)軸击罪,就能旋轉(zhuǎn)起來哲嘲,因此 angle 代表旋轉(zhuǎn)的角度,范圍為 [0.0, 360.0]媳禁。
基本約束和圖釘定位兩套約束系統(tǒng)是互斥的眠副,只能用其一。如果你使用基本約束時也想讓元素轉(zhuǎn)起來竣稽,可以使用 Pinned Translate:
translate: PinnedTranslate(PinnedInfo(
null,
Anchor(0.2, AnchorType.percent, 0.2, AnchorType.percent),
null,
angle: 90,
))
如果不設置目標孔的位置囱怕,則相對于自身孔的位置旋轉(zhuǎn)。
隨意定位(Arbitrary Position)
盡管 ConstraintLayout 的布局能力已經(jīng)很靈活了丧枪,但我還想更進一步光涂,讓你能夠自定義!因此我暴露了布局的接口給你:
typedef CalcSizeCallback = BoxConstraints Function(
RenderBox parent, List<ConstrainedNode> anchors);
typedef CalcOffsetCallback = Offset Function(
RenderBox parent, ConstrainedNode self, List<ConstrainedNode> anchors);
使用方法如下:
Container(
color: Colors.orange,
).applyConstraint(
size: matchConstraint, // 寬高必須設置為 matchConstraint
anchors: [sId(-1)], // 依賴的元素拧烦,只有依賴的元素都布局好了忘闻,才會調(diào)用 callback
calcSizeCallback: (parent, anchors) {
// 動態(tài)返回子元素的大小
},
calcOffsetCallback: (parent, self, anchors) {
// 動態(tài)返回子元素的 Offset
},
)
具體可參考在線示例×挡基于此齐佳,你幾乎可以為所欲為。
約束與 Widget 分離
約束和布局其實是可以分離的债沮,這個特性借鑒了 Compose 的約束布局炼吴。
@override
Widget build(BuildContext context) {
return Scaffold(
body: ConstraintLayout(
childConstraints: [
Constraint(
id: cId('title'),
size: 200,
centerTo: parent,
),
],
children: [
Container(
color: Colors.red,
).applyConstraintId(
id: cId('title'),
),
],
),
);
}
分離后,你就可以動態(tài)地改變一個子元素的約束了疫衩。此外硅蹦,可以在 childConstraints 中聲明 Helper Widgets(Guideline、Barrier)闷煤,這樣可以避免創(chuàng)建 RenderObject童芹。具體請看下文中的性能優(yōu)化部分。
Flutter ConstraintLayout 提供了兩個 ParentDataWidget鲤拿,分別是 Constrained 和 UnConstrained假褪。applyConstraint 是對 Constrained 的包裝,applyConstraintId 是對 UnConstrained 的包裝近顷。前者聲明完整的約束信息生音,后者只聲明子元素和約束的對應關系。
布局調(diào)試
Flutter ConstraintLayout 提供了布局調(diào)試的功能窒升,提供了以下的調(diào)試開關:
- showHelperWidgets:輔助 Widget 包含 Guideline 和 Barrier缀遍,默認情況下它們是不可見的,可開啟此開關讓它們可見
- showClickArea:當使用 clickPadding 擴大點擊區(qū)域時饱须,可開啟此開關查看實際的點擊區(qū)域
- showZIndex:可開啟此開關查看各子元素的 z-Index
- showChildDepth:在后面的原理分析文章中再作介紹
- debugPrintConstraints:在后面的原理分析文章中再作介紹
- showLayoutPerformanceOverlay:開啟此開關后瑟由,會將每一幀的 layout、paint 耗時繪制出來,單位為微秒(us歹苦,1ms 等于 1000us)
自身大小設置
Flutter ConstraintLayout 默認會撐滿父布局,但你也可以自定義它的大小督怜,我提供了 width殴瘦、height、size 三個屬性來設置約束布局自身的大泻鸥堋:
// fixed size蚪腋、matchParent(默認值)、wrapContent
final double width;
final double height;
/// When size is non-null, both width and height are set to size
final double? size;
開放式語法(Open Grammar)
開放式語法是一個比較大的創(chuàng)新姨蟋,有了它你可以使用任何 Dart 的語法來組織子元素屉凯,而不僅僅局限于集合中的 if和集合中的 for這種簡單表達式。示例如下:
@override
Widget build(BuildContext context) {
return Scaffold(
body: ConstraintLayout().open(() {
int i = 0;
while (i < 100) {
Text("$i").applyConstraint(
left: parent.left,
top: i == 0 ? parent.top : sId(-1).bottom,
);
i++;
}
}),
);
你需要調(diào)用 open 擴展函數(shù)眼溶,就可以在函數(shù)的作用域中使用任何語法組織子元素悠砚。上面的幾行簡單的代碼就構(gòu)造了一個列表:
約束提示
當前的版本有完善的約束缺失、非法堂飞、冗余提示灌旧。一旦約束有問題,要么會觸發(fā) assert 錯誤绰筛,要么會直接拋出異常枢泰。由于 Flutter 中異常并不會導致程序崩潰,因此即便拋出異常后也無法中斷后續(xù)的布局铝噩、繪制流程衡蚂,而在這個階段會觸發(fā)更多的異常。因此當你發(fā)現(xiàn)布局展示不符合預期時骏庸,大概率是內(nèi)部拋異常了或 assert 出錯了毛甲。你要去看看異常日志,一般翻到最頂部才能看到根本原因敞恋。
瀑布流丽啡、網(wǎng)格、列表
Flutter ConstraintLayout 可以當成一個比較通用的布局平臺硬猫,你只需要生成約束补箍,把布局、繪制交給它就好啸蜜。我把這個稱作擴展坑雅。瀑布流、網(wǎng)格衬横、列表就是以擴展的形式提供的裹粤,具體請參考在線示例吧。比如上例中的列表就是個擴展蜂林,它生成約束充當了 Column 的能力遥诉。
圓形定位
和 Android 的約束布局一樣拇泣,F(xiàn)lutter ConstraintLayout 也提供了圓形定位。但兩者的實現(xiàn)方式截然不同矮锈,后者并沒有做特殊的支持霉翔,只是增加了一個 Util 函數(shù):
/// For circle position
Offset circleTranslate({
required double radius,
/// [0.0,360.0]
required double angle,
}) {
assert(radius >= 0 && radius != double.infinity);
double xTranslate = sin((angle / 180) * pi) * radius;
double yTranslate = -cos((angle / 180) * pi) * radius;
return Offset(xTranslate, yTranslate);
}
并配合包裝約束 centerTo 一起使用,示例如下:
for (int i = 0; i < 12; i++)
Text(
'${i + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 25,
),
).applyConstraint(
centerTo: rId(0),
translate: circleTranslate(
radius: 205,
angle: (i + 1) * 30,
),
)
性能優(yōu)化
1.當布局復雜時苞笨,如果子元素需要頻繁重繪债朵,可以考慮使用 RepaintBoundary。當然合成 Layer 也有開銷瀑凝,所以需要合理使用序芦。
class OffPaintExample extends StatelessWidget {
const OffPaintExample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ConstraintLayout(
children: [
Container(
color: Colors.orangeAccent,
).offPaint().applyConstraint(
width: 200,
height: 200,
topRightTo: parent,
)
],
),
),
);
}
}
此處的 offPaint 擴展方法是對 RepaintBoundary 的簡單封裝。
2.盡量使用 const Widget粤咪。如果你沒法將子元素聲明為 const 而它自身又不會改變谚中。可以使用內(nèi)置的 OffBuildWidget 來避免子元素重復 build射窒。
class OffBuildExample extends StatelessWidget {
const OffBuildExample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ConstraintLayout(
children: [
// 子樹不會改變
Container(
color: Colors.orangeAccent,
).offBuild(id: 'id').applyConstraint(
width: 200,
height: 200,
topRightTo: parent,
)
],
),
),
);
}
}
3.子元素會自動成為 RelayoutBoundary 除非它的寬或高是 wrapContent藏杖。可以酌情的減少 wrapContent 的使用脉顿,因為當 ConstraintLayout
自身的大小發(fā)生變化時(通常是窗口大小發(fā)生變化蝌麸,移動端幾乎不存在此類情況),所有寬或高為 wrapContent
的子元素都會被重新布局艾疟。而其他元素由于傳遞給它們的約束未發(fā)生變化来吩,不會觸發(fā)真正的布局。
4.如果你在 children 列表中使用 Guideline 或 Barrier蔽莱, Element 和 RenderObject 將不可避免的被創(chuàng)建弟疆,它們會被布局但不會繪制。此時你可以使用
GuidelineDefine 或 BarrierDefine 來優(yōu)化盗冷, Element 和 RenderObject 就不會再創(chuàng)建了怠苔。
class BarrierExample extends StatelessWidget {
const BarrierExample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
ConstraintId leftChild = ConstraintId('leftChild');
ConstraintId rightChild = ConstraintId('rightChild');
ConstraintId barrier = ConstraintId('barrier');
return Scaffold(
body: ConstraintLayout(
childConstraints: [
BarrierDefine(
id: barrier,
direction: BarrierDirection.bottom,
referencedIds: [leftChild, rightChild],
),
],
children: [
Container(
color: const Color(0xFF005BBB),
).applyConstraint(
id: leftChild,
width: 200,
height: 200,
topLeftTo: parent,
),
Container(
color: const Color(0xFFFFD500),
).applyConstraint(
id: rightChild,
width: 200,
height: matchConstraint,
centerRightTo: parent,
heightPercent: 0.5,
verticalBias: 0,
),
const Text(
'Align to barrier',
style: TextStyle(
fontSize: 40,
color: Colors.blue,
),
).applyConstraint(
centerHorizontalTo: parent,
top: barrier.bottom,
)
],
),
);
}
}
5.每一幀,ConstraintLayout 會比對參數(shù)并決定以下事情:
- 是否需要重新計算約束仪糖?
- 是否需要重新布局柑司?
- 是否需要重新繪制?
- 是否需要重排繪制順序锅劝?
- 是否需要重排事件分發(fā)順序攒驰?
這些比對不會成為性能瓶頸,但會提高 CPU 占用率故爵。如果你對 ConstraintLayout 內(nèi)部原理足夠了解(后面會寫一篇原理分析的文章)玻粪,你可以使用 ConstraintLayoutController 來手動觸發(fā)這些操作,停止參數(shù)比對。
class ConstraintControllerExampleState extends State<ConstraintControllerExample> {
double x = 0;
double y = 0;
ConstraintLayoutController controller = ConstraintLayoutController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(
title: 'Constraint Controller',
codePath: 'example/constraint_controller.dart',
),
body: ConstraintLayout(
controller: controller,
children: [
GestureDetector(
child: Container(
color: Colors.pink,
alignment: Alignment.center,
child: const Text('box draggable'),
),
onPanUpdate: (details) {
setState(() {
x += details.delta.dx;
y += details.delta.dy;
controller.markNeedsPaint();
});
},
).applyConstraint(
size: 200,
centerTo: parent,
translate: Offset(x, y),
)
],
),
);
}
}
結(jié)束語
好了劲室,以上就是 Flutter ConstraintLayout(約束布局)的所有功能介紹伦仍,趕緊收藏起來吧。
https://github.com/hackware1993/Flutter_ConstraintLayout
我是 hackware痹籍,關注我的公眾號:FlutterFirst呢铆,一起成長!