Flutter ConstraintLayout(約束布局)完全指南

我會用三篇文章來講透 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)污抬。所有的基本約束如下:

  1. left
    1. toLeft
    2. toCenter(默認偏移量為 0.5汞贸,代表中心)
    3. toRight
  2. right
    1. toLeft
    2. toCenter(默認偏移量為 0.5,代表中心)
    3. toRight
  3. top
    1. toTop
    2. toCenter(默認偏移量為 0.5,代表中心)
    3. toBottom
  4. bottom
    1. toTop
    2. toCenter(默認偏移量為 0.5,代表中心)
    3. toBottom
  5. baseline
    1. toTop
    2. toCenter(默認偏移量為 0.5恼布,代表中心)
    3. toBaseline
    4. 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,
)
水平方向上居中豁生,頂部和 parent 的中心對齊

如果你把左右或上下都約束到同一個位置兔毒,那子元素就會相對于這個位置居中。

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,
)
水平方向和黃塊的右側(cè)對齊甸箱,頂部和黃塊的底部對齊

值得注意的是育叁,如果將子元素的寬或高設置為 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龟梦。它們可以取值為:

  1. matchParent: 撐滿父布局
  2. wrapContent(默認值): 自己有多大就撐多大隐锭,可設置最小或最大大小
  3. matchConstraint: 大小由約束決定
  4. 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要拂,與之對應的是相對 id:

  1. rId(3) 代表第三個子元素抠璃,以此類推
  2. rId(-1) 代表最后一個子元素
  3. rId(-2) 代表倒數(shù)第二個子元素,以此類推
  4. sId(-1) 代表上一個兄弟元素脱惰,以此類推
  5. 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 個包裝約束抽碌,分別是:

  1. topLeftTo
  2. topCenterTo
  3. topRightTo
  4. centerLeftTo
  5. centerTo
  6. centerRightTo
  7. bottomLeftTo
  8. bottomCenterTo
  9. bottomRightTo
  10. centerHorizontalTo
  11. centerVerticalTo
  12. outTopLeftTo
  13. outTopCenterTo
  14. outTopRightTo
  15. outCenterLeftTo
  16. outCenterRightTo
  17. outBottomLeftTo
  18. outBottomCenterTo
  19. outBottomRightTo
  20. centerTopLeftTo
  21. centerTopCenterTo
  22. centerTopRightTo
  23. centerCenterLeftTo
  24. centerCenterRightTo
  25. centerBottomLeftTo
  26. centerBottomCenterTo
  27. 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ū)域。淺色區(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,還可以使用約束布局提供的等比例布局功能堡妒。相關的屬性是:

  1. widthHeightRatio: 1 / 3,
  2. 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扰她。一般很少用到它。比如以下場景就需要用到它:

eIndex

圖片中的 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,
)
Barrier

引導線 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

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]媳禁。

旋轉(zhuǎn)

基本約束和圖釘定位兩套約束系統(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)試開關:

  1. showHelperWidgets:輔助 Widget 包含 Guideline 和 Barrier缀遍,默認情況下它們是不可見的,可開啟此開關讓它們可見
  2. showClickArea:當使用 clickPadding 擴大點擊區(qū)域時饱须,可開啟此開關查看實際的點擊區(qū)域
  3. showZIndex:可開啟此開關查看各子元素的 z-Index
  4. showChildDepth:在后面的原理分析文章中再作介紹
  5. debugPrintConstraints:在后面的原理分析文章中再作介紹
  6. 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ù)并決定以下事情:

  1. 是否需要重新計算約束仪糖?
  2. 是否需要重新布局柑司?
  3. 是否需要重新繪制?
  4. 是否需要重排繪制順序锅劝?
  5. 是否需要重排事件分發(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呢铆,一起成長!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蹲缠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悠垛,更是在濱河造成了極大的恐慌线定,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件确买,死亡現(xiàn)場離奇詭異斤讥,居然都是意外死亡,警方通過查閱死者的電腦和手機湾趾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門芭商,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人搀缠,你說我怎么就攤上這事铛楣。” “怎么了艺普?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵簸州,是天一觀的道長。 經(jīng)常有香客問我歧譬,道長岸浑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任瑰步,我火速辦了婚禮矢洲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缩焦。我一直安慰自己读虏,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布舌界。 她就那樣靜靜地躺著掘譬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呻拌。 梳的紋絲不亂的頭發(fā)上葱轩,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音,去河邊找鬼靴拱。 笑死垃喊,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的袜炕。 我是一名探鬼主播本谜,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼偎窘!你這毒婦竟也來了乌助?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤陌知,失蹤者是張志新(化名)和其女友劉穎他托,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仆葡,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡赏参,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了沿盅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片把篓。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖腰涧,靈堂內(nèi)的尸體忽然破棺而出韧掩,到底是詐尸還是另有隱情,我是刑警寧澤南窗,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布揍很,位于F島的核電站,受9級特大地震影響万伤,放射性物質(zhì)發(fā)生泄漏窒悔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一敌买、第九天 我趴在偏房一處隱蔽的房頂上張望简珠。 院中可真熱鬧,春花似錦虹钮、人聲如沸聋庵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祭玉。三九已至,卻和暖如春春畔,著一層夾襖步出監(jiān)牢的瞬間脱货,已是汗流浹背岛都。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留振峻,地道東北人臼疫。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像扣孟,于是被迫代替她去往敵國和親烫堤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容