Flutter 佈局元件——用 Align 對齊你的元件

語言: CN / TW / HK

theme: vuepress highlight: darcula


我們經常有對齊的訴求,比如一個元件在一個大元件的左上角,右下角,中間顯示等等,Container 中可以設定 alignment 屬性來實現。Flutter 也有專門的佈局元件來實現同樣的效果,那就是 Align。

這一篇文章,我就全方位介紹一下 Align 元件,從南到北(基本使用,原理)。

Align 介紹.png

Align 元件介紹

Align 元件的功能主要有兩個:對齊其內部的子節點基於子節點的大小調整自己的大小

效果是這樣的:

align (1).gif

我們舉個例子,你想要讓子節點顯示在右下角,那麼可以設定屬性為 Alignment.bottomRight,但是有個前提需要 Align 本身的尺寸大於子節點。

對於對齊來說,最起碼外圈尺寸是要大於子節點的大小,兩個要是一樣大,肯定就重疊了呀。我們看外圈也就是 Align 的尺寸,預設是多少呢?會盡可能大的,父節點給 Align 的約束是多少,那麼 Align 就取約束的最大值。

當然了也有非預設的規則:

如果沒有約束並且 widthFactorheightFactor 也沒設定,那麼 Align 的大小就是子節點的大小。

如果 widthFactorheightFactor 設定了,那麼 Align 的大小就是子節點的大小與因子的運算值。比如 widthFactor 是 2.0,那麼 Align 的寬度就是子節點寬度的兩倍。

現在我們知道了 Alin 是啥,下面我們看 Align 的屬性。

Align 的屬性

| 屬性 | 型別 | 作用 | | --- | --- | --- | | key | Key? | 元件的標示 | | alignment | AlignmentGeometry | 子節點的對齊方法,一般設定為 Alignment | | widthFactor | double? | 寬度因子,Align 的寬度 = 子節點的寬度 * 寬度因子 | | heightFactor | double? | 高度因子,Align 的高度 = 子節點的高度 * 高度因子 | | child | Widget? | 待對齊的子節點 |

alignment 對齊

這個屬性用來控制對齊,雖然型別是 AlignmentGeometry 但是我們常用 Alignment 來賦值。

Alignment 是用構造方法的 x 和 y 來控制位置,範圍是 -1 到 1 。如果 x 的值是 -1 ,子節點放在 Align 的左邊,1 表示子節點會放在 Align 的右邊,同理 y 的 -1 表示子節點在 Align 的上邊,1 表示在下邊。我們在下面會介紹具體的計算過程。

widthFactor 寬度因子

如果設定非 null,那麼 Align 的寬度就是 子節點的寬度與因子的乘積,這個值不能是負數。

heightFactor 高度因子

如果設定非 null,那麼 Align 的高度就是 子節點的高度與因子的乘積,這個值不能是負數。

知道了這些屬性,下面我們使用效果。

Align 使用

Align 的外圈是黑色邊框的 Container,子節點是 60*60 的藍色色塊

基本使用

基礎程式碼如下:

dart Container( width: 300, height: 300, decoration: BoxDecoration(border: Border.all(color: Colors.black)), child: Align( child: Container( height: 60, width: 60, color: Colors.blue, ), ), )

企業微信截圖_a2e7fd70-2a42-4a85-8cb4-14a088022558.png

Align 的佈局預設居中,可以調整其 aligment 屬性

Align 擺放原理

老規矩 👉三棵樹最終章Align 是渲染型元件,它的渲染物件是 RenderPositionedBox,所以我們去 RenderPositionedBox 看擺放的原理。

ParentData 父節點需要知道的資料

在介紹擺放之前,我們先介紹一個概念 ParentData ,就是父渲染物件想要知道的資料。 比如位元組點的位置等等, 對應到程式碼中就是 RenderObject 的 parentData 屬性。

RenderObject 的 ParentData 分為兩大類:盒子資料 和 Sliver 資料,我們以 ContainerParentDataMixin 為例,看看儲存的內容:

```dart mixin ContainerParentDataMixin on ParentData {

ChildType? previousSibling;

ChildType? nextSibling;

@override void detach() { super.detach(); } } `` 從這個資料 model 中,父節點可以知道某個子節點的前後節點是誰。 ChildType 是繼承自RenderObject` 的範型。

image.png

我們熟知的 Row/Column、Stack、Wrap 等元件對應的渲染物件持有的 parentData 都是 ContainerParentDataMixin 的子型別。所以它們的渲染物件在佈局的時候,才可以依次佈局子節點。

RenderPositionedBoxparentDataBoxParentData。BoxParentData 的定義如 下:

```dart class BoxParentData extends ParentData { /// The offset at which to paint the child in the parent's coordinate system. Offset offset = Offset.zero;

@override String toString() => 'offset=$offset'; } ``offset就是Align位元組點在Align` 中的位置,看到這裡你就明白了,對齊就是這個值的計算。下面我們看計算的過程。

佈局過程

擺放就是佈局測量和繪製。分別對應著 performLayoutpaint

佈局過程如下: - 第一:確定佈局子節點,確定子節點的大小 - 第二:根據縮放因子和子節點的大小確定自己的大小 - 第三:根據對齊屬性確定子節點的位置 - 第四:根據位置進行繪製

下面我們從程式碼中來看具體的過程:

``` @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); //第一處 size = constraints.constrain(Size( shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity, shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity, )); //第二處 alignChild(); //第三處 } else { size = constraints.constrain(Size( shrinkWrapWidth ? 0.0 : double.infinity, shrinkWrapHeight ? 0.0 : double.infinity, )); } } `` **我們看第一處的程式碼**,第一處是佈局子節點,這個入參非常有意思:**constraints.loosen()** 和 **parentUsesSize`**。

constraints.loosen() 的作用是子節點的約束是寬鬆的:0 - Align的最大寬度,所以 Align 的子節點最大可用空間不會超過 Align,超過會怎麼辦呢?截斷! 不會出現 over 那樣的溢位提示。

parentUsesSize 的作用是告訴 framework,Align 的尺寸資訊依賴我的子節點,如果我的子節點標記為 dirty 了,請帶上我。

所以第一處的程式碼是確定子節點的尺寸,我們看第二處的程式碼,第二處是根據子節點的大小來確定自己的大小。我們以寬度為例:

| 標題 | 寬度因子不是null | 寬度因子是 null | | --- | --- | --- | | 約束無限 | true 子節點寬度 * 寬度因子 | true 子節點寬度 | | 約束有限 | true 子節點寬度 * 寬度因子 | false 寬度無限(最大寬度) |

挺有意思吧~,只要設定了尺寸因子,那麼 Align 的尺寸就是子節點的大小與尺寸因子的乘積了。

只要不設定,要麼就是子節點的寬度,要麼就是父佈局的寬度,這樣就實現了子節點在其爺爺節點中的對齊,Align 只是橋樑而已。

我們第三處,對齊子節點。

```dart @protected void alignChild() { _resolve(); final BoxParentData childParentData = child!.parentData! as BoxParentData; childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset); }

Offset alongOffset(Offset other) { final double centerX = other.dx / 2.0; final double centerY = other.dy / 2.0; return Offset(centerX + x * centerX, centerY + y * centerY); }

`` 對齊的就是確定了我們上面講到的確定offset,確定的方式就是alongOffset`。

我們可以暫時先想一下怎麼確定位置?

Flutter 的處理是先居中對齊,然後左減右加,加減的範圍就是 Alignment 構造方法中 x,y 的絕對值。

比如 Align 的寬度範圍是 0 —— 120, child 的寬度是 60。

所以居中的位置是 (120 - 60 )/ 2 = 30, child 的範圍就是 30 - 90。

我們在看左上角 Alignment topLeft = Alignment(-1.0, -1.0) 的計算過程。

首先,先確定居中的位置 30 \ 其次,確定寬度 centerX + x * centerX ,就是 30 + (-1)* 30 = 0 \ 最後,x 的座標就是 0

所以,才有文章開頭動畫中範圍是 -1 到 1,-1 代表最左邊和最上邊,1 代表最右邊和最下邊,其他的數值,在這個範圍浮動。

示意圖.png

這就是位置 offset 的計算過程,有了這個 offset,有什麼用呢?

按著這個位置繪製和響應手勢!!

繪製過程

RenderPositionedBox 並沒有繪製的 paint 方法,繪製方法在其父類 RenderShiftedBox 中。

dart @override void paint(PaintingContext context, Offset offset) { if (child != null) { final BoxParentData childParentData = child!.parentData! as BoxParentData; context.paintChild(child!, childParentData.offset + offset); } } 我們看到就是在 Align 的基礎上增加布過程中計算好的的 offset

比如上面佈局過程計算好的 offset 是 (20,20),那麼它的真實位置就是 Align 左上角的座標,向右向下偏移(20,20),偏移後的就是座標就是 Align 子節點真實的繪製座標。

手勢響應範圍

我們知道手勢是有測試範圍的,一般是元件的範圍內,也會增加是否落在了自己的元件上。如下: dart bool hitTest(BoxHitTestResult result, { required Offset position }) { if (_size!.contains(position)) { if (hitTestChildren(result, position: position) || hitTestSelf(position)) { result.add(BoxHitTestEntry(this, position)); return true; } } return false; } 上面是通用的處理方式,如果落點在自己的元件範圍內,會繼續判斷是否落在了子節點上 hitTestChildren,是否自己有額外的判斷 hitTestSelf

我們看 Align 渲染物件的 hitTestChildren

@override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { if (child != null) { final BoxParentData childParentData = child!.parentData! as BoxParentData; return result.addWithPaintOffset( offset: childParentData.offset,// 第一處 position: position, hitTest: (BoxHitTestResult result, Offset transformed) { assert(transformed == position - childParentData.offset); return child!.hitTest(result, position: transformed); }, ); } return false; } 我們看到在檢測自己的子節點是否滿足點選的時候,加了一個偏移轉換,轉換的座標就是佈局階段的 offset 。

通過上面的繪製和點選檢測,我們可以清晰的理解 ParentData 的作用,它攜帶的資料就是給 Align 元件用的。

總結

這一篇就結束啦,這是佈局元件的第一篇。介紹了 Align 的使用場景、基本屬性、基本使用。在這些的基礎上,探究了 Align 能夠實現對齊的原理,濃縮成一句話就是:先找到中間,在計算因子。我們使用的 Center 元件就是 Align 的子類,只是將對齊屬性設定為了居中而已。