flex 是 w3c 在 2009 年提出的響應式佈局,現在已經得到所有主流的瀏覽器支援,也是當下前端開發主流的佈局方式。
flex 憑藉其佈局屬性適配不同的螢幕,提高開發效率,減適配問題。在如此優秀的響應式能力下,隱藏了什麼設計和多少的複雜度,什麼樣的情況下會觸發多次排版。瞭解內部實現能更好的在合適的場景使用選擇性使用 flex,搭建更高效響應的頁面。
Flex 佈局屬性
基礎概念
主軸(main axis):容器根據 flex-direction 屬性確定的排版方向,即橫軸或豎軸
交叉軸(cross axis):與主軸垂直的排版方向,即橫軸或豎軸
容器屬性
display: flex
指定元素以 flex 方式佈局
flex-direction: row(預設)/ column / row-reverse / column-reverse
指定主軸的排版方向
- row(預設)橫向排版,起點在左端
- cloumn 豎向排版,起點在頂端
- row-reverse 橫向排版,起點在右端
- column-reversse 豎向排版,七點在底端
flex-wrap: wrap(預設) / nowrap / wrap-reverse
決定當可用排版空間不足時,是否允許換行,以及換行後的順序,模式包括允許換行、不換行、換行後整體反向。
- wrap (預設)空間不足時,進行換行
- nowrap 不換行
- wrap-reverse 換行後,第一行在最下方,行排版方向向上
justify-content: flex-start(預設) / flex-end / center / space-between / space-around
指定專案在主軸上的對齊方式
- flex-start (預設)對齊主軸的起點,例如:row 情況下左對齊
- flex-end 對齊主軸的終點,例如:row 情況下右對齊
- center 主軸內居中
- space-betwwen 均勻排列每個專案,首個專案放置於起點,末尾專案放置於終點
- space-around 均勻排列每個專案,每個專案周圍分配相同的空間
align-items: stretch / flex-start / flex-end / center / baseline
指定專案在交叉軸的對齊方式
- stretch(預設)每個專案進行拉伸,知道所有專案大小鋪滿父容器
- flex-start 對齊交叉軸的起點
- flex-end 對齊交叉軸的終點
- center 交叉軸內居中
- baseline 在一行中,所有專案以首個專案的文字排版為基線對齊,僅在 flex-direction: row / row-reverse 生效
align-content: stretch / flex-start / flex-end / center / space-between / space-around
指定容器中存在多行情況下,在交叉軸上,行間對齊方式
- stretch(預設)行在交叉軸上的大小進行拉伸,鋪滿容器
- flex-start 行向交叉軸起點對齊
- flex-end 行向交叉軸終點對齊
- center 行在交叉軸上居中
- space-between 均勻排列每一行,第一行放置於起點,最後一行放置於終點
- space-around 均勻排列每一行,每一行周圍分配相同的空間
專案屬性
order: (預設 0)
指定專案的排列順序
flex-grow: (預設 0)
指定專案的放大比例,預設為0,即如果存在剩餘空間,也不進行放大。
flex-shrink: number (預設 1)
指定專案的縮小比例,預設為1,即在空間不足(僅當不換行時候起效),所有專案等比縮小,當設定為0,該專案不進行縮小。
flex-basis: number / auto(預設 auto)
指定專案的主軸的初始大小,auto 的含義是參考 width 或 height 的大小,
align-self: auto / stretch / flex-start / flex-end / center / base-line
指定專案在容器交叉軸的對齊方式,auto 為參照容器的 align-items,其餘值和 align-items 介紹一致。
Flex 原始碼理解
憑藉 ReactNative 的潮流,Yoga 迅速崛起,發展成了 flex 排版中的佼佼者。flex 設計始於W3C,逐漸被各大瀏覽器支援,所以像是 Webkit 這樣的排版的程式碼是最開始的 flex 排版設計原始碼。我通過閱讀 Webikit 的 RenderFlexibleBox 原始碼、Facebook Yoga 原始碼 和 Google 的 flexbox-layout 原始碼 瞭解 flex 排版的實現細節。這三者的思想和流程都是一致的,Webkit 的實現是最為全的,但是它受原有的其他屬性所影響,看起來比較難理解,其他兩個就比較純粹一些。由於我最先接觸 Yoga,所以這裡以 Yoga 的程式碼為解析的原始碼進行分析,2017 年 5 月份的版本,到最新的版本中間有修一些 bug 和整體程式碼的結構化,但是整體關鍵內容還是一樣的(主要是我看的時候忘記更新了,寫了一大半)。當然,此時 Yoga 的程式碼寫在一塊了,晦澀難懂,這是 Yoga 不好的地方。
單位介紹
auto: YGUnitAuto / YGUnitUndefined 未設定,由父容器屬性和子專案決定
百分比: YGUnitPercent 大小為父容器的寬度乘以設定的百分比得出來的值
數值: YGUnitPoint 大小為具體設定的數值
測量資訊的模式
YGMeasureAtMost: 當前專案大小不確切,但有最大值限制
YGMeasureExactly: 當前專案的大小是確切可知的
YGMeasureUndefined: 當前專案大小不確定
專案空間的盒子模型
這個圖看到的就是整個專案佔據的空間。在盒模型中有個屬性 box-sizing 用來定
- position 在當前盒子中專案四邊的定位,優優先順序 right > left & top > left。不會佔據任何大小。
- margin 專案的外邊距,專案空間大小的組成。
- border 專案的邊框,專案空間大小的組成。
- padding 專案內邊距,專案空間大小的組成。
- width & height,當 box-sizing 設為 content-box,指內部藍色的區域。當 box-siziong 設為 boder-box時,指含 border 以內的區域。這兩種模型是盒模型的演進遺物,必須要了解,否則後面有些會出現模稜兩可的地方。Yoga 中預設是 border-box。
Yoga 特殊屬性
不支援 order 。新增 aspect-ratio 橫縱比設定,只有 width 或者 height 確定,就能確定另外一個變數。
排版測量概念
在排版引擎中有兩個概念,layout 和 measure。在 Yoga 裡面由於函式式程式碼的關係,看起來只有一個 Layout,但其實它也是具備這兩個概念的。
measure 指測量專案所需要的大小。
layout 指將專案確定的放置在具體的 (x, y) 點
排版流程概述
在看細節程式碼前,先了解下整體的排版思路,對於看細節上對於前後程式碼能進行聯絡。Yoga 整體的思路是得到了當前專案的具體大小,然後獲取子專案的大小(如果需要孫子專案確定則測量孫子專案),排版子專案,就這麼從樹節點一路排版下去,測量和排版階段混合(混合主要的原因是 flex 中的位置屬性也有可能引起大小的變化)。以下的流程存在遞迴邏輯。
- 入口函式由外層容器給出接下來的 flex 節點的可用空間,根據 flex 的根節點的 style 設定情況,確定給到根節點盒子大小,然後對根節點進行排版操作。
- 對於給定用於排版的空間,在排版或者測量階段開始之前,先判斷快取的排版資訊或者測量資訊是否可用,如果沒有或者不可用,則開始排版或者測量階段。
- 在排版或者測量階段,如果滿足略過子專案的測量和排版的條件(沒有孩子或本身具有外界設定的測量方法),則跳出該階段。在測量階段如果能滿足不需要測量子專案就能知道大小,也跳出該階段。否則繼續下一步。
- 確定主軸和交叉軸的方向以及給子專案的可用空間,確定或者測量子專案的主軸大小(測量需要跳轉第 2 步)。
- 根據 flex 的規則,進行行佈局的排版或者測量,確定每一行的子專案數量,在當前行佈局非伸縮子專案後主軸剩餘空間,進行伸縮子專案的主軸大小伸縮,確定伸縮子專案的大小,重新測量伸縮子專案的孫子(跳轉第 2 步)。
- 在當前行測量或排版階段,根據主軸的排版方式 justify-content 確定子專案的在主軸的位置並計算專案主軸大小。統計交叉軸的大小,根據交叉軸的排版方式 align-items 確定子專案在交叉軸的位置,如果需要拉伸,則重新測量子專案。累計所有行的交叉軸的大小和最大主軸大小。如果有下一行則回到第 5 步繼續下一行的計算,否則繼續。
- 如果在排版階段,根據 align-content 進行每一行的位置的確定,如果有拉伸操作,則需要重新排版該子專案。
- 根據自身 style 和孩子的測量大小確定專案的主軸和交叉軸的大小,如果在排版階段,則進行絕對佈局子專案的排版,以及確定其他子專案在所給空間上的位置。回到第 2 步的後續步驟第 9 步。
- 快取測量或者排版的結果資訊。
- 當整棵樹排版完成後,進行整棵樹的排版測量資訊的取整。
實現細節
Yoga 程式碼的實現細節分析是基於 commit: f68b50bb4bc215edc45a10fda70a51028286f77e 的程式碼。
整體的實現非常長,多達 3500+ 行程式碼,而且每個步驟都是精華,所以還需要跟著以下步驟和思維一步一步跟下去,否則很容易迷失。
- 入口函式,從 YGJNI.cpp 的 YGNodeCalculateLayout 方法可以得到 Yoga 的入口是 YGNodeCalculateLayout 方法。
- 進入到 YGNodeCalculateLayout 方法後,這個入口是整體的入口,不是遞迴的入口。這裡確定當前容器所給的寬高和其寬高的測量模式(這裡的寬高代表盒子模型的大小),開始進行當前專案及其子專案的排版,當整體排版流程結束後,對於整個節點的排版資訊進行四捨五入,確保是整型。
// Yoga 中設定的值為一個結構體,表示單位數值,包含了數值 value,和數值的單位
typedef struct YGValue {
float value;
YGUnit unit;
} YGValue;
// 獲取專案的寬高尺寸,一般使用設定的寬高。如果最大值和最小值設定並相等,這使用該值作為尺寸。
static inline void YGResolveDimensions(YGNodeRef node) {
for (YGDimension dim = YGDimensionWidth; dim <= YGDimensionHeight; dim++) {
if (node->style.maxDimensions[dim].unit != YGUnitUndefined &&
YGValueEqual(node->style.maxDimensions[dim], node->style.minDimensions[dim])) {
node->resolvedDimensions[dim] = &node->style.maxDimensions[dim];
} else {
node->resolvedDimensions[dim] = &node->style.dimensions[dim];
}
}
}
// 根據單位算出真實值
static inline float YGResolveValue(const YGValue *const value, const float parentSize) {
switch (value->unit) {
case YGUnitUndefined:
case YGUnitAuto:
return YGUndefined; // 未定義
case YGUnitPoint:
return value->value; // 本身設定的值
case YGUnitPercent:
return value->value * parentSize / 100.0f; // 根據父親百分比設定
}
return YGUndefined;
}
// 判斷專案的在 style 中設定的尺寸是否是確切的,確切代表,單位不應該是 YGAuto 或 YGUndefined;如果單位
// 是 YGPoint,數值不能是負數;如果單位是百分比,數值也不能是負數。
static inline bool YGNodeIsStyleDimDefined(const YGNodeRef node,
const YGFlexDirection axis,
const float parentSize) {
return !(node->resolvedDimensions[dim[axis]]->unit == YGUnitAuto ||
node->resolvedDimensions[dim[axis]]->unit == YGUnitUndefined ||
(node->resolvedDimensions[dim[axis]]->unit == YGUnitPoint &&
node->resolvedDimensions[dim[axis]]->value < 0.0f) ||
(node->resolvedDimensions[dim[axis]]->unit == YGUnitPercent &&
(node->resolvedDimensions[dim[axis]]->value < 0.0f || YGFloatIsUndefined(parentSize))));
}
// 排版入口
void YGNodeCalculateLayout(const YGNodeRef node,
const float parentWidth,
const float parentHeight,
const YGDirection parentDirection) {
// 每一次進入 Yoga 的排版,這個值都自增並且被設定給每個專案,主要用於確保 dirty 的專案在父容器給定
// 空間不變時,只會被遞迴遍歷一次。另外因為有些情況會略過子專案的大小測量或排版,例如當父專案寬度最大值為
// 0,在測量的時候就會被略過。後面可以理解到該屬性的作用。
gCurrentGenerationCount++;
// 獲取專案的尺寸
YGResolveDimensions(node);
// 確定寬度和寬度的模式
float width = YGUndefined;
YGMeasureMode widthMeasureMode = YGMeasureModeUndefined;
if (YGNodeIsStyleDimDefined(node, YGFlexDirectionRow, parentWidth)) {
// 如果專案的尺寸是確切的,則根據單位獲取確切的大小,dim[YGFlexDirectionRow]=YGDimensionWidth
// 這裡加上 margin 是要確保 availableWidth 是盒子的寬度。
width = YGResolveValue(node->resolvedDimensions[dim[YGFlexDirectionRow]], parentWidth) + YGNodeMarginForAxis(node, YGFlexDirectionRow, parentWidth);
// 此處的尺寸模式為確切
widthMeasureMode = YGMeasureModeExactly;
} else if (YGResolveValue(&node->style.maxDimensions[YGDimensionWidth], parentWidth) >= 0.0f) {
// 如果專案的尺寸不是確切的,但是具有最大值,則取最大值。
width = YGResolveValue(&node->style.maxDimensions[YGDimensionWidth], parentWidth);
// 尺寸模式為有最大值限制
widthMeasureMode = YGMeasureModeAtMost;
} else {
// 如果以上兩個條件都沒有,寬度則使用父親給定的寬度,專案的大小交由後續自身屬性或孩子來決定。
width = parentWidth;
// 如果父親尺寸為確定值,則尺寸模式為確切,否則尺寸模式為未知
widthMeasureMode = YGFloatIsUndefined(width) ? YGMeasureModeUndefined : YGMeasureModeExactly;
}
// 確定高度和高度尺寸的模式,和上述的寬度同理,程式碼也是類似,可自行對比。
float height = YGUndefined;
YGMeasureMode heightMeasureMode = YGMeasureModeUndefined;
if (YGNodeIsStyleDimDefined(node, YGFlexDirectionColumn, parentHeight)) {
height = YGResolveValue(node->resolvedDimensions[dim[YGFlexDirectionColumn]], parentHeight) +
YGNodeMarginForAxis(node, YGFlexDirectionColumn, parentWidth);
heightMeasureMode = YGMeasureModeExactly;
} else if (YGResolveValue(&node->style.maxDimensions[YGDimensionHeight], parentHeight) >= 0.0f) {
height = YGResolveValue(&node->style.maxDimensions[YGDimensionHeight], parentHeight);
heightMeasureMode = YGMeasureModeAtMost;
} else {
height = parentHeight;
heightMeasureMode = YGFloatIsUndefined(height) ? YGMeasureModeUndefined : YGMeasureModeExactly;
}
// 進入下一個環節
if (YGLayoutNodeInternal(node,
width,
height,
parentDirection,
widthMeasureMode,
heightMeasureMode,
parentWidth,
parentHeight,
true,
"initial",
node->config)) {
// 當所有節點都遞迴排版完畢,設定自身的位置
YGNodeSetPosition(node, node->layout.direction, parentWidth, parentHeight, parentWidth);
// 遞迴將所有節點的排版資訊包括大小和位置均進行四捨五入,這裡有很大學問
YGRoundToPixelGrid(node, node->config->pointScaleFactor, 0.0f, 0.0f);
if (gPrintTree) {
YGNodePrint(node, YGPrintOptionsLayout | YGPrintOptionsChildren | YGPrintOptionsStyle);
}
}
}
// 遞迴將所有節點的排版資訊包括大小和位置均進行四捨五入
static void YGRoundToPixelGrid(const YGNodeRef node,
const float pointScaleFactor,
const float absoluteLeft,
const float absoluteTop) {
if (pointScaleFactor == 0.0f) {
return;
}
const float nodeLeft = node->layout.position[YGEdgeLeft];
const float nodeTop = node->layout.position[YGEdgeTop];
const float nodeWidth = node->layout.dimensions[YGDimensionWidth];
const float nodeHeight = node->layout.dimensions[YGDimensionHeight];
const float absoluteNodeLeft = absoluteLeft + nodeLeft;
const float absoluteNodeTop = absoluteTop + nodeTop;
const float absoluteNodeRight = absoluteNodeLeft + nodeWidth;
const float absoluteNodeBottom = absoluteNodeTop + nodeHeight;
// 如果自身擁有測量的方法,則不進行四捨五入,而是強行向上取整
const bool textRounding = node->nodeType == YGNodeTypeText;
node->layout.position[YGEdgeLeft] =
YGRoundValueToPixelGrid(nodeLeft, pointScaleFactor, false, textRounding);
node->layout.position[YGEdgeTop] =
YGRoundValueToPixelGrid(nodeTop, pointScaleFactor, false, textRounding);
// 根據排版值最終確定大小,而不是直接強行強轉測量大小
// 這裡有一個場景,例如 父親寬 200px,橫向排版,具有三個 flex:1 的孩子,均分後的孩子寬度為
// 如果強行轉測量大小,則孩子寬度為67、67、66,這就會出現和 web 不一樣的結果,而按照這裡的做法
// 則是67、66、67.
node->layout.dimensions[YGDimensionWidth] =
YGRoundValueToPixelGrid(absoluteNodeRight, pointScaleFactor, textRounding, false) -
YGRoundValueToPixelGrid(absoluteNodeLeft, pointScaleFactor, false, textRounding);
node->layout.dimensions[YGDimensionHeight] =
YGRoundValueToPixelGrid(absoluteNodeBottom, pointScaleFactor, textRounding, false) -
YGRoundValueToPixelGrid(absoluteNodeTop, pointScaleFactor, false, textRounding);
const uint32_t childCount = YGNodeListCount(node->children);
for (uint32_t i = 0; i < childCount; i++) {
YGRoundToPixelGrid(YGNodeGetChild(node, i), pointScaleFactor, absoluteNodeLeft, absoluteNodeTop);
}
}
複製程式碼
- 接下來的這個方法主要是用於判斷是否需要重新進行專案空間計算(排版或者測量)的操作。判斷的依據是本身是否已經排過版(髒標記是否更新),同時快取的排版資訊中父容器給定的可用寬高和模式與當前的的父容器給定的環境對比對於當前專案來說不需要重新進行測量重排,則可以使用快取的資訊,而不需要重新進行專案空間的計算。
// 入口
bool YGLayoutNodeInternal(const YGNodeRef node,
const float availableWidth,
const float availableHeight,
const YGDirection parentDirection,
const YGMeasureMode widthMeasureMode,
const YGMeasureMode heightMeasureMode,
const float parentWidth,
const float parentHeight,
const bool performLayout,
const char *reason,
const YGConfigRef config) {
// 獲取當前專案的排版資訊
YGLayout *layout = &node->layout;
// 深度自增,沒什麼用
gDepth++;
// 判斷是否需要重新進行專案計算,條件是以下兩個其中一個
// 1.專案是髒的(需要重排),同時在一個大排版週期中專案還未被排版過( generationCount 在這裡起了判斷是
// 否排版過的作用);2.或者父親排版方向改變了
const bool needToVisitNode =
(node->isDirty && layout->generationCount != gCurrentGenerationCount) ||
layout->lastParentDirection != parentDirection;
// 如果需要重新進行專案,則重新整理快取的資料
if (needToVisitNode) {
// 用於設定在一個排版週期中快取排版的數量,這個最大值是16,代表複雜的排版可能會被重排版次數高達16次!
layout->nextCachedMeasurementsIndex = 0;
layout->cachedLayout.widthMeasureMode = (YGMeasureMode) -1;
layout->cachedLayout.heightMeasureMode = (YGMeasureMode) -1;
layout->cachedLayout.computedWidth = -1;
layout->cachedLayout.computedHeight = -1;
}
YGCachedMeasurement *cachedResults = NULL;
// 如果外部有設定測量函式則進入 if 函式。測量是一個非常耗時的操作,比如文字測量,所以能不能從快取中獲取非常重要
if (node->measure) {
// 橫豎向的外邊距
const float marginAxisRow = YGNodeMarginForAxis(node, YGFlexDirectionRow, parentWidth);
const float marginAxisColumn = YGNodeMarginForAxis(node, YGFlexDirectionColumn, parentWidth);
// 首先,判斷能不能直接用當前的快取的排版
if (YGNodeCanUseCachedMeasurement(widthMeasureMode,
availableWidth,
heightMeasureMode,
availableHeight,
layout->cachedLayout.widthMeasureMode,
layout->cachedLayout.availableWidth,
layout->cachedLayout.heightMeasureMode,
layout->cachedLayout.availableHeight,
layout->cachedLayout.computedWidth,
layout->cachedLayout.computedHeight,
marginAxisRow,
marginAxisColumn,
config)) {
cachedResults = &layout->cachedLayout;
} else {
// 將之前的快取結果都拿出來看看是不是能用,這個能極大節省時間。
for (uint32_t i = 0; i < layout->nextCachedMeasurementsIndex; i++) {
if (YGNodeCanUseCachedMeasurement(widthMeasureMode,
availableWidth,
heightMeasureMode,
availableHeight,
layout->cachedMeasurements[i].widthMeasureMode,
layout->cachedMeasurements[i].availableWidth,
layout->cachedMeasurements[i].heightMeasureMode,
layout->cachedMeasurements[i].availableHeight,
layout->cachedMeasurements[i].computedWidth,
layout->cachedMeasurements[i].computedHeight,
marginAxisRow,
marginAxisColumn,
config)) {
cachedResults = &layout->cachedMeasurements[i];
break;
}
}
}
} else if (performLayout) {
// 如果是需要進行排版,則判斷快取的排版是否可用,判斷可用標準是父親給定的可用寬高及其模式沒有變化
if (YGFloatsEqual(layout->cachedLayout.availableWidth, availableWidth) &&
YGFloatsEqual(layout->cachedLayout.availableHeight, availableHeight) &&
layout->cachedLayout.widthMeasureMode == widthMeasureMode &&
layout->cachedLayout.heightMeasureMode == heightMeasureMode) {
cachedResults = &layout->cachedLayout;
}
} else {
// 如果不是排版而是測量,則獲取快取的測量大小,判斷可用標準父親給定的可用寬高及其模式沒有變化
for (uint32_t i = 0; i < layout->nextCachedMeasurementsIndex; i++) {
if (YGFloatsEqual(layout->cachedMeasurements[i].availableWidth, availableWidth) &&
YGFloatsEqual(layout->cachedMeasurements[i].availableHeight, availableHeight) &&
layout->cachedMeasurements[i].widthMeasureMode == widthMeasureMode &&
layout->cachedMeasurements[i].heightMeasureMode == heightMeasureMode) {
cachedResults = &layout->cachedMeasurements[i];
break;
}
}
}
if (!needToVisitNode && cachedResults != NULL)
// 如果不需要重新進行專案,同時有快取就直接將快取設定給 measuredDimensions (具體寬高)
layout->measuredDimensions[YGDimensionWidth] = cachedResults->computedWidth;
layout->measuredDimensions[YGDimensionHeight] = cachedResults->computedHeight;
if (gPrintChanges && gPrintSkips) {
printf("%s%d.{[skipped] ", YGSpacer(gDepth), gDepth);
if (node->print) {
node->print(node);
}
printf("wm: %s, hm: %s, aw: %f ah: %f => d: (%f, %f) %s\n",
YGMeasureModeName(widthMeasureMode, performLayout),
YGMeasureModeName(heightMeasureMode, performLayout),
availableWidth,
availableHeight,
cachedResults->computedWidth,
cachedResults->computedHeight,
reason);
}
} else {
if (gPrintChanges) {
printf("%s%d.{%s", YGSpacer(gDepth), gDepth, needToVisitNode ? "*" : "");
if (node->print) {
node->print(node);
}
printf("wm: %s, hm: %s, aw: %f ah: %f %s\n",
YGMeasureModeName(widthMeasureMode, performLayout),
YGMeasureModeName(heightMeasureMode, performLayout),
availableWidth,
availableHeight,
reason);
}
// 如果需要重新進行專案測量或者排版,則進入下一環節
YGNodelayoutImpl(node,
availableWidth,
availableHeight,
parentDirection,
widthMeasureMode,
heightMeasureMode,
parentWidth,
parentHeight,
performLayout,
config);
if (gPrintChanges) {
printf("%s%d.}%s", YGSpacer(gDepth), gDepth, needToVisitNode ? "*" : "");
if (node->print) {
node->print(node);
}
printf("wm: %s, hm: %s, d: (%f, %f) %s\n",
YGMeasureModeName(widthMeasureMode, performLayout),
YGMeasureModeName(heightMeasureMode, performLayout),
layout->measuredDimensions[YGDimensionWidth],
layout->measuredDimensions[YGDimensionHeight],
reason);
}
// 記錄當前父容器方向
layout->lastParentDirection = parentDirection;
// 如果快取為空,設定快取
if (cachedResults == NULL) {
// 快取超出了可設定大小,代表之前的都沒啥用,重新記錄快取
if (layout->nextCachedMeasurementsIndex == YG_MAX_CACHED_RESULT_COUNT) {
if (gPrintChanges) {
printf("Out of cache entries!\n");
}
layout->nextCachedMeasurementsIndex = 0;
}
// 獲取需要更新快取的入口,如果是排版,則獲取排版單一的快取入口,如果是測量,則獲取當前指向的入口
YGCachedMeasurement *newCacheEntry;
if (performLayout) {
newCacheEntry = &layout->cachedLayout;
} else {
newCacheEntry = &layout->cachedMeasurements[layout->nextCachedMeasurementsIndex];
layout->nextCachedMeasurementsIndex++;
}
// 更新相關引數
newCacheEntry->availableWidth = availableWidth;
newCacheEntry->availableHeight = availableHeight;
newCacheEntry->widthMeasureMode = widthMeasureMode;
newCacheEntry->heightMeasureMode = heightMeasureMode;
newCacheEntry->computedWidth = layout->measuredDimensions[YGDimensionWidth];
newCacheEntry->computedHeight = layout->measuredDimensions[YGDimensionHeight];
}
}
if (performLayout) {
// 如果是排版則記錄排版的大小,更新髒標誌。
node->layout.dimensions[YGDimensionWidth] = node->layout.measuredDimensions[YGDimensionWidth];
node->layout.dimensions[YGDimensionHeight] = node->layout.measuredDimensions[YGDimensionHeight];
node->hasNewLayout = true;
node->isDirty = false;
}
gDepth--;
layout->generationCount = gCurrentGenerationCount;
// 返回 true 是排版了,false 是跳過了使用快取。
return (needToVisitNode || cachedResults == NULL);
}
// 判斷當前快取的測量值是否可用
bool YGNodeCanUseCachedMeasurement(const YGMeasureMode widthMode,
const float width,
const YGMeasureMode heightMode,
const float height,
const YGMeasureMode lastWidthMode,
const float lastWidth,
const YGMeasureMode lastHeightMode,
const float lastHeight,
const float lastComputedWidth,
const float lastComputedHeight,
const float marginRow,
const float marginColumn,
const YGConfigRef config) {
if (lastComputedHeight < 0 || lastComputedWidth < 0) {
return false;
}
bool useRoundedComparison = config != NULL && config->pointScaleFactor != 0;
const float effectiveWidth = useRoundedComparison ? YGRoundValueToPixelGrid(width, config->pointScaleFactor, false, false) : width;
const float effectiveHeight = useRoundedComparison ? YGRoundValueToPixelGrid(height, config->pointScaleFactor, false, false) : height;
const float effectiveLastWidth = useRoundedComparison ? YGRoundValueToPixelGrid(lastWidth, config->pointScaleFactor, false, false) : lastWidth;
const float effectiveLastHeight = useRoundedComparison ? YGRoundValueToPixelGrid(lastHeight, config->pointScaleFactor, false, false) : lastHeight;
// 1. 判斷寬高和其模式是否相等
const bool hasSameWidthSpec = lastWidthMode == widthMode && YGFloatsEqual(effectiveLastWidth, effectiveWidth);
const bool hasSameHeightSpec = lastHeightMode == heightMode && YGFloatsEqual(effectiveLastHeight, effectiveHeight);
const bool widthIsCompatible =
hasSameWidthSpec ||
// 2. 當前寬度模式為確切,同時快取的計算寬度和給出的寬度是相同的。
YGMeasureModeSizeIsExactAndMatchesOldMeasuredSize(widthMode, width - marginRow, lastComputedWidth) ||
// 3. 當前寬度模式為最大值,快取寬度模式為未知,同時所給可用寬度大小大於或等於快取的計算寬度
YGMeasureModeOldSizeIsUnspecifiedAndStillFits(widthMode,
width - marginRow,
lastWidthMode,
lastComputedWidth) ||
// 4. 當前寬度模式和快取寬度模式均為最大範圍,快取可用寬度值大於當前可用寬度值,同時快取的計算寬度
// 小於或等於當前可用寬度
YGMeasureModeNewMeasureSizeIsStricterAndStillValid(
widthMode, width - marginRow, lastWidthMode, lastWidth, lastComputedWidth);
// 同寬度分析
const bool heightIsCompatible =
hasSameHeightSpec || YGMeasureModeSizeIsExactAndMatchesOldMeasuredSize(heightMode,
height - marginColumn,
lastComputedHeight) ||
YGMeasureModeOldSizeIsUnspecifiedAndStillFits(heightMode,
height - marginColumn,
lastHeightMode,
lastComputedHeight) ||
YGMeasureModeNewMeasureSizeIsStricterAndStillValid(
heightMode, height - marginColumn, lastHeightMode, lastHeight, lastComputedHeight);
// 返回寬度和高度是否仍然適用
return widthIsCompatible && heightIsCompatible;
}
// 這個方法主要用來進行數值的四捨五入,要根據 pointScaleFactor 進行數值的四捨五入。防止直接對數值進行四
// 舍五入導致之後的換算回來有問題。簡而言之根據縮放比率進行四捨五入,得到縮放比率同等級別的精度。可以保證
// 在不同大小的解析度情況下不會出現可能左右偏移一個畫素
static float YGRoundValueToPixelGrid(const float value,
const float pointScaleFactor,
const bool forceCeil,
const bool forceFloor) {
float fractial = fmodf(value, pointScaleFactor);
if (YGFloatsEqual(fractial, 0)) {
return value - fractial;
}
if (forceCeil) {
return value - fractial + pointScaleFactor;
} else if (forceFloor) {
return value - fractial;
} else {
return value - fractial + (fractial >= pointScaleFactor / 2.0f ? pointScaleFactor : 0);
}
}
// 當前寬高模式為確切,同時快取的計算寬高和給出的寬高是相同的,代表確切值可用,返回true
static inline bool YGMeasureModeSizeIsExactAndMatchesOldMeasuredSize(YGMeasureMode sizeMode,
float size,
float lastComputedSize) {
return sizeMode == YGMeasureModeExactly && YGFloatsEqual(size, lastComputedSize);
}
// 當前寬高模式為最大值,快取寬高模式為未知,同時所給的寬高大小大於或等於快取的寬高,代表未知模式下測出來的值,在具有最大範圍模式下仍然適用,返回true
static inline bool YGMeasureModeOldSizeIsUnspecifiedAndStillFits(YGMeasureMode sizeMode,
float size,
YGMeasureMode lastSizeMode,
float lastComputedSize) {
return sizeMode == YGMeasureModeAtMost && lastSizeMode == YGMeasureModeUndefined &&
(size >= lastComputedSize || YGFloatsEqual(size, lastComputedSize));
}
// 當前寬度模式和快取寬度模式均為最大範圍,快取可用寬度值大於當前可用寬度值,同時快取的計算寬度小於或等於當前可用寬度,當前情況寬度測量的結果必定一樣,仍然適用,則返回true
static inline bool YGMeasureModeNewMeasureSizeIsStricterAndStillValid(YGMeasureMode sizeMode,
float size,
YGMeasureMode lastSizeMode,
float lastSize,
float lastComputedSize) {
return lastSizeMode == YGMeasureModeAtMost && sizeMode == YGMeasureModeAtMost &&
lastSize > size && (lastComputedSize <= size || YGFloatsEqual(size, lastComputedSize));
}
複製程式碼
- 接下來的程式碼分析都在 YGNodelayoutImpl 方法中,這個方法中包含了整個 flex 排版的精髓,主要用於測量自身大小,同時排版子專案。整體程式碼非常長,這裡將程式碼分段介紹,直到結束。首先的操作盒子模型中 margin / border / padding,如果本身可以確定大小,同時不是在排版流程時或者是具有自身測量的方法則直接返回跳出後續的程式碼,因為後續的子專案暫時不需要進行大小測量(排版的流程是需要先知道子專案的大小,然後就進行子專案位置確定,子專案的子專案的大小可以延後等到子專案進行位置確定階段)。
static void YGNodelayoutImpl(const YGNodeRef node,
const float availableWidth,
const float availableHeight,
const YGDirection parentDirection,
const YGMeasureMode widthMeasureMode,
const YGMeasureMode heightMeasureMode,
const float parentWidth,
const float parentHeight,
const bool performLayout,
const YGConfigRef config) {
YGAssertWithNode(node,
YGFloatIsUndefined(availableWidth) ? widthMeasureMode == YGMeasureModeUndefined
: true,
"availableWidth is indefinite so widthMeasureMode must be "
"YGMeasureModeUndefined");
YGAssertWithNode(node,
YGFloatIsUndefined(availableHeight) ? heightMeasureMode == YGMeasureModeUndefined
: true,
"availableHeight is indefinite so heightMeasureMode must be "
"YGMeasureModeUndefined");
// 確定當前專案的方向,如RTL / LTR,如果是繼承父親,則使用父親的。
const YGDirection direction = YGNodeResolveDirection(node, parentDirection);
node->layout.direction = direction;
// 根據專案方向,確定橫豎軸方向,如 row / row-reverse
const YGFlexDirection flexRowDirection = YGResolveFlexDirection(YGFlexDirectionRow, direction);
const YGFlexDirection flexColumnDirection =
YGResolveFlexDirection(YGFlexDirectionColumn, direction);
// 盒子模型中邊界都用 edge 表示,這樣在橫豎軸方向可以起到泛指的作用,否則容易迷糊。比如 EdgeStart 表
// 示起始位置,它代表 row 情況下盒子左側,row-reverse 情況下盒子右側。
// 計算這些邊距值時由於設定的多樣性,例如 padding: 10px 10px 或者 padding: 10px。就導致了 Yoga
// 在處理時化成了 YGEdgeVertical 或者 YGEdgeAll 這樣去判斷這些值是否設定。
node->layout.margin[YGEdgeStart] = YGNodeLeadingMargin(node, flexRowDirection, parentWidth);
node->layout.margin[YGEdgeEnd] = YGNodeTrailingMargin(node, flexRowDirection, parentWidth);
node->layout.margin[YGEdgeTop] = YGNodeLeadingMargin(node, flexColumnDirection, parentWidth);
node->layout.margin[YGEdgeBottom] = YGNodeTrailingMargin(node, flexColumnDirection, parentWidth);
node->layout.border[YGEdgeStart] = YGNodeLeadingBorder(node, flexRowDirection);
node->layout.border[YGEdgeEnd] = YGNodeTrailingBorder(node, flexRowDirection);
node->layout.border[YGEdgeTop] = YGNodeLeadingBorder(node, flexColumnDirection);
node->layout.border[YGEdgeBottom] = YGNodeTrailingBorder(node, flexColumnDirection);
node->layout.padding[YGEdgeStart] = YGNodeLeadingPadding(node, flexRowDirection, parentWidth);
node->layout.padding[YGEdgeEnd] = YGNodeTrailingPadding(node, flexRowDirection, parentWidth);
node->layout.padding[YGEdgeTop] = YGNodeLeadingPadding(node, flexColumnDirection, parentWidth);
node->layout.padding[YGEdgeBottom] =
YGNodeTrailingPadding(node, flexColumnDirection, parentWidth);
// 當然專案設定了測量的方法,則跳轉到第 5 步,然後跳出排版步驟。這裡預設有測量方式的專案都不具備孩子,即無論對於測量還是排版,都不需要往後繼續遍歷。
if (node->measure) {
YGNodeWithMeasureFuncSetMeasuredDimensions(node,
availableWidth,
availableHeight,
widthMeasureMode,
heightMeasureMode,
parentWidth,
parentHeight);
return;
}
const uint32_t childCount = YGNodeListCount(node->children);
// 當專案的孩子數量為0時,跳轉到第 6 步,然後跳出排版步驟。這裡預設沒有孩子的專案都不需要往後繼續遍歷,
// 因為不需要為後面的孩子進行排版,只需要在這一步獲得自身大小即可。
if (childCount == 0) {
YGNodeEmptyContainerSetMeasuredDimensions(node,
availableWidth,
availableHeight,
widthMeasureMode,
heightMeasureMode,
parentWidth,
parentHeight);
return;
}
// 當不需要進行子專案排版,同時專案大小可以馬上確定(請看第 7 步),則直接跳出排版步驟。
if (!performLayout && YGNodeFixedSizeSetMeasuredDimensions(node,
availableWidth,
availableHeight,
widthMeasureMode,
heightMeasureMode,
parentWidth,
parentHeight)) {
return;
}
複製程式碼
- 當專案設定了測量方法,當可用空間不是未定義時,去除盒子模型的邊距,獲得剩餘寬高。當寬高都是確切的,則直接使用寬高值並進行閾值限制。否則通過測量方法進行測量,在進行閾值限制。具有測量方法的專案預設孩子位置由其自身管理。
static void YGNodeWithMeasureFuncSetMeasuredDimensions(const YGNodeRef node,
const float availableWidth,
const float availableHeight,
const YGMeasureMode widthMeasureMode,
const YGMeasureMode heightMeasureMode,
const float parentWidth,
const float parentHeight) {
YGAssertWithNode(node, node->measure != NULL, "Expected node to have custom measure function");
// 計算主軸交叉軸上的邊距和邊框
const float paddingAndBorderAxisRow =
YGNodePaddingAndBorderForAxis(node, YGFlexDirectionRow, availableWidth);
const float paddingAndBorderAxisColumn =
YGNodePaddingAndBorderForAxis(node, YGFlexDirectionColumn, availableWidth);
const float marginAxisRow = YGNodeMarginForAxis(node, YGFlexDirectionRow, availableWidth);
const float marginAxisColumn = YGNodeMarginForAxis(node, YGFlexDirectionColumn, availableWidth);
// 當可用空間不是未定義時,去除邊距和邊框的內部寬高
const float innerWidth = YGFloatIsUndefined(availableWidth)
? availableWidth
: fmaxf(0, availableWidth - marginAxisRow - paddingAndBorderAxisRow);
const float innerHeight = YGFloatIsUndefined(availableHeight)
? availableHeight
: fmaxf(0, availableHeight - marginAxisColumn - paddingAndBorderAxisColumn);
if (widthMeasureMode == YGMeasureModeExactly && heightMeasureMode == YGMeasureModeExactly) {
// 當寬高都是確切的,則不需要經過測量的步驟,直接使用確切的寬高(availableWidth - marginAxisRow
// 就是確切的寬高,第 2 步使有闡述),確保確切寬高是在限制的閾值內
node->layout.measuredDimensions[YGDimensionWidth] = YGNodeBoundAxis(
node, YGFlexDirectionRow, availableWidth - marginAxisRow, parentWidth, parentWidth);
node->layout.measuredDimensions[YGDimensionHeight] = YGNodeBoundAxis(
node, YGFlexDirectionColumn, availableHeight - marginAxisColumn, parentHeight, parentWidth);
} else {
// 如果寬高不確定,則需要呼叫測量的方法確定大小,測量傳入的可用寬高是去除了邊框和邊距的。
const YGSize measuredSize =
node->measure(node, innerWidth, widthMeasureMode, innerHeight, heightMeasureMode);
// 將獲得的測量值進行閾值限制,同時如果模式是確切的,則使用確切值 (availableWidth - marginAxisRow),否則使用測量值 measureSize。
node->layout.measuredDimensions[YGDimensionWidth] =
YGNodeBoundAxis(node,
YGFlexDirectionRow,
(widthMeasureMode == YGMeasureModeUndefined ||
widthMeasureMode == YGMeasureModeAtMost)
? measuredSize.width + paddingAndBorderAxisRow
: availableWidth - marginAxisRow,
availableWidth,
availableWidth);
node->layout.measuredDimensions[YGDimensionHeight] =
YGNodeBoundAxis(node,
YGFlexDirectionColumn,
(heightMeasureMode == YGMeasureModeUndefined ||
heightMeasureMode == YGMeasureModeAtMost)
? measuredSize.height + paddingAndBorderAxisColumn
: availableHeight - marginAxisColumn,
availableHeight,
availableWidth);
}
}
// 確保確切的寬高不會超過最大值,同時不小於最小值。另外在 boder-box 中當內邊距和邊框的值大於寬高值則使用
// 前者作為寬高。
static inline float YGNodeBoundAxis(const YGNodeRef node,
const YGFlexDirection axis,
const float value,
const float axisSize,
const float widthSize) {
return fmaxf(YGNodeBoundAxisWithinMinAndMax(node, axis, value, axisSize),
YGNodePaddingAndBorderForAxis(node, axis, widthSize));
}
// 獲取前沿和後沿的內邊距和邊框的和值,widthSize 這裡對於主軸和交叉軸都是一樣,原因是邊距和邊框設定的
// 百分比是根據專案寬度計算真實值。(敲黑板),接下來的程式碼就是獲取邊距和邊框然後確定其值。
static inline float YGNodePaddingAndBorderForAxis(const YGNodeRef node,
const YGFlexDirection axis,
const float widthSize) {
return YGNodeLeadingPaddingAndBorder(node, axis, widthSize) +
YGNodeTrailingPaddingAndBorder(node, axis, widthSize);
}
複製程式碼
- 當專案的孩子數量為0時,專案的寬高僅有本身決定,即有確切寬高或限制寬高或內邊距和邊框則作為自身大小,否則自身大小就為 0。
static void YGNodeEmptyContainerSetMeasuredDimensions(const YGNodeRef node,
const float availableWidth,
const float availableHeight,
const YGMeasureMode widthMeasureMode,
const YGMeasureMode heightMeasureMode,
const float parentWidth,
const float parentHeight) {
const float paddingAndBorderAxisRow =
YGNodePaddingAndBorderForAxis(node, YGFlexDirectionRow, parentWidth);
const float paddingAndBorderAxisColumn =
YGNodePaddingAndBorderForAxis(node, YGFlexDirectionColumn, parentWidth);
const float marginAxisRow = YGNodeMarginForAxis(node, YGFlexDirectionRow, parentWidth);
const float marginAxisColumn = YGNodeMarginForAxis(node, YGFlexDirectionColumn, parentWidth);
// 計算盒模型 width 和 height 時,
// 1. 確切寬高。當專案具有確切寬高時,使用確切寬高,並進行閾值限制。
// 2. 如果沒有確切寬高,則使用內邊距和邊框的和值,並進行閾值限制。
node->layout.measuredDimensions[YGDimensionWidth] =
YGNodeBoundAxis(node,
YGFlexDirectionRow,
(widthMeasureMode == YGMeasureModeUndefined ||
widthMeasureMode == YGMeasureModeAtMost)
? paddingAndBorderAxisRow
: availableWidth - marginAxisRow,
parentWidth,
parentWidth);
node->layout.measuredDimensions[YGDimensionHeight] =
YGNodeBoundAxis(node,
YGFlexDirectionColumn,
(heightMeasureMode == YGMeasureModeUndefined ||
heightMeasureMode == YGMeasureModeAtMost)
? paddingAndBorderAxisColumn
: availableHeight - marginAxisColumn,
parentHeight,
parentWidth);
}
複製程式碼
- 當專案不需要進行排版,需要確定專案大小是否可以馬上確定,如果可以馬上確定,則返回 true,否則返回false。馬上確定寬高的標準為,1. 寬或高模式為最大值且值為0,可以確定。2. 寬和高模式同時為確切。後續孩子的大小及排版(影響當前專案的大小)由當前專案在排版階段遍歷即可知道。
static bool YGNodeFixedSizeSetMeasuredDimensions(const YGNodeRef node,
const float availableWidth,
const float availableHeight,
const YGMeasureMode widthMeasureMode,
const YGMeasureMode heightMeasureMode,
const float parentWidth,
const float parentHeight) {
// 這一步其實比較奇怪,因為本身即使為寬或高 0 ,該專案其中一個大小可能還是收子專案大小影響,但這裡把這一步省略了,放到了排版階段,這樣在排版階段又會引起一次大小的變化。如果能改成如下,在排版階段不會影響父親的大小,這樣我想會更明瞭。
// if (((widthMeasureMode == YGMeasureModeAtMost && availableWidth <= 0.0f) &&
// (heightMeasureMode == YGMeasureModeAtMost && availableHeight <= 0.0f)) ||
// (widthMeasureMode == YGMeasureModeExactly && heightMeasureMode == YGMeasureModeExactly))
if ((widthMeasureMode == YGMeasureModeAtMost && availableWidth <= 0.0f) ||
(heightMeasureMode == YGMeasureModeAtMost && availableHeight <= 0.0f) ||
(widthMeasureMode == YGMeasureModeExactly && heightMeasureMode == YGMeasureModeExactly)) {
const float marginAxisColumn = YGNodeMarginForAxis(node, YGFlexDirectionColumn, parentWidth);
const float marginAxisRow = YGNodeMarginForAxis(node, YGFlexDirectionRow, parentWidth);
// 當專案的可用寬高是未知,而且模式是最大值的情況下可用寬高為 0, 那麼使用 0 作為測量值,並進行閾值判
// 斷。否則就代表寬高為確切,使用確切寬高(availableWidth - marginAxisRow)。
node->layout.measuredDimensions[YGDimensionWidth] =
YGNodeBoundAxis(node,
YGFlexDirectionRow,
YGFloatIsUndefined(availableWidth) ||
(widthMeasureMode == YGMeasureModeAtMost && availableWidth < 0.0f)
? 0.0f
: availableWidth - marginAxisRow,
parentWidth,
parentWidth);
node->layout.measuredDimensions[YGDimensionHeight] =
YGNodeBoundAxis(node,
YGFlexDirectionColumn,
YGFloatIsUndefined(availableHeight) ||
(heightMeasureMode == YGMeasureModeAtMost && availableHeight < 0.0f)
? 0.0f
: availableHeight - marginAxisColumn,
parentHeight,
parentWidth);
// 返回 true 代表不用進行子專案遍歷,可以知道自己的大小
return true;
}
// 返回 false 代表不用進行子專案遍歷,不知道自己的大小
return false;
}
複製程式碼
- 接下來的步驟就是確定主軸和交叉軸的方向,計算專案本身對於孩子而言在主軸和交叉軸可用空間包括最大值和最小值。
// 確定主軸和交叉軸的方向
const YGFlexDirection mainAxis = YGResolveFlexDirection(node->style.flexDirection, direction);
const YGFlexDirection crossAxis = YGFlexDirectionCross(mainAxis, direction);
const bool isMainAxisRow = YGFlexDirectionIsRow(mainAxis);
const YGJustify justifyContent = node->style.justifyContent;
const bool isNodeFlexWrap = node->style.flexWrap != YGWrapNoWrap;
// 確定主軸和交叉軸父容器提供的空間大小
const float mainAxisParentSize = isMainAxisRow ? parentWidth : parentHeight;
const float crossAxisParentSize = isMainAxisRow ? parentHeight : parentWidth;
// 用於記錄 absolute 的子專案,在排版最後再進行這些專案的排版。
YGNodeRef firstAbsoluteChild = NULL;
YGNodeRef currentAbsoluteChild = NULL;
// 確定主軸和交叉軸上的內外邊距和邊框
const float leadingPaddingAndBorderMain =
YGNodeLeadingPaddingAndBorder(node, mainAxis, parentWidth);
const float trailingPaddingAndBorderMain =
YGNodeTrailingPaddingAndBorder(node, mainAxis, parentWidth);
const float leadingPaddingAndBorderCross =
YGNodeLeadingPaddingAndBorder(node, crossAxis, parentWidth);
const float paddingAndBorderAxisMain = YGNodePaddingAndBorderForAxis(node, mainAxis, parentWidth);
const float paddingAndBorderAxisCross =
YGNodePaddingAndBorderForAxis(node, crossAxis, parentWidth);
// 確定主軸和交叉軸的測量模式
YGMeasureMode measureModeMainDim = isMainAxisRow ? widthMeasureMode : heightMeasureMode;
YGMeasureMode measureModeCrossDim = isMainAxisRow ? heightMeasureMode : widthMeasureMode;
// 確定橫豎方向上的內邊距和邊框和值
const float paddingAndBorderAxisRow =
isMainAxisRow ? paddingAndBorderAxisMain : paddingAndBorderAxisCross;
const float paddingAndBorderAxisColumn =
isMainAxisRow ? paddingAndBorderAxisCross : paddingAndBorderAxisMain;
// 確定橫豎方向上的外邊距
const float marginAxisRow = YGNodeMarginForAxis(node, YGFlexDirectionRow, parentWidth);
const float marginAxisColumn = YGNodeMarginForAxis(node, YGFlexDirectionColumn, parentWidth);
// 根據最大最小大小,去除橫豎方向上的內外邊距,(這裡我刪了 - marginAxisRow 這個,因為是一個 bug,後面
// 被修復了),得出專案內部的最大最小寬度和高度的限制,在後續給子專案排版時用到。
const float minInnerWidth =
YGResolveValue(&node->style.minDimensions[YGDimensionWidth], parentWidth) -
paddingAndBorderAxisRow;
const float maxInnerWidth =
YGResolveValue(&node->style.maxDimensions[YGDimensionWidth], parentWidth) -
paddingAndBorderAxisRow;
const float minInnerHeight =
YGResolveValue(&node->style.minDimensions[YGDimensionHeight], parentHeight) - paddingAndBorderAxisColumn;
const float maxInnerHeight =
YGResolveValue(&node->style.maxDimensions[YGDimensionHeight], parentHeight) - paddingAndBorderAxisColumn;
// 換算成主軸空間的最大最小限制
const float minInnerMainDim = isMainAxisRow ? minInnerWidth : minInnerHeight;
const float maxInnerMainDim = isMainAxisRow ? maxInnerWidth : maxInnerHeight;
// 確定該專案可用的內部寬度,計算方式整個盒子的大小去除邊距和邊框
float availableInnerWidth = availableWidth - marginAxisRow - paddingAndBorderAxisRow;
if (!YGFloatIsUndefined(availableInnerWidth)) {
// 如果不是未定義,那麼進行閾值限制。獲得在限定大小內的可用空間
availableInnerWidth = fmaxf(fminf(availableInnerWidth, maxInnerWidth), minInnerWidth);
}
// 同可用內部寬度計算方式
float availableInnerHeight = availableHeight - marginAxisColumn - paddingAndBorderAxisColumn;
if (!YGFloatIsUndefined(availableInnerHeight)) {
availableInnerHeight = fmaxf(fminf(availableInnerHeight, maxInnerHeight), minInnerHeight);
}
// 換算成主軸和交叉軸可用空間大小
float availableInnerMainDim = isMainAxisRow ? availableInnerWidth : availableInnerHeight;
const float availableInnerCrossDim = isMainAxisRow ? availableInnerHeight : availableInnerWidth;
// singleFlexChild 具體作用需要往後繼續看,後續的程式碼會將這個 child 的 computedFlexBasis 設定為 0,即
// flex-basis 為 0,意思是隻要滿足 flex-grow 和 flex-shrink 都大於 0,那麼就認為剩餘空間為父親的大小,
// child 直接填充整個剩餘空間。
// 但是,在 web 上,當僅有單個 child 並且滿足上述條件,如果大小超出去了,flex-shrink 範圍在(0, 1),之間
// 它不會撐滿父親,而是大於父親,如父親 width: 100px,孩子 width: 150px; flex-shrink: 0.5,那麼計算
// 得出來的結果孩子的大小為 125px。(和 web 表現不一致,算是Yoga的一個 bug)
YGNodeRef singleFlexChild = NULL;
if (measureModeMainDim == YGMeasureModeExactly) {
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef child = YGNodeGetChild(node, i);
if (singleFlexChild) {
if (YGNodeIsFlex(child)) {
singleFlexChild = NULL;
break;
}
} else if (YGResolveFlexGrow(child) > 0.0f && YGNodeResolveFlexShrink(child) > 0.0f) {
singleFlexChild = child;
}
}
}
float totalOuterFlexBasis = 0;
複製程式碼
- 確定每個子專案的 flex-basis 大小,便於後續根據 flex-basis 做拉伸的計算
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef child = YGNodeListGet(node->children, i);
// 如果是 display:none 則代表其孩子全部不需要展示,則遞迴遍歷孩子並設定 0 排版,並更新髒標誌。
if (child->style.display == YGDisplayNone) {
YGZeroOutLayoutRecursivly(child);
child->hasNewLayout = true;
child->isDirty = false;
continue;
}
// 確定子專案的設定大小
YGResolveDimensions(child);
if (performLayout) {
// 如果是排版操作,則設定子專案的排版的位置 layout.position (不是盒模型的 position),
const YGDirection childDirection = YGNodeResolveDirection(child, direction);
YGNodeSetPosition(child,
childDirection,
availableInnerMainDim,
availableInnerCrossDim,
availableInnerWidth);
}
if (child->style.positionType == YGPositionTypeAbsolute) {
// absolute 的子專案不參與 flex 排版,用連結串列方式記錄,便於之後拿出來進行另外的排版
if (firstAbsoluteChild == NULL) {
firstAbsoluteChild = child;
}
if (currentAbsoluteChild != NULL) {
currentAbsoluteChild->nextChild = child;
}
currentAbsoluteChild = child;
child->nextChild = NULL;
} else {
// 如果不是 absolute 專案
if (child == singleFlexChild) {
// 如果子專案是唯一的擁有 flex 伸縮屬性的專案,則將 computedFlexBasis 設定為 0
child->layout.computedFlexBasisGeneration = gCurrentGenerationCount;
child->layout.computedFlexBasis = 0;
} else {
// 計算單個子專案的 flex-basis,跳到第 10 步
YGNodeComputeFlexBasisForChild(node,
child,
availableInnerWidth,
widthMeasureMode,
availableInnerHeight,
availableInnerWidth,
availableInnerHeight,
heightMeasureMode,
direction,
config);
}
}
// 計算總體需要的主軸空間 flex-basis
totalOuterFlexBasis +=
child->layout.computedFlexBasis + YGNodeMarginForAxis(child, mainAxis, availableInnerWidth);
}
// 總體的 flex-basis 是否超出可用空間
const bool flexBasisOverflows = measureModeMainDim == YGMeasureModeUndefined
? false
: totalOuterFlexBasis > availableInnerMainDim;
// 如果是專案有換行,同時子專案需要的主軸空間超過了可用空間,同時測量模式是最大值,則將主軸的測量模式設
// 為確切的。因為總的子專案需要的超過了最大可用空間的,就按照最大值的確切的模式去計運算元專案空間。
if (isNodeFlexWrap && flexBasisOverflows && measureModeMainDim == YGMeasureModeAtMost) {
measureModeMainDim = YGMeasureModeExactly;
}
複製程式碼
- 測量單個子專案的 flex-basis,這裡的程式碼有些冗餘,傳入的引數就可以看出來。在這個方法裡主要是獲取主軸大小 flex-basis,它依賴於 style 中設定的值,或者是主軸方向設定的寬度或者高度,如果均未定義,則需要通過測量來自來確定 flex-basis。
static void YGNodeComputeFlexBasisForChild(const YGNodeRef node, // 當前專案
const YGNodeRef child, // 子專案
const float width, // 當前專案可用寬度
const YGMeasureMode widthMode,
const float height,// 當前專案可用高度
const float parentWidth, // 當前專案可用寬度
const float parentHeight, // 當前專案可用高度
const YGMeasureMode heightMode,
const YGDirection direction,
const YGConfigRef config) {
// 確定主軸方向
const YGFlexDirection mainAxis = YGResolveFlexDirection(node->style.flexDirection, direction);
// 確定主軸是否橫向
const bool isMainAxisRow = YGFlexDirectionIsRow(mainAxis);
// 確定主軸空間,下面兩者是相等的,都是當前專案的主軸可用空間,冗餘程式碼。
const float mainAxisSize = isMainAxisRow ? width : height;
const float mainAxisParentSize = isMainAxisRow ? parentWidth : parentHeight;
float childWidth;
float childHeight;
YGMeasureMode childWidthMeasureMode;
YGMeasureMode childHeightMeasureMode;
// 確定子專案主軸初始化大小
const float resolvedFlexBasis =
YGResolveValue(YGNodeResolveFlexBasisPtr(child), mainAxisParentSize);
// 子專案橫縱向大小是否設定
const bool isRowStyleDimDefined = YGNodeIsStyleDimDefined(child, YGFlexDirectionRow, parentWidth);
const bool isColumnStyleDimDefined =
YGNodeIsStyleDimDefined(child, YGFlexDirectionColumn, parentHeight);
if (!YGFloatIsUndefined(resolvedFlexBasis) && !YGFloatIsUndefined(mainAxisSize)) {
// 如果主軸初始化大小確定的,同時專案給予子專案的主軸空間是確定的,設定子專案的 layout.computedFlexBasis 為確定的值,同時保證能相容內邊距和邊框
if (YGFloatIsUndefined(child->layout.computedFlexBasis) ||
(YGConfigIsExperimentalFeatureEnabled(child->config, YGExperimentalFeatureWebFlexBasis) &&
child->layout.computedFlexBasisGeneration != gCurrentGenerationCount)) {
child->layout.computedFlexBasis =
fmaxf(resolvedFlexBasis, YGNodePaddingAndBorderForAxis(child, mainAxis, parentWidth));
}
} else if (isMainAxisRow && isRowStyleDimDefined) {
// 主軸是橫向,同時子專案的橫向大小確定,flex-basis 為 auto 時參照子專案的 width,保證相容內邊距和邊框。
child->layout.computedFlexBasis =
fmaxf(YGResolveValue(child->resolvedDimensions[YGDimensionWidth], parentWidth),
YGNodePaddingAndBorderForAxis(child, YGFlexDirectionRow, parentWidth));
} else if (!isMainAxisRow && isColumnStyleDimDefined) {
// 主軸是豎向,同時子專案的豎向大小確定,flex-basis 為 auto 時參照子專案的 height,保證相容內邊距和邊框。
child->layout.computedFlexBasis =
fmaxf(YGResolveValue(child->resolvedDimensions[YGDimensionHeight], parentHeight),
YGNodePaddingAndBorderForAxis(child, YGFlexDirectionColumn, parentWidth));
} else {
// 設定子專案初始值,childWidth childHeight 指子專案怎個盒子模型大小
childWidth = YGUndefined;
childHeight = YGUndefined;
childWidthMeasureMode = YGMeasureModeUndefined;
childHeightMeasureMode = YGMeasureModeUndefined;
// 確定子專案橫豎向的外邊距
const float marginRow = YGNodeMarginForAxis(child, YGFlexDirectionRow, parentWidth);
const float marginColumn = YGNodeMarginForAxis(child, YGFlexDirectionColumn, parentWidth);
// 當子專案寬高是被設定的,則直接使用設定值,並且測量模式設定為確切的
if (isRowStyleDimDefined) {
childWidth =
YGResolveValue(child->resolvedDimensions[YGDimensionWidth], parentWidth) + marginRow;
childWidthMeasureMode = YGMeasureModeExactly;
}
if (isColumnStyleDimDefined) {
childHeight =
YGResolveValue(child->resolvedDimensions[YGDimensionHeight], parentHeight) + marginColumn;
childHeightMeasureMode = YGMeasureModeExactly;
}
// 當主軸是豎向,同時 overflow 模式是 scroll。或者 overflow 不是 scroll。同時子專案寬度未定義
// 和該專案可用寬度是定義的,則子專案使用該專案的可用空間,並設定測量模式為最大值。
// 這個沒有在 W3C 的標準中,但是主流的瀏覽器都支援這個邏輯。這種情況可以以 scrollview 為例思考一
// 下,子專案交叉軸的最大距離不應該超過父專案可用的大小,另外如果本身 div 不支援 scroll,那麼給子項
// 目的可用空間也應該是子專案的最大可用空間。
if ((!isMainAxisRow && node->style.overflow == YGOverflowScroll) ||
node->style.overflow != YGOverflowScroll) {
if (YGFloatIsUndefined(childWidth) && !YGFloatIsUndefined(width)) {
childWidth = width;
childWidthMeasureMode = YGMeasureModeAtMost;
}
}
// 當主軸是橫向,同時 overflow 模式是 scroll。或者 overflow 不是 scroll。同時子專案寬度未定義
// 和該專案可用寬度是定義的,則子專案使用該專案的可用空間,並設定測量模式為最大值。
if ((isMainAxisRow && node->style.overflow == YGOverflowScroll) ||
node->style.overflow != YGOverflowScroll) {
if (YGFloatIsUndefined(childHeight) && !YGFloatIsUndefined(height)) {
childHeight = height;
childHeightMeasureMode = YGMeasureModeAtMost;
}
}
// 在專案的交叉軸上,專案的交叉軸空間是確切的,而子專案的方向大小沒有設定,同時子專案的 align-self
// 和專案的 align-items 得出的結果是 stretch 拉伸,則子專案的交叉軸上的大小應該設定為專案的交叉軸
// 大小,並設定模式為確切的。
if (!isMainAxisRow && !YGFloatIsUndefined(width) && !isRowStyleDimDefined &&
widthMode == YGMeasureModeExactly && YGNodeAlignItem(node, child) == YGAlignStretch) {
childWidth = width;
childWidthMeasureMode = YGMeasureModeExactly;
}
if (isMainAxisRow && !YGFloatIsUndefined(height) && !isColumnStyleDimDefined &&
heightMode == YGMeasureModeExactly && YGNodeAlignItem(node, child) == YGAlignStretch) {
childHeight = height;
childHeightMeasureMode = YGMeasureModeExactly;
}
// 如果子專案橫縱比有設定,則主軸上的 flex-basis 可根據確切的橫縱值去設定。
// 但是這裡為什麼要返回呢?為什麼不遍歷孩子?aspectRatio 這個是 Yoga 自己的屬性,瀏覽器沒有,
if (!YGFloatIsUndefined(child->style.aspectRatio)) {
// 主軸方向是豎向,同時寬度是確切的,那麼 flex-basis 為寬度除以橫縱比,並做邊距邊框限制
if (!isMainAxisRow && childWidthMeasureMode == YGMeasureModeExactly) {
child->layout.computedFlexBasis =
fmaxf((childWidth - marginRow) / child->style.aspectRatio,
YGNodePaddingAndBorderForAxis(child, YGFlexDirectionColumn, parentWidth));
return;
} else if (isMainAxisRow && childHeightMeasureMode == YGMeasureModeExactly) {
// 主軸方向是橫向,同時高度是確切的,那麼 flex-basis 為高度乘以橫縱比,並做邊距邊框限制
child->layout.computedFlexBasis =
fmaxf((childHeight - marginColumn) * child->style.aspectRatio,
YGNodePaddingAndBorderForAxis(child, YGFlexDirectionRow, parentWidth));
return;
}
}
// 將子專案寬高進行最大值限制
YGConstrainMaxSizeForMode(
child, YGFlexDirectionRow, parentWidth, parentWidth, &childWidthMeasureMode, &childWidth);
YGConstrainMaxSizeForMode(child,
YGFlexDirectionColumn,
parentHeight,
parentWidth,
&childHeightMeasureMode,
&childHeight);
// 返回到了第 3 步了,僅呼叫子專案測量的方法。
YGLayoutNodeInternal(child,
childWidth,
childHeight,
direction,
childWidthMeasureMode,
childHeightMeasureMode,
parentWidth,
parentHeight,
false,
"measure",
config);
// 子專案的主軸 flex-basis 在進行測量後得出並進行限制。
child->layout.computedFlexBasis =
fmaxf(child->layout.measuredDimensions[dim[mainAxis]],
YGNodePaddingAndBorderForAxis(child, mainAxis, parentWidth));
}
// 用於記錄是否在計運算元專案的 flex-basis 時候進行了子專案的遞迴測量
child->layout.computedFlexBasisGeneration = gCurrentGenerationCount;
}
// 獲取專案的主軸初始化大小 flex-basis 值
static inline const YGValue *YGNodeResolveFlexBasisPtr(const YGNodeRef node) {
// 如果設定的 flex-basis 不為 auto 或 undefined 則使用設定值
if (node->style.flexBasis.unit != YGUnitAuto && node->style.flexBasis.unit != YGUnitUndefined) {
return &node->style.flexBasis;
}
// 如果 flex 被設定,同時大於 0, 則在使用 web 預設情況下返回 auto ,否則返回 zero,flex-basis 為0
if (!YGFloatIsUndefined(node->style.flex) && node->style.flex > 0.0f) {
return node->config->useWebDefaults ? &YGValueAuto : &YGValueZero;
}
return &YGValueAuto;
}
// 為設定的大小,限制最大值
static void YGConstrainMaxSizeForMode(const YGNodeRef node,
const enum YGFlexDirection axis,
const float parentAxisSize,
const float parentWidth,
YGMeasureMode *mode,
float *size) {
const float maxSize = YGResolveValue(&node->style.maxDimensions[dim[axis]], parentAxisSize) +
YGNodeMarginForAxis(node, axis, parentWidth);
switch (*mode) {
case YGMeasureModeExactly:
case YGMeasureModeAtMost:
// 如果最大值設定了,則以最大值為 size
*size = (YGFloatIsUndefined(maxSize) || *size < maxSize) ? *size : maxSize;
break;
case YGMeasureModeUndefined:
// 㘝外部設定進來的未定義,而最大值存在,則使用最大值模式,及使用其值。
if (!YGFloatIsUndefined(maxSize)) {
*mode = YGMeasureModeAtMost;
*size = maxSize;
}
break;
}
}
複製程式碼
- 計算完所有子專案在主軸上的基準大小,就要根據基準大小開始在專案中放置子專案,同時統計行數。當然這個方法步驟很長,還包括了伸縮的操作。這裡先進行行數統計的方法分析,主要是看子專案的盒子模型的主軸大小的累加是否超過了可用的主軸空間,是則換行,並計算剩餘空間,以便後續的伸縮操作。
// 每一行開始的子專案索引
uint32_t startOfLineIndex = 0;
// 每一行結束的子專案索引
uint32_t endOfLineIndex = 0;
// 行數
uint32_t lineCount = 0;
// 用於統計交叉軸上所有行所需要的大小
float totalLineCrossDim = 0;
// 記錄所有行中主軸上最大的大小
float maxLineMainDim = 0;
// 遍歷所有行。當一行的空間被子專案填滿了,就通過該迴圈計算下一行。
for (; endOfLineIndex < childCount; lineCount++, startOfLineIndex = endOfLineIndex) {
// 當前行中的子專案數量
uint32_t itemsOnLine = 0;
// 被具有確切的 flex-basis 消耗的總空間,排除了具有 display:none absolute flex-grow flex-shrink 的子專案
float sizeConsumedOnCurrentLine = 0;
float sizeConsumedOnCurrentLineIncludingMinConstraint = 0;
// 記錄 flex-grow 的總數(分母)
float totalFlexGrowFactors = 0;
// 記錄 flex-shrink 的總數(分母)
float totalFlexShrinkScaledFactors = 0;
// 記錄可伸縮的子專案,方便待會進行遍歷。
YGNodeRef firstRelativeChild = NULL;
YGNodeRef currentRelativeChild = NULL;
// 將孩子放入當前行,如果當前行被佔滿,則跳出該迴圈。
for (uint32_t i = startOfLineIndex; i < childCount; i++, endOfLineIndex++) {
const YGNodeRef child = YGNodeListGet(node->children, i);
if (child->style.display == YGDisplayNone) {
continue;
}
child->lineIndex = lineCount;
if (child->style.positionType != YGPositionTypeAbsolute) {
// 計算主軸的外邊距
const float childMarginMainAxis = YGNodeMarginForAxis(child, mainAxis, availableInnerWidth);
// 限制子專案的 flex-basis 在子專案最大值和最小值間。
const float flexBasisWithMaxConstraints =
fminf(YGResolveValue(&child->style.maxDimensions[dim[mainAxis]], mainAxisParentSize),
fmaxf(YGResolveValue(&child->style.minDimensions[dim[mainAxis]],
mainAxisParentSize),
child->layout.computedFlexBasis));
const float flexBasisWithMinAndMaxConstraints =
fmaxf(YGResolveValue(&child->style.minDimensions[dim[mainAxis]], mainAxisParentSize),
flexBasisWithMaxConstraints);
// 如果專案是允許換行的,同時當前行已經有多於一個元素了,當該子專案放入時,其最限制大小與累計的限
// 制大小的和超出了主軸可用空間,那麼則將這個子專案放到下一行中。
if (sizeConsumedOnCurrentLineIncludingMinConstraint + flexBasisWithMinAndMaxConstraints +
childMarginMainAxis >
availableInnerMainDim &&
isNodeFlexWrap && itemsOnLine > 0) {
break;
}
// 記錄當前消耗的總共空間,和當前消耗的具有限制的總空間,為什麼要記錄兩個值?加星*
sizeConsumedOnCurrentLineIncludingMinConstraint +=
flexBasisWithMinAndMaxConstraints + childMarginMainAxis;
sizeConsumedOnCurrentLine += flexBasisWithMaxConstraints + childMarginMainAxis;
itemsOnLine++;
// 如果是一個可伸縮的自專案,記錄累計的 flex-shrink 和 flex-grow 並構建連結串列
if (YGNodeIsFlex(child)) {
totalFlexGrowFactors += YGResolveFlexGrow(child);
// 注意注意:flex-shrink 和 flex-grow 不一樣,flex-shrink 是需要參照 flex-basis 進行整體縮放的比例控制。
totalFlexShrinkScaledFactors +=
-YGNodeResolveFlexShrink(child) * child->layout.computedFlexBasis;
}
// 這裡其實可以寫在上面的括號上,記錄可伸縮子專案的連結串列
if (firstRelativeChild == NULL) {
firstRelativeChild = child;
}
if (currentRelativeChild != NULL) {
currentRelativeChild->nextChild = child;
}
currentRelativeChild = child;
child->nextChild = NULL;
}
}
// 如果不需要測量交叉軸,或者不是排版流程,則跳過測量和排版伸縮孩子的過程
const bool canSkipFlex = !performLayout && measureModeCrossDim == YGMeasureModeExactly;
// 為了方便去進行孩子的主軸排版位置計算,用 leadingMainDim 表示起始邊沿距離第一個元素的距離
// betweenMainDim 表示每個元素之間的距離。
float leadingMainDim = 0;
float betweenMainDim = 0;
// 如果可用的主軸空間的測量模式不為確切,必須確保主軸可用空間要在最大值和最小值範圍內
if (measureModeMainDim != YGMeasureModeExactly) {
if (!YGFloatIsUndefined(minInnerMainDim) && sizeConsumedOnCurrentLine < minInnerMainDim) {
// 當主軸的空間大小是已知的,則需要根據最大值和最小值來計算可用主軸空間
availableInnerMainDim = minInnerMainDim;
} else if (!YGFloatIsUndefined(maxInnerMainDim) &&
sizeConsumedOnCurrentLine > maxInnerMainDim) {
// 當主軸空間未知,則預設使用被消耗的空間作為可用主軸空間,即沒有剩餘的空間給可以伸縮的子專案。
availableInnerMainDim = maxInnerMainDim;
} else {
if (!node->config->useLegacyStretchBehaviour &&
(totalFlexGrowFactors == 0 || YGResolveFlexGrow(node) == 0)) {
// 當沒有任何可伸縮的孩子,同時子專案所佔用總大小也在限制內,則使用該大小做為主軸可用空間,
// 因為後續不需要多餘的空間再做任何變化,。
availableInnerMainDim = sizeConsumedOnCurrentLine;
}
}
}
// 確定剩餘的可用空間
float remainingFreeSpace = 0;
if (!YGFloatIsUndefined(availableInnerMainDim)) {
// 當主軸可用空間是確定時,剩餘空間為主軸可用空間去掉被無法伸縮專案所佔用的總空間
remainingFreeSpace = availableInnerMainDim - sizeConsumedOnCurrentLine;
} else if (sizeConsumedOnCurrentLine < 0) {
// 當主軸可用空間不確定時,代表該專案的主軸的空間大小由孩子決定。同時非伸縮專案所佔用的總空間為負值
// 時,使用其負數作為剩餘可用空間。當 flex-wrap 為 nowrap 時,這裡很好理解,假設某個子專案的
// margin-left 為負數,且絕對值大於所有佔用的空間,本身父專案由孩子決定大小,而孩子這時候使父親整
// 體大小為 0,所以可用來縮放的空間就是 0 - 佔用的空間。當 flex-wrap 為 wrap 時,在上述情況下
// 的 web 表現不是這樣子,這個問題有待研究,加星*。
remainingFreeSpace = -sizeConsumedOnCurrentLine;
}
// 記錄原來的剩餘空間和被使用後剩下的空間
const float originalRemainingFreeSpace = remainingFreeSpace;
float deltaFreeSpace = 0;
複製程式碼
- 如果不能跳過計算伸縮子專案的操作,則進入伸縮子專案的測量和排版的方法中。這個步驟主要做兩次遍歷去填充剩餘的空間,第一次遍歷去除受大小限制的縮放的專案所佔用的看見和縮放因子,第二次遍歷確定所有縮放專案的大小。接著就是確定交叉軸大小和模式,進行遞迴的孩子的測量或者排版操作。
if (!canSkipFlex) {
float childFlexBasis;
float flexShrinkScaledFactor;
float flexGrowFactor;
float baseMainSize;
float boundMainSize;
// 第一次遍歷,判斷可伸縮的子專案是不是受最大值和最小值限制。如果是則去除這些子專案在伸縮因子上的影響
// 即,這些子專案不會伸縮超過最大值和最小值的限制。下面兩個值用於記錄需要去除的影響值。
float deltaFlexShrinkScaledFactors = 0;
float deltaFlexGrowFactors = 0;
currentRelativeChild = firstRelativeChild;
while (currentRelativeChild != NULL) {
// 計算在最大最小值限制範圍內的主軸 flex-basis 空間
childFlexBasis =
fminf(YGResolveValue(¤tRelativeChild->style.maxDimensions[dim[mainAxis]],
mainAxisParentSize),
fmaxf(YGResolveValue(¤tRelativeChild->style.minDimensions[dim[mainAxis]],
mainAxisParentSize),
currentRelativeChild->layout.computedFlexBasis));
// 如果是空間不足情況下,需要縮小。
if (remainingFreeSpace < 0) {
// 計算縮小因子
flexShrinkScaledFactor = -YGNodeResolveFlexShrink(currentRelativeChild) * childFlexBasis;
if (flexShrinkScaledFactor != 0) {
// 計算縮小後的專案的主軸大小
baseMainSize =
childFlexBasis +
remainingFreeSpace / totalFlexShrinkScaledFactors * flexShrinkScaledFactor;
// 計算在最大最小值限制後的縮小主軸大小
boundMainSize = YGNodeBoundAxis(currentRelativeChild,
mainAxis,
baseMainSize,
availableInnerMainDim,
availableInnerWidth);
// 如果是受限制影響,則去除該子專案在縮小因子上額影響,
if (baseMainSize != boundMainSize) {
// 累計記錄這類子專案受限制後可縮小的空間和因子,後面從總的可用空間和因子中減去
deltaFreeSpace -= boundMainSize - childFlexBasis;
deltaFlexShrinkScaledFactors -= flexShrinkScaledFactor;
}
}
} else if (remainingFreeSpace > 0) {
// 在空間充足的情況下進行子專案的伸展
// 計算當前子專案的放大因子
flexGrowFactor = YGResolveFlexGrow(currentRelativeChild);
if (flexGrowFactor != 0) {
// 計算放大後子專案的主軸大小
baseMainSize =
childFlexBasis + remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor;
// 將放大後的大小進行限制
boundMainSize = YGNodeBoundAxis(currentRelativeChild,
mainAxis,
baseMainSize,
availableInnerMainDim,
availableInnerWidth);
// 如果受限制影響,則在記錄這類專案可以放大的最大空間和其因子,後面從總的可用空間和因子中減去
if (baseMainSize != boundMainSize) {
deltaFreeSpace -= boundMainSize - childFlexBasis;
deltaFlexGrowFactors -= flexGrowFactor;
}
}
}
currentRelativeChild = currentRelativeChild->nextChild;
}
// 從總的縮小因子中減去記錄的待去除的因子
totalFlexShrinkScaledFactors += deltaFlexShrinkScaledFactors;
// 從總的放大因子中減去記錄的待去除的因子
totalFlexGrowFactors += deltaFlexGrowFactors;
// 從總的剩餘空間中減去記錄的待去除的空間
remainingFreeSpace += deltaFreeSpace;
// 第二次遍歷,確定所有可伸縮子專案的大小
deltaFreeSpace = 0;
currentRelativeChild = firstRelativeChild;
while (currentRelativeChild != NULL) {
// 計運算元專案主軸需要的大小
childFlexBasis =
fminf(YGResolveValue(¤tRelativeChild->style.maxDimensions[dim[mainAxis]],
mainAxisParentSize),
fmaxf(YGResolveValue(¤tRelativeChild->style.minDimensions[dim[mainAxis]],
mainAxisParentSize),
currentRelativeChild->layout.computedFlexBasis));
// 用於記錄子專案經過縮放後的大小
float updatedMainSize = childFlexBasis;
if (remainingFreeSpace < 0) {
// 當進行縮小時,獲取縮小因子
flexShrinkScaledFactor = -YGNodeResolveFlexShrink(currentRelativeChild) * childFlexBasis;
if (flexShrinkScaledFactor != 0) {
float childSize;
// 根據縮小因子計算縮小後的子專案大小
if (totalFlexShrinkScaledFactors == 0) {
childSize = childFlexBasis + flexShrinkScaledFactor;
} else {
childSize =
childFlexBasis +
(remainingFreeSpace / totalFlexShrinkScaledFactors) * flexShrinkScaledFactor;
}
// 將縮小後的大小進行限制,獲得最終大小
updatedMainSize = YGNodeBoundAxis(currentRelativeChild,
mainAxis,
childSize,
availableInnerMainDim,
availableInnerWidth);
}
} else if (remainingFreeSpace > 0) {
// 當子專案進行放大時, 獲取放大因子
flexGrowFactor = YGResolveFlexGrow(currentRelativeChild);
if (flexGrowFactor != 0) {
// 根據放大因子計算放大後的大小並進行限制
updatedMainSize =
YGNodeBoundAxis(currentRelativeChild,
mainAxis,
childFlexBasis +
remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor,
availableInnerMainDim,
availableInnerWidth);
}
}
// 記錄剩餘的空間
deltaFreeSpace -= updatedMainSize - childFlexBasis;
// 計算主軸和交叉軸的外邊距
const float marginMain =
YGNodeMarginForAxis(currentRelativeChild, mainAxis, availableInnerWidth);
const float marginCross =
YGNodeMarginForAxis(currentRelativeChild, crossAxis, availableInnerWidth);
float childCrossSize;
// 子專案的主軸大小為縮放後的大小加外邊距(盒子大小)
float childMainSize = updatedMainSize + marginMain;
YGMeasureMode childCrossMeasureMode;
YGMeasureMode childMainMeasureMode = YGMeasureModeExactly;
if (!YGFloatIsUndefined(availableInnerCrossDim) &&
!YGNodeIsStyleDimDefined(currentRelativeChild, crossAxis, availableInnerCrossDim) &&
measureModeCrossDim == YGMeasureModeExactly &&
!(isNodeFlexWrap && flexBasisOverflows) &&
YGNodeAlignItem(node, currentRelativeChild) == YGAlignStretch) {
// 當父親給的可用空間和模式為確切,子專案本身大小為設定,同時父親是可以單行排版的,
// 子專案在交叉軸上的排版方向是拉伸的,則子專案交叉軸大小拉伸與父親相同,測量模式為確切。
childCrossSize = availableInnerCrossDim;
childCrossMeasureMode = YGMeasureModeExactly;
} else if (!YGNodeIsStyleDimDefined(currentRelativeChild,
crossAxis,
availableInnerCrossDim)) {
// 當子專案的交叉軸大小為未設定,那麼子專案交叉軸大小為父親的可用大小,如果是確切則模式為最大
// 值,否則為未知。
childCrossSize = availableInnerCrossDim;
childCrossMeasureMode =
YGFloatIsUndefined(childCrossSize) ? YGMeasureModeUndefined : YGMeasureModeAtMost;
} else {
// 出現其他的情況,如果專案的交叉軸大小一致,則為確切,否則為未知。當交叉軸的單位為百分比時
// 這裡有個特殊操作,情況必須在其為可伸縮情況下,測量模式變為未知,大小交給子專案的孩子決定。
childCrossSize = YGResolveValue(currentRelativeChild->resolvedDimensions[dim[crossAxis]],
availableInnerCrossDim) +
marginCross;
const bool isLoosePercentageMeasurement =
currentRelativeChild->resolvedDimensions[dim[crossAxis]]->unit == YGUnitPercent &&
measureModeCrossDim != YGMeasureModeExactly;
childCrossMeasureMode = YGFloatIsUndefined(childCrossSize) || isLoosePercentageMeasurement
? YGMeasureModeUndefined
: YGMeasureModeExactly;
}
// 如果橫縱比定義了,則要根據橫縱比進行調整,這裡其實不太協調,因為調整完之後如果主軸大小變了
// 上面的規則都行不通了。
if (!YGFloatIsUndefined(currentRelativeChild->style.aspectRatio)) {
childCrossSize = fmaxf(
isMainAxisRow
? (childMainSize - marginMain) / currentRelativeChild->style.aspectRatio
: (childMainSize - marginMain) * currentRelativeChild->style.aspectRatio,
YGNodePaddingAndBorderForAxis(currentRelativeChild, crossAxis, availableInnerWidth));
childCrossMeasureMode = YGMeasureModeExactly;
if (YGNodeIsFlex(currentRelativeChild)) {
childCrossSize = fminf(childCrossSize - marginCross, availableInnerCrossDim);
childMainSize =
marginMain + (isMainAxisRow
? childCrossSize * currentRelativeChild->style.aspectRatio
: childCrossSize / currentRelativeChild->style.aspectRatio);
}
childCrossSize += marginCross;
}
// 進行子專案可用主軸交叉軸大小限制
YGConstrainMaxSizeForMode(currentRelativeChild,
mainAxis,
availableInnerMainDim,
availableInnerWidth,
&childMainMeasureMode,
&childMainSize);
YGConstrainMaxSizeForMode(currentRelativeChild,
crossAxis,
availableInnerCrossDim,
availableInnerWidth,
&childCrossMeasureMode,
&childCrossSize);
// 確定在交叉軸上排版模式是否拉伸模式
const bool requiresStretchLayout =
!YGNodeIsStyleDimDefined(currentRelativeChild, crossAxis, availableInnerCrossDim) &&
YGNodeAlignItem(node, currentRelativeChild) == YGAlignStretch;
const float childWidth = isMainAxisRow ? childMainSize : childCrossSize;
const float childHeight = !isMainAxisRow ? childMainSize : childCrossSize;
const YGMeasureMode childWidthMeasureMode =
isMainAxisRow ? childMainMeasureMode : childCrossMeasureMode;
const YGMeasureMode childHeightMeasureMode =
!isMainAxisRow ? childMainMeasureMode : childCrossMeasureMode;
// 遞迴呼叫子專案的孩子的排版,這裡決定遞迴時是以測量還是排版模式由 performLayout 和
// requireStretchLayout 決定,前一個 flag 正常,後一個 flag 的作用主要是如果是不需要拉伸的
// 那麼就直接排版,否則如果是要拉伸就只是測量。因為在後面排版 align-content 還可能根據
// 交叉軸是否 stretch 導致一次因為拉伸出現的大小改變,而在遞迴時需要
// 重新觸發排版,所以當交叉軸是 stretch 時,這裡的遞迴使用測量可以減少一次無用的排版遞迴操作
YGLayoutNodeInternal(currentRelativeChild,
childWidth,
childHeight,
direction,
childWidthMeasureMode,
childHeightMeasureMode,
availableInnerWidth,
availableInnerHeight,
performLayout && !requiresStretchLayout,
"flex",
config);
currentRelativeChild = currentRelativeChild->nextChild;
}
}
複製程式碼
- 在當前行子專案在主軸的大小均確認完畢後,需要再次確定剩餘空間,以便根據專案主軸的排版方式確定子專案排版的位置和確定專案主軸和交叉軸大小。
remainingFreeSpace = originalRemainingFreeSpace + deltaFreeSpace;
// 如果專案的主軸測量模式是最大值,同時有剩餘空間沒有使用(沒有可縮放孩子),則根據專案主軸最小空間計算
// 剩餘空間,如果最小空間未定義,同時專案大小未知,意味可以不需要剩餘空間。
if (measureModeMainDim == YGMeasureModeAtMost && remainingFreeSpace > 0) {
if (node->style.minDimensions[dim[mainAxis]].unit != YGUnitUndefined &&
YGResolveValue(&node->style.minDimensions[dim[mainAxis]], mainAxisParentSize) >= 0) {
remainingFreeSpace =
fmaxf(0,
YGResolveValue(&node->style.minDimensions[dim[mainAxis]], mainAxisParentSize) -
(availableInnerMainDim - remainingFreeSpace));
} else {
remainingFreeSpace = 0;
}
}
// 計算當前行中子專案在主軸上的外邊距 margin 是否有 auto 的設定。當有 auto 設定時,剩餘的空間需要均勻分配給
// 這些子專案的主軸的前沿和後沿的 margin,但是分配的這個 margin 由於是額外的,在這裡是直接由父親計算好,沒有
// 在遞迴時候把這個當做子專案盒模型一部分傳遞,但是它仍然是子專案盒模型的 margin。
int numberOfAutoMarginsOnCurrentLine = 0;
for (uint32_t i = startOfLineIndex; i < endOfLineIndex; i++) {
const YGNodeRef child = YGNodeListGet(node->children, i);
if (child->style.positionType == YGPositionTypeRelative) {
if (YGMarginLeadingValue(child, mainAxis)->unit == YGUnitAuto) {
numberOfAutoMarginsOnCurrentLine++;
}
if (YGMarginTrailingValue(child, mainAxis)->unit == YGUnitAuto) {
numberOfAutoMarginsOnCurrentLine++;
}
}
}
// 當子專案中沒有 margin auto 的設定,則可以依照 flex 的 justify-content 來設定主軸上的排版位置
// leadingMainDim 為首個子專案距離主軸前沿的距離,betweenMaindDim 子專案之間的間距
if (numberOfAutoMarginsOnCurrentLine == 0) {
switch (justifyContent) {
case YGJustifyCenter:
// 居中設定,主軸前沿的距離為剩餘空間一半
leadingMainDim = remainingFreeSpace / 2;
break;
case YGJustifyFlexEnd:
// 尾對齊,主軸前沿的距離為所有剩餘空間
leadingMainDim = remainingFreeSpace;
break;
case YGJustifySpaceBetween:
// 兩邊對齊,子專案間隔相等
if (itemsOnLine > 1) {
betweenMainDim = fmaxf(remainingFreeSpace, 0) / (itemsOnLine - 1);
} else {
betweenMainDim = 0;
}
break;
case YGJustifySpaceAround:
// 子專案兩邊的分配的空間相等
betweenMainDim = remainingFreeSpace / itemsOnLine;
leadingMainDim = betweenMainDim / 2;
break;
case YGJustifyFlexStart:
break;
}
}
// 主軸和交叉軸的大小,後面的程式碼也會利用這個變數進行子專案位置排列的計算
float mainDim = leadingPaddingAndBorderMain + leadingMainDim;
float crossDim = 0;
for (uint32_t i = startOfLineIndex; i < endOfLineIndex; i++) {
const YGNodeRef child = YGNodeListGet(node->children, i);
if (child->style.display == YGDisplayNone) {
continue;
}
if (child->style.positionType == YGPositionTypeAbsolute &&
YGNodeIsLeadingPosDefined(child, mainAxis)) {
if (performLayout) {
// 當子專案是 absolute 絕對佈局時,top 和 left 已經定義,則進行相應位置的擺放
child->layout.position[pos[mainAxis]] =
YGNodeLeadingPosition(child, mainAxis, availableInnerMainDim) +
YGNodeLeadingBorder(node, mainAxis) +
YGNodeLeadingMargin(child, mainAxis, availableInnerWidth);
}
} else {
// 絕對佈局的子專案不參與 flex 佈局
if (child->style.positionType == YGPositionTypeRelative) {
if (YGMarginLeadingValue(child, mainAxis)->unit == YGUnitAuto) {
// 當然子專案的前沿 margin 是 auto 時,主軸距離增加
mainDim += remainingFreeSpace / numberOfAutoMarginsOnCurrentLine;
}
// 如果是排版步驟,則設定孩子盒模型主軸起點
if (performLayout) {
child->layout.position[pos[mainAxis]] += mainDim;
}
// 當然子專案的後沿 margin 是 auto 時,主軸距離增加
if (YGMarginTrailingValue(child, mainAxis)->unit == YGUnitAuto) {
mainDim += remainingFreeSpace / numberOfAutoMarginsOnCurrentLine;
}
if (canSkipFlex) {
// 如果是跳過了 flex 的步驟,那麼YGNodeDimWithMargin是不能用的,因為裡面使用到的
// measureDim 是還未計算過的,這裡使用 computedFlexBasis。
// 累加子專案的大小,最後可以得出專案的大小
mainDim += betweenMainDim + YGNodeMarginForAxis(child, mainAxis, availableInnerWidth) +
child->layout.computedFlexBasis;
// 因為跳過了 flex 代表交叉軸是確切的(原因看前面程式碼)
crossDim = availableInnerCrossDim;
} else {
// 累加子專案的大小,最後可以得出專案的大小
mainDim += betweenMainDim + YGNodeDimWithMargin(child, mainAxis, availableInnerWidth);
// 專案的交叉軸大小,由最大的子專案交叉軸大小決定
crossDim = fmaxf(crossDim, YGNodeDimWithMargin(child, crossAxis, availableInnerWidth));
}
} else if (performLayout) {
// 放置絕對佈局專案
child->layout.position[pos[mainAxis]] +=
YGNodeLeadingBorder(node, mainAxis) + leadingMainDim;
}
}
}
// 累加尾部邊距和邊框,得到專案最終主軸大小,這個值已經加了內邊距和 border
mainDim += trailingPaddingAndBorderMain;
float containerCrossAxis = availableInnerCrossDim;
if (measureModeCrossDim == YGMeasureModeUndefined ||
measureModeCrossDim == YGMeasureModeAtMost) {
// 當測量模式不是確切的,那麼
// 如果交叉軸大小不是確切的或是最大值,則由最大的孩子的交叉軸值決定專案的交叉軸大小,並確保在限制內
containerCrossAxis = YGNodeBoundAxis(node,
crossAxis,
crossDim + paddingAndBorderAxisCross,
crossAxisParentSize,
parentWidth) -
paddingAndBorderAxisCross;
}
// 如果專案是單行排版,同時交叉軸測量模式為絕對值,則交叉軸大小為可用的交叉軸空間
if (!isNodeFlexWrap && measureModeCrossDim == YGMeasureModeExactly) {
crossDim = availableInnerCrossDim;
}
// 根據最大最小值進行限制,這個值沒有加上內邊距和 border
crossDim = YGNodeBoundAxis(node,
crossAxis,
crossDim + paddingAndBorderAxisCross,
crossAxisParentSize,
parentWidth) - paddingAndBorderAxisCross;
複製程式碼
- 接下來計運算元專案在當前行交叉軸上的排版,這一步驟只在排版下進行,測量階段不進行。如果需要進行 stretch 則需要使子專案進行排版和測量的操作,以便滿足新的空間。這一步結束後,flex 計算行排版資訊的迴圈到此終止。
// 這一步驟只在該專案排版下進行,測量不進行
if (performLayout) {
for (uint32_t i = startOfLineIndex; i < endOfLineIndex; i++) {
const YGNodeRef child = YGNodeListGet(node->children, i);
if (child->style.display == YGDisplayNone) {
continue;
}
if (child->style.positionType == YGPositionTypeAbsolute) {
// 如果子專案是絕對定位,則根據四個定位 top / left / right / bottom 設定在交叉軸上的位置
if (YGNodeIsLeadingPosDefined(child, crossAxis)) {
child->layout.position[pos[crossAxis]] =
YGNodeLeadingPosition(child, crossAxis, availableInnerCrossDim) +
YGNodeLeadingBorder(node, crossAxis) +
YGNodeLeadingMargin(child, crossAxis, availableInnerWidth);
} else {
child->layout.position[pos[crossAxis]] =
YGNodeLeadingBorder(node, crossAxis) +
YGNodeLeadingMargin(child, crossAxis, availableInnerWidth);
}
} else {
float leadingCrossDim = leadingPaddingAndBorderCross;
// 子專案的交叉軸排版可以由父專案決定,自身設定的優先順序更高
const YGAlign alignItem = YGNodeAlignItem(node, child);
// 當子專案在交叉軸的排版是拉伸,同時 marigin 均不是 auto(auto 的話就不需要拉伸,而是自由使
// 用 margin 撐滿),那就需要重新計算孩子在交叉軸上的排版
if (alignItem == YGAlignStretch &&
YGMarginLeadingValue(child, crossAxis)->unit != YGUnitAuto &&
YGMarginTrailingValue(child, crossAxis)->unit != YGUnitAuto) {
// 如果子專案具有確切被設定的交叉軸大小,那麼不需要進行拉伸,否則需要重新排版
if (!YGNodeIsStyleDimDefined(child, crossAxis, availableInnerCrossDim)) {
// 子專案主軸大小使用測量過的大小
float childMainSize = child->layout.measuredDimensions[dim[mainAxis]];
// 子專案交叉軸如果定義了橫軸比則使用橫縱比結果,否則使用當前父親的行交叉軸大小
float childCrossSize =
!YGFloatIsUndefined(child->style.aspectRatio)
? ((YGNodeMarginForAxis(child, crossAxis, availableInnerWidth) +
(isMainAxisRow ? childMainSize / child->style.aspectRatio
: childMainSize * child->style.aspectRatio)))
: crossDim;
// 盒模型
childMainSize += YGNodeMarginForAxis(child, mainAxis, availableInnerWidth);
// 將交叉軸和主軸大小進行範圍限制,這裡主軸使用了確切的測量模式,這裡有個疑惑就是,在
// 前面程式碼設定的主軸測量模式不一定是確切的。關於這個的解答應該是因為這次的測量是之前測量的
// 結果,所以孩子的測量結果不會和之前所測量的有出入。
YGMeasureMode childMainMeasureMode = YGMeasureModeExactly;
YGMeasureMode childCrossMeasureMode = YGMeasureModeExactly;
YGConstrainMaxSizeForMode(child,
mainAxis,
availableInnerMainDim,
availableInnerWidth,
&childMainMeasureMode,
&childMainSize);
YGConstrainMaxSizeForMode(child,
crossAxis,
availableInnerCrossDim,
availableInnerWidth,
&childCrossMeasureMode,
&childCrossSize);
const float childWidth = isMainAxisRow ? childMainSize : childCrossSize;
const float childHeight = !isMainAxisRow ? childMainSize : childCrossSize;
const YGMeasureMode childWidthMeasureMode =
YGFloatIsUndefined(childWidth) ? YGMeasureModeUndefined : YGMeasureModeExactly;
const YGMeasureMode childHeightMeasureMode =
YGFloatIsUndefined(childHeight) ? YGMeasureModeUndefined : YGMeasureModeExactly;
// 遞迴測量排版子專案。
YGLayoutNodeInternal(child,
childWidth,
childHeight,
direction,
childWidthMeasureMode,
childHeightMeasureMode,
availableInnerWidth,
availableInnerHeight,
true,
"stretch",
config);
}
} else {
// 如果不需要拉伸,則根據剩餘空間和排版模式,在交叉軸上放置子專案到對應的位置
// 剩餘交叉軸空間
const float remainingCrossDim =
containerCrossAxis - YGNodeDimWithMargin(child, crossAxis, availableInnerWidth);
if (YGMarginLeadingValue(child, crossAxis)->unit == YGUnitAuto &&
YGMarginTrailingValue(child, crossAxis)->unit == YGUnitAuto) {
// 如果 margin 為 auto,則均勻的分配子專案交叉軸兩側空間。
leadingCrossDim += fmaxf(0.0f, remainingCrossDim / 2);
} else if (YGMarginTrailingValue(child, crossAxis)->unit == YGUnitAuto) {
// 如果尾部 margin 為 auto,則不用做任何操作,因為本身就已經把剩餘空間放在尾部
} else if (YGMarginLeadingValue(child, crossAxis)->unit == YGUnitAuto) {
// 如果前沿 margin 為 auto,則將剩餘空間都放在前沿
leadingCrossDim += fmaxf(0.0f, remainingCrossDim);
} else if (alignItem == YGAlignFlexStart) {
// 如果排版模式是對齊前沿,則不需要做任何操作
} else if (alignItem == YGAlignCenter) {
// 如果排版模式是居中,則將剩餘空間均分
leadingCrossDim += remainingCrossDim / 2;
} else {
// 如果是對其尾部,則剩餘空間都放在前沿
leadingCrossDim += remainingCrossDim;
}
}
// 設定子專案的排版位置
child->layout.position[pos[crossAxis]] += totalLineCrossDim + leadingCrossDim;
}
}
}
// totalLineCrossDim 是多行情況下積累的交叉軸行高
totalLineCrossDim += crossDim;
// 計算專案整體最大行寬。
maxLineMainDim = fmaxf(maxLineMainDim, mainDim);
}
複製程式碼
- 計算 align-content 情況下多行的排版狀況,行之間是沒有間隔的,所以 align-content 是對每一行的子專案位置的一個整體重新確認。這裡的 baseline 是一個值得關注的操作,支隊主軸為橫向起作用。另外在 stretch 情況下,如果子專案是需要拉伸的,則需要重新進行排版。
// align-content 針對的是行的排版方式,僅在排版情況下進行,同時滿足行數大於一行,或者需要根據行的第一個元
// 素的 baseline 文字的基準對齊,並且該專案的交叉軸可用空間是確定值(用於確定剩餘的交叉軸空間,否則為0)
// 注意:行都是撐滿的,行間不會有間距。
if (performLayout && (lineCount > 1 || YGIsBaselineLayout(node)) &&
!YGFloatIsUndefined(availableInnerCrossDim)) {
// 交叉軸中排版完所有行之後剩餘的空間
const float remainingAlignContentDim = availableInnerCrossDim - totalLineCrossDim;
// 對於每一行而言 align-content 分配的多餘空間
float crossDimLead = 0;
// 第一行的專案整體距離前沿的距離
float currentLead = leadingPaddingAndBorderCross;
switch (node->style.alignContent) {
case YGAlignFlexEnd:
// 整體對齊尾部,距離前沿的位置設定為所有剩餘空間
currentLead += remainingAlignContentDim;
break;
case YGAlignCenter:
// 整體居中,距離前沿的位置設定位剩餘空間的一半
currentLead += remainingAlignContentDim / 2;
break;
case YGAlignStretch:
if (availableInnerCrossDim > totalLineCrossDim) {
crossDimLead = remainingAlignContentDim / lineCount;
}
break;
case YGAlignSpaceAround:
// 每一行的前後兩側留下的空間相等
if (availableInnerCrossDim > totalLineCrossDim) {
currentLead += remainingAlignContentDim / (2 * lineCount);
if (lineCount > 1) {
crossDimLead = remainingAlignContentDim / lineCount;
}
} else {
currentLead += remainingAlignContentDim / 2;
}
break;
case YGAlignSpaceBetween:
// 前後兩行對齊兩邊,行間間隔相等
if (availableInnerCrossDim > totalLineCrossDim && lineCount > 1) {
crossDimLead = remainingAlignContentDim / (lineCount - 1);
}
break;
case YGAlignAuto:
case YGAlignFlexStart:
case YGAlignBaseline:
break;
}
// 遍歷所有行,確定當前行的大小,同時為行內的子專案進行放置,確定是否需要重新測量排版
uint32_t endIndex = 0;
for (uint32_t i = 0; i < lineCount; i++) {
const uint32_t startIndex = endIndex;
uint32_t ii;
float lineHeight = 0;
float maxAscentForCurrentLine = 0;
float maxDescentForCurrentLine = 0;
for (ii = startIndex; ii < childCount; ii++) {
const YGNodeRef child = YGNodeListGet(node->children, ii);
if (child->style.display == YGDisplayNone) {
continue;
}
if (child->style.positionType == YGPositionTypeRelative) {
// 根據 lineIndex 找到當前行的子專案
if (child->lineIndex != i) {
break;
}
if (YGNodeIsLayoutDimDefined(child, crossAxis)) {
// 尋找子專案中最大行高
lineHeight = fmaxf(lineHeight,
child->layout.measuredDimensions[dim[crossAxis]] +
YGNodeMarginForAxis(child, crossAxis, availableInnerWidth));
}
if (YGNodeAlignItem(node, child) == YGAlignBaseline) {
// 基準值排版的計算方式是,獲取每個專案中以首個文字的 bottom 為基線(如果沒有則以
// 當前專案的底部為基線)的距離頂部的距離 ascent,距離底部距離 descent,在專案中
// 獲取最大的 maxAscent 和 maxDescent,這兩個的和值就是整個行的高度。而每個專案
// 根據 maxAscent 和基線的差值,就可以計算出對齊基線時距離頂部的位置。
const float ascent =
YGBaseline(child) +
YGNodeLeadingMargin(child, YGFlexDirectionColumn, availableInnerWidth);
const float descent =
child->layout.measuredDimensions[YGDimensionHeight] +
YGNodeMarginForAxis(child, YGFlexDirectionColumn, availableInnerWidth) - ascent;
maxAscentForCurrentLine = fmaxf(maxAscentForCurrentLine, ascent);
maxDescentForCurrentLine = fmaxf(maxDescentForCurrentLine, descent);
lineHeight = fmaxf(lineHeight, maxAscentForCurrentLine + maxDescentForCurrentLine);
}
}
}
// 記錄下一行的起始位置
endIndex = ii;
// 加上根據 align-content 分配的多餘空間
lineHeight += crossDimLead;
// 這個 performLayout 的判斷多餘了
if (performLayout) {
// 對當前行的專案進行交叉軸上的放置
for (ii = startIndex; ii < endIndex; ii++) {
const YGNodeRef child = YGNodeListGet(node->children, ii);
// 忽略 displaynone 節點
if (child->style.display == YGDisplayNone) {
continue;
}
if (child->style.positionType == YGPositionTypeRelative) {
switch (YGNodeAlignItem(node, child)) {
case YGAlignFlexStart: {
// 當交叉軸排版是對齊前沿,則子專案的交叉軸頂部位置為前沿的距離與邊距邊框和值
child->layout.position[pos[crossAxis]] =
currentLead + YGNodeLeadingMargin(child, crossAxis, availableInnerWidth);
break;
}
case YGAlignFlexEnd: {
// 當交叉軸排版是對齊後沿,則子專案的交叉軸頂部位置為前沿距離與去除了自身
// 大小後的空間和值
child->layout.position[pos[crossAxis]] =
currentLead + lineHeight -
YGNodeTrailingMargin(child, crossAxis, availableInnerWidth) -
child->layout.measuredDimensions[dim[crossAxis]];
break;
}
case YGAlignCenter: {
// 當交叉軸居中,則頂部位置為去除自身大小後的空間的一半,同時加上前沿距離
float childHeight = child->layout.measuredDimensions[dim[crossAxis]];
child->layout.position[pos[crossAxis]] =
currentLead + (lineHeight - childHeight) / 2;
break;
}
case YGAlignStretch: {
// 如果是拉伸,則頂部位置就是行開始的位置
child->layout.position[pos[crossAxis]] =
currentLead + YGNodeLeadingMargin(child, crossAxis, availableInnerWidth);
// 重新測量和排版子專案的孩子,只是更新交叉軸的高度
if (!YGNodeIsStyleDimDefined(child, crossAxis, availableInnerCrossDim)) {
const float childWidth =
isMainAxisRow ? (child->layout.measuredDimensions[YGDimensionWidth] +
YGNodeMarginForAxis(child, mainAxis, availableInnerWidth))
: lineHeight;
const float childHeight =
!isMainAxisRow ? (child->layout.measuredDimensions[YGDimensionHeight] +
YGNodeMarginForAxis(child, crossAxis, availableInnerWidth))
: lineHeight;
if (!(YGFloatsEqual(childWidth,
child->layout.measuredDimensions[YGDimensionWidth]) &&
YGFloatsEqual(childHeight,
child->layout.measuredDimensions[YGDimensionHeight]))) {
YGLayoutNodeInternal(child,
childWidth,
childHeight,
direction,
YGMeasureModeExactly,
YGMeasureModeExactly,
availableInnerWidth,
availableInnerHeight,
true,
"multiline-stretch",
config);
}
}
break;
}
case YGAlignBaseline: {
// 如果是以基準排版,則頂部位置確定方式為利用行內最大的基準值減去當前基準值
// 加上前沿距離和邊框邊距,這裡僅設定 position[YGEdgeTop] 的原因是
// baseline 僅對 flex-direction 橫向起效,所以當排版模式為
// baselinse,只要設定 top 位置即可,後續的 reverse 反轉操作也不會發
// 生在交叉軸上。
child->layout.position[YGEdgeTop] =
currentLead + maxAscentForCurrentLine - YGBaseline(child) +
YGNodeLeadingPosition(child, YGFlexDirectionColumn, availableInnerCrossDim);
break;
}
case YGAlignAuto:
case YGAlignSpaceBetween:
case YGAlignSpaceAround:
break;
}
}
}
}
currentLead += lineHeight;
}
}
// 計算基準 baseline 的方法
static float YGBaseline(const YGNodeRef node) {
if (node->baseline != NULL) {
// 如果該專案有設定基準的判定方法,則從該方法中獲取
const float baseline = node->baseline(node,
node->layout.measuredDimensions[YGDimensionWidth],
node->layout.measuredDimensions[YGDimensionHeight]);
YGAssertWithNode(node,
!YGFloatIsUndefined(baseline),
"Expect custom baseline function to not return NaN");
return baseline;
}
// 如果專案本身沒有計算 baseline 的方法,則詢問孩子交叉軸排版方式 align 為 baseline 的孩子
// 如果孩子存在,則尋找其 baseline,否則直接使用當前專案的高度作為 baseline。
YGNodeRef baselineChild = NULL;
const uint32_t childCount = YGNodeGetChildCount(node);
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef child = YGNodeGetChild(node, i);
if (child->lineIndex > 0) {
break;
}
if (child->style.positionType == YGPositionTypeAbsolute) {
continue;
}
if (YGNodeAlignItem(node, child) == YGAlignBaseline) {
baselineChild = child;
break;
}
if (baselineChild == NULL) {
baselineChild = child;
}
}
// 沒有孩子排版方式為 baseline 則使用當前專案的高度作為 baseline。
if (baselineChild == NULL) {
return node->layout.measuredDimensions[YGDimensionHeight];
}
// 否則使用孩子的 baseline 及距離父親的高度作為整體 baseline
const float baseline = YGBaseline(baselineChild);
return baseline + baselineChild->layout.position[YGEdgeTop];
}
複製程式碼
- 計算最終的大小,並對方向反轉的行專案做出正確位置的放置,同時進行絕對佈局的專案的排列。
// 測量大小直接通過可用空間減去外邊距得到,這個值只有當主軸或者交叉軸的測量模式為確切的時候,才具有意義。否則會
// 被下面兩個 if 分支所覆蓋。
node->layout.measuredDimensions[YGDimensionWidth] = YGNodeBoundAxis(
node, YGFlexDirectionRow, availableWidth - marginAxisRow, parentWidth, parentWidth);
node->layout.measuredDimensions[YGDimensionHeight] = YGNodeBoundAxis(
node, YGFlexDirectionColumn, availableHeight - marginAxisColumn, parentHeight, parentWidth);
// 如果測量模式沒有給定具體的主軸大小,或者只有最大值的限制且 overflow 不是 scroll,那麼直接使用最大的行
// 寬作為節點的主軸測量大小,既依大小賴於孩子
if (measureModeMainDim == YGMeasureModeUndefined ||
(node->style.overflow != YGOverflowScroll && measureModeMainDim == YGMeasureModeAtMost)) {
// 進行大小的限制,確保不小於內邊距和邊框之和
node->layout.measuredDimensions[dim[mainAxis]] =
YGNodeBoundAxis(node, mainAxis, maxLineMainDim, mainAxisParentSize, parentWidth);
} else if (measureModeMainDim == YGMeasureModeAtMost &&
node->style.overflow == YGOverflowScroll) {
// 如果測量模式是最大值,同時 overflow 為 scroll,就代表當子專案總體主軸大小超過了所給可用空間
// 則該專案的大小應為可用空間的大小,這是確保可以滑動的前提(孩子總體大小超過父親),這時 overflow 優
// 先級高於 min max。當子專案總體主軸大小小於所給最大空間,則以較小值作為基準,同時需要確保
node->layout.measuredDimensions[dim[mainAxis]] = fmaxf(
fminf(availableInnerMainDim + paddingAndBorderAxisMain,
YGNodeBoundAxisWithinMinAndMax(node, mainAxis, maxLineMainDim, mainAxisParentSize)),
paddingAndBorderAxisMain);
}
// 與上述主軸的含義一致
if (measureModeCrossDim == YGMeasureModeUndefined ||
(node->style.overflow != YGOverflowScroll && measureModeCrossDim == YGMeasureModeAtMost)) {
node->layout.measuredDimensions[dim[crossAxis]] =
YGNodeBoundAxis(node,
crossAxis,
totalLineCrossDim + paddingAndBorderAxisCross,
crossAxisParentSize,
parentWidth);
} else if (measureModeCrossDim == YGMeasureModeAtMost &&
node->style.overflow == YGOverflowScroll) {
node->layout.measuredDimensions[dim[crossAxis]] =
fmaxf(fminf(availableInnerCrossDim + paddingAndBorderAxisCross,
YGNodeBoundAxisWithinMinAndMax(node,
crossAxis,
totalLineCrossDim + paddingAndBorderAxisCross,
crossAxisParentSize)),
paddingAndBorderAxisCross);
}
// 測量的結果仍然是以正常方向進行的,如果當 flex-wrap 是 wrap-reverse,那麼需要將行的交叉軸排版方向反轉
// 這裡實現方式就是遍歷所有孩子,專案整體大小減去子專案頂部距離和大小,達到反轉效果。
if (performLayout && node->style.flexWrap == YGWrapWrapReverse) {
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef child = YGNodeGetChild(node, i);
if (child->style.positionType == YGPositionTypeRelative) {
child->layout.position[pos[crossAxis]] = node->layout.measuredDimensions[dim[crossAxis]] -
child->layout.position[pos[crossAxis]] -
child->layout.measuredDimensions[dim[crossAxis]];
}
}
}
if (performLayout) {
// 在知道專案整體大小之後,就可以進行絕對佈局的孩子的,佈局方式詳見第 17 步
for (currentAbsoluteChild = firstAbsoluteChild; currentAbsoluteChild != NULL;
currentAbsoluteChild = currentAbsoluteChild->nextChild) {
YGNodeAbsoluteLayoutChild(node,
currentAbsoluteChild,
availableInnerWidth,
isMainAxisRow ? measureModeMainDim : measureModeCrossDim,
availableInnerHeight,
direction,
config);
}
// 那 flex-direction 上的反轉呢?如 row-reverse。在確定 crossAxis rowAxis,在計算 position
// 時候,當為 row 時,position[pos[mainAxis]] 設定的是 left 的位置,當為 row-reverse,設定的
// 是 rigint 的位置,也就是當 reverse 的時候並沒有確切的設定 top 和 right 位置,而 top 和 right
// 是 Yoga 用於定位的,所以在此對 reverse 情況下的主軸和交叉軸需要重新設定 top 和 right的值
const bool needsMainTrailingPos =
mainAxis == YGFlexDirectionRowReverse || mainAxis == YGFlexDirectionColumnReverse;
// 交叉軸沒有可能是 YGFlexDirectionColumnReverse。當 YGDirection 為 RTL 時候,才可能是 YGFlexDirectionRowReverse
const bool needsCrossTrailingPos =
crossAxis == YGFlexDirectionRowReverse || crossAxis == YGFlexDirectionColumnReverse;
// 重新設定 top 和 right的值
if (needsMainTrailingPos || needsCrossTrailingPos) {
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef child = YGNodeListGet(node->children, i);
if (child->style.display == YGDisplayNone) {
continue;
}
if (needsMainTrailingPos) {
YGNodeSetChildTrailingPosition(node, child, mainAxis);
}
if (needsCrossTrailingPos) {
YGNodeSetChildTrailingPosition(node, child, crossAxis);
}
}
}
}
// 設定 node.layout.position 的 top 和 right 值
static void YGNodeSetChildTrailingPosition(const YGNodeRef node,
const YGNodeRef child,
const YGFlexDirection axis) {
const float size = child->layout.measuredDimensions[dim[axis]];
// 當需要設定的是 top position 時,計算方式為 parent size - child size - child bottom position。加上 size 的原因是因為在排版階段,設定的 top 值實質被當做 position[YGEdgeBottom],因此在 reverse 時候需要減去 position[YGEdgeBottom] 和 child size,獲取反轉後 top position。對於 left position 的計算方式概念一樣。
child->layout.position[trailing[axis]] =
node->layout.measuredDimensions[dim[axis]] - size - child->layout.position[pos[axis]];
}
複製程式碼
- 絕對佈局專案的排版,對其子專案的測量和排版。絕對佈局的專案主要依據 width / height 和 top / right / left / bottom 進行大小確定和定位,如果大小不能確定,則需要先進行孩子的測量再排版。
static void YGNodeAbsoluteLayoutChild(const YGNodeRef node,
const YGNodeRef child,
const float width,
const YGMeasureMode widthMode,
const float height,
const YGDirection direction,
const YGConfigRef config) {
// 確定主軸和交叉軸的方向
const YGFlexDirection mainAxis = YGResolveFlexDirection(node->style.flexDirection, direction);
const YGFlexDirection crossAxis = YGFlexDirectionCross(mainAxis, direction);
const bool isMainAxisRow = YGFlexDirectionIsRow(mainAxis);
// 預設測量模式和測量大小為未知
float childWidth = YGUndefined;
float childHeight = YGUndefined;
YGMeasureMode childWidthMeasureMode = YGMeasureModeUndefined;
YGMeasureMode childHeightMeasureMode = YGMeasureModeUndefined;
// 確定橫向和豎向上的綜外邊距
const float marginRow = YGNodeMarginForAxis(child, YGFlexDirectionRow, width);
const float marginColumn = YGNodeMarginForAxis(child, YGFlexDirectionColumn, width);
if (YGNodeIsStyleDimDefined(child, YGFlexDirectionRow, width)) {
// 如果 style 中設定固定大小的寬度,則使用該值,計算盒子大小
childWidth = YGResolveValue(child->resolvedDimensions[YGDimensionWidth], width) + marginRow;
} else {
// 如果沒有設定寬度,但是前後的 position 設定了,則使用 position 來確定盒子大小。position 的意思是在
// 父專案的空間上,相對於父專案 top right left bottom 邊的距離 offset。
if (YGNodeIsLeadingPosDefined(child, YGFlexDirectionRow) &&
YGNodeIsTrailingPosDefined(child, YGFlexDirectionRow)) {
childWidth = node->layout.measuredDimensions[YGDimensionWidth] -
(YGNodeLeadingBorder(node, YGFlexDirectionRow) +
YGNodeTrailingBorder(node, YGFlexDirectionRow)) -
(YGNodeLeadingPosition(child, YGFlexDirectionRow, width) +
YGNodeTrailingPosition(child, YGFlexDirectionRow, width));
childWidth = YGNodeBoundAxis(child, YGFlexDirectionRow, childWidth, width, width);
}
}
// 對於高度的確定和上述的寬度的確定的方式一致。
if (YGNodeIsStyleDimDefined(child, YGFlexDirectionColumn, height)) {
childHeight =
YGResolveValue(child->resolvedDimensions[YGDimensionHeight], height) + marginColumn;
} else {
if (YGNodeIsLeadingPosDefined(child, YGFlexDirectionColumn) &&
YGNodeIsTrailingPosDefined(child, YGFlexDirectionColumn)) {
childHeight = node->layout.measuredDimensions[YGDimensionHeight] -
(YGNodeLeadingBorder(node, YGFlexDirectionColumn) +
YGNodeTrailingBorder(node, YGFlexDirectionColumn)) -
(YGNodeLeadingPosition(child, YGFlexDirectionColumn, height) +
YGNodeTrailingPosition(child, YGFlexDirectionColumn, height));
childHeight = YGNodeBoundAxis(child, YGFlexDirectionColumn, childHeight, height, width);
}
}
// 當 aspectRatio 橫縱比被設定時,如果寬度或者高度是確切的,則可以確定另外一邊的確切大小
if (YGFloatIsUndefined(childWidth) ^ YGFloatIsUndefined(childHeight)) {
if (!YGFloatIsUndefined(child->style.aspectRatio)) {
if (YGFloatIsUndefined(childWidth)) {
childWidth =
marginRow + fmaxf((childHeight - marginColumn) * child->style.aspectRatio,
YGNodePaddingAndBorderForAxis(child, YGFlexDirectionColumn, width));
} else if (YGFloatIsUndefined(childHeight)) {
childHeight =
marginColumn + fmaxf((childWidth - marginRow) / child->style.aspectRatio,
YGNodePaddingAndBorderForAxis(child, YGFlexDirectionRow, width));
}
}
}
// 如果寬度和高度有任一不確定值,則需要進行孩子大小的測量來確定改專案的大小
if (YGFloatIsUndefined(childWidth) || YGFloatIsUndefined(childHeight)) {
childWidthMeasureMode =
YGFloatIsUndefined(childWidth) ? YGMeasureModeUndefined : YGMeasureModeExactly;
childHeightMeasureMode =
YGFloatIsUndefined(childHeight) ? YGMeasureModeUndefined : YGMeasureModeExactly;
// 如果主軸是交叉軸,通過寬度未定義,而且測量模式不是未定義,則使用父親的大小來限制該專案的盒子大小。
if (!isMainAxisRow && YGFloatIsUndefined(childWidth) && widthMode != YGMeasureModeUndefined &&
width > 0) {
childWidth = width;
childWidthMeasureMode = YGMeasureModeAtMost;
}
// 測量該專案的大小,會遞迴計算孩子所需大小
YGLayoutNodeInternal(child,
childWidth,
childHeight,
direction,
childWidthMeasureMode,
childHeightMeasureMode,
childWidth,
childHeight,
false,
"abs-measure",
config);
// 獲取測量過後的大小
childWidth = child->layout.measuredDimensions[YGDimensionWidth] +
YGNodeMarginForAxis(child, YGFlexDirectionRow, width);
childHeight = child->layout.measuredDimensions[YGDimensionHeight] +
YGNodeMarginForAxis(child, YGFlexDirectionColumn, width);
}
// 對於該專案的子專案的排版操作,回到第 3 步驟
YGLayoutNodeInternal(child,
childWidth,
childHeight,
direction,
YGMeasureModeExactly,
YGMeasureModeExactly,
childWidth,
childHeight,
true,
"abs-layout",
config);
// 根據 position 的設定來確定在父空間中最終放置的位置
if (YGNodeIsTrailingPosDefined(child, mainAxis) && !YGNodeIsLeadingPosDefined(child, mainAxis)) {
// 如果主軸的後沿位置確定而前沿不確定,則位置就根據後沿確定,並反向計算出前沿的位置
child->layout.position[leading[mainAxis]] = node->layout.measuredDimensions[dim[mainAxis]] -
child->layout.measuredDimensions[dim[mainAxis]] -
YGNodeTrailingBorder(node, mainAxis) -
YGNodeTrailingMargin(child, mainAxis, width) -
YGNodeTrailingPosition(child, mainAxis, isMainAxisRow ? width : height);
} else if (!YGNodeIsLeadingPosDefined(child, mainAxis) &&
node->style.justifyContent == YGJustifyCenter) {
// 如果主軸前沿後沿沒定義,同時主軸排列方式為居中,則計算出居中的前沿的位置
child->layout.position[leading[mainAxis]] = (node->layout.measuredDimensions[dim[mainAxis]] -
child->layout.measuredDimensions[dim[mainAxis]]) /
2.0f;
} else if (!YGNodeIsLeadingPosDefined(child, mainAxis) &&
node->style.justifyContent == YGJustifyFlexEnd) {
// 如果主軸前沿後沿沒定義,同時主軸排列方式為對齊後沿,則計算出對齊後沿時前沿的位置
child->layout.position[leading[mainAxis]] = (node->layout.measuredDimensions[dim[mainAxis]] -
child->layout.measuredDimensions[dim[mainAxis]]);
} // 其他的主軸前沿位置均為 0
// 交叉軸的 position 計算方式和主軸的計算方式一致,不再贅述
if (YGNodeIsTrailingPosDefined(child, crossAxis) &&
!YGNodeIsLeadingPosDefined(child, crossAxis)) {
child->layout.position[leading[crossAxis]] = node->layout.measuredDimensions[dim[crossAxis]] -
child->layout.measuredDimensions[dim[crossAxis]] -
YGNodeTrailingBorder(node, crossAxis) -
YGNodeTrailingMargin(child, crossAxis, width) -
YGNodeTrailingPosition(child, crossAxis, isMainAxisRow ? height : width);
} else if (!YGNodeIsLeadingPosDefined(child, crossAxis) &&
YGNodeAlignItem(node, child) == YGAlignCenter) {
child->layout.position[leading[crossAxis]] =
(node->layout.measuredDimensions[dim[crossAxis]] -
child->layout.measuredDimensions[dim[crossAxis]]) /
2.0f;
} else if (!YGNodeIsLeadingPosDefined(child, crossAxis) &&
YGNodeAlignItem(node, child) == YGAlignFlexEnd) {
child->layout.position[leading[crossAxis]] = (node->layout.measuredDimensions[dim[crossAxis]] -
child->layout.measuredDimensions[dim[crossAxis]]);
}
}
複製程式碼
到此程式碼分析完畢
感謝你的耐心閱讀,希望能保持耐心去做好喜歡的事情。如果有人和疑問歡迎留言討論。