adwaysengineerblog.hatenablog.com
adwaysengineerblog.hatenablog.com
こんにちは、@binaryta です。
2度に渡ってYogaの仕組みを追っていきましたが、今回が最終回です。
YogaのメインルーチンをSTEPごとに解読する
前回、メインルーチンの前処理部分まで解読しました。
前処理ではnodeのmaring, padding, borderなどのstyleを割り当てていました。
ということで続きを見ていきましょう。
STEP 1: nodeの収容領域(inner)の計算
前処理以降は子nodeを持つ要素しか処理されません。
STEP 1のプロセスは、現在処理しているnodeのメンバに対して値を設定することは特になく、新しい変数の初期化処理を行なっています。
どのような変数が初期化されるのかというと、nodeのinnerサイズを確定するための変数です。
// STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM const YGFlexDirection mainAxis = YGResolveFlexDirection(node->getStyle().flexDirection, direction); const YGFlexDirection crossAxis = YGFlexDirectionCross(mainAxis, direction); const bool isMainAxisRow = YGFlexDirectionIsRow(mainAxis); const bool isNodeFlexWrap = node->getStyle().flexWrap != YGWrapNoWrap; const float mainAxisownerSize = isMainAxisRow ? ownerWidth : ownerHeight; const float crossAxisownerSize = isMainAxisRow ? ownerHeight : ownerWidth; const float leadingPaddingAndBorderCross = YGUnwrapFloatOptional(node->getLeadingPaddingAndBorder(crossAxis, ownerWidth)); const float paddingAndBorderAxisMain = YGNodePaddingAndBorderForAxis(node, mainAxis, ownerWidth); const float paddingAndBorderAxisCross = YGNodePaddingAndBorderForAxis(node, crossAxis, ownerWidth); 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 = YGUnwrapFloatOptional(node->getMarginForAxis(YGFlexDirectionRow, ownerWidth)); const float marginAxisColumn = YGUnwrapFloatOptional(node->getMarginForAxis(YGFlexDirectionColumn, ownerWidth)); const float minInnerWidth = YGUnwrapFloatOptional(YGResolveValue(node->getStyle().minDimensions[YGDimensionWidth], ownerWidth)) - paddingAndBorderAxisRow; const float maxInnerWidth = YGUnwrapFloatOptional(YGResolveValue(node->getStyle().maxDimensions[YGDimensionWidth], ownerWidth)) - paddingAndBorderAxisRow; const float minInnerHeight = YGUnwrapFloatOptional(YGResolveValue(node->getStyle().minDimensions[YGDimensionHeight], ownerHeight)) - paddingAndBorderAxisColumn; const float maxInnerHeight = YGUnwrapFloatOptional(YGResolveValue(node->getStyle().maxDimensions[YGDimensionHeight], ownerHeight)) - paddingAndBorderAxisColumn; const float minInnerMainDim = isMainAxisRow ? minInnerWidth : minInnerHeight; const float maxInnerMainDim = isMainAxisRow ? maxInnerWidth : maxInnerHeight;
- 主軸と交差軸の方向を解決する
- 主軸が "行" 方向なら
ownerWidth
を主軸のコンテナサイズとする
主軸が "列" 方向ならownerHeight
を主軸のコンテナサイズとする - 主軸が "行" 方向なら
ownerHeight
を交叉軸のコンテナサイズとする
主軸が "列" 方向ならownerWidth
を主軸のコンテナサイズとする - 親nodeの主軸、交叉軸のサイズを確定
- paddingとborderの合計値を確定
- 主軸の両端のpadding, borderの合計値を確定
- 交叉軸の両端のpadding, borderの合計値を確定
- 主軸が "行" 方向なら
widthMeasureMode
の値を主軸のサイジングルールとする
主軸が "列" 方向ならheightMeasureMode
の値を主軸のサイジングルールとする - 主軸が "行" 方向なら
heightMeasureMode
の値を交叉軸のサイジングルールとする
主軸が "列" 方向ならwidthMeasureMode
の値を交叉軸のサイジングルールとする - 行と列のpadding, borderを確定
- 行と列のmarginを確定
- widthとheightそれぞれのinnerの最大値, 最小値を取得
minInnerWidth
,maxInnerHeight
,minInnerWidth
,minInnerWidth
- 主軸が "行" 方向なら
minInnerWidth
を主軸の最小収容サイズにする
主軸が "列" 方向ならminInnerHeight
を主軸の最小収容サイズにする - 主軸が "行" 方向なら
maxInnerWidth
を主軸の最大収容サイズにする
主軸が "列" 方向ならmaxInnerHeight
を主軸の最大収容サイズにする
STEP 2: 主軸方向と交叉軸方向の利用可能なサイズを決定する
// STEP 2: DETERMINE AVAILABLE SIZE IN MAIN AND CROSS DIRECTIONS float availableInnerWidth = YGNodeCalculateAvailableInnerDim(node, YGFlexDirectionRow, availableWidth, ownerWidth); float availableInnerHeight = YGNodeCalculateAvailableInnerDim(node, YGFlexDirectionColumn, availableHeight, ownerHeight); float availableInnerMainDim = isMainAxisRow ? availableInnerWidth : availableInnerHeight; const float availableInnerCrossDim = isMainAxisRow ? availableInnerHeight : availableInnerWidth; float totalOuterFlexBasis = 0;
- 利用可能なinnerの幅、高さ[px]の確定
- 利用可能なinnerの主軸方向、交叉軸方向の大きさ[px]を確定
innerのサイズ計算は図を見ると解るように次のようになります。
availableInner = availableDim - margin - paddingAndBorder
innerのサイズ計算はYGNodeCalculateAvailableInnerDim関数が担います。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L1729-L1765
STEP 3: 各flex item(子node) のflex-basisを決定する
ここでは説明の都合上App.jsのrender関数内部のJSXを次のように変更します。
render() { return ( <View style={{ flex: 1, justifyContent: 'space-around', alignItems: 'center', backgroundColor: '#ddd' }}> <View style={{ flexDirection: 'row', padding: 10, borderWidth: 10, width: 300, height: 300, backgroundColor: '#999' }}> <View style={{ flexBasis: 1, padding: 20, backgroundColor: '#111' }}/> <View style={{ width: 100, padding: 20, backgroundColor: '#333' }}/> <View style={{ aspectRatio: 0.2, padding: 20, backgroundColor: '#777' }}/> </View> </View> ); }
このSTEPではflexスタイル指定についての知識が少々必要なため 「flex - CSS: カスケーディングスタイルシート | MDN」 を軽く見ておいた方がいいと思います。
flex-grow
, flex-shrink
, flex-basis
プロパティの挙動についてある程度知っている方は読まなくていいと思います。
では早速見ていきます。
// STEP 3: DETERMINE FLEX BASIS FOR EACH ITEM YGNodeComputeFlexBasisForChildren( node, availableInnerWidth, availableInnerHeight, widthMeasureMode, heightMeasureMode, direction, mainAxis, config, performLayout, totalOuterFlexBasis); const bool flexBasisOverflows = measureModeMainDim == YGMeasureModeUndefined ? false : totalOuterFlexBasis > availableInnerMainDim; if (isNodeFlexWrap && flexBasisOverflows && measureModeMainDim == YGMeasureModeAtMost) { measureModeMainDim = YGMeasureModeExactly; }
まず、関数名から解るように子nodeのflex basisを算出しています。
YGNodeComputeFlexBasisForChildren
が呼ばれ、その中でさらにYGNodeComputeFlexBasisForChild
が呼ばれます。
先にこの2つの関数について見ていきます。
YGNodeComputeFlexBasisForChildren
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L1767-L1847
- 全ての子nodeを取得
- 主軸のサイジングルールを取得
- 主軸のサイジングルールが
YGMeasureModeExactly
の場合は全ての子nodeを再帰処理- flexGrowとflexShrinkが両方指定してある場合は
singleFlexChild
にそのnodeを格納
singleFlexChild
は残りのスペースと正確に一致するように、子を測定したり縮小したりする代わりに、computedFlexBasisを0に設定できることを意味する
- flexGrowとflexShrinkが両方指定してある場合は
- 全ての子nodeを再帰処理
- 子nodeの配置位置を設定
- そのnodeに
position: absolute
( 相対位置 ) のスタイル指定がされている場合はcontinue - そのnodeが
singleFlexChild
と同じ場合は、
flexBasisに0を設定する - そのnodeが
singleFlexChild
と違う場合は、
YGNodeComputeFlexBasisForChild
が呼ばれる
YGNodeComputeFlexBasisForChild
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L1188-L1361
それぞれの分岐処理の最後でnodeのメンバ関数setLayoutComputedFlexBasis
を呼び出していることに注目してください。
分岐した処理の内部でやっていることは基本的にはnodeのwidth, heightを計算し、サイジングルールを定めてそれを適用しています。
大まかな概要のみ次に示します。
if (!resolvedFlexBasis.isUndefined() && !YGFloatIsUndefined(mainAxisSize)) { /* そのnodeにflexBasisが定義されていて、親nodeにwidthが定義されている場合に分岐. */ ..... } else if (isMainAxisRow && isRowStyleDimDefined) { /* 主軸方向がrow方向で、そのnodeにwidthが定義されている場合に分岐する. widthは明確なので、それをflexBasisとして使用する. */ ..... } else if (!isMainAxisRow && isColumnStyleDimDefined) { /* 主軸方向がcolumn方向で、そのnodeにheightが定義されている場合に分岐する. heightは明確なので、それをflexBasisとして使用する. */ ..... } else { /* それ以外の場合. ここを通る場合はアスペクト比やoverflow Scrollなどを考慮した上で widht, heightを算出する. */ ..... }
この分岐処理は先ほど変更したJSXと対比して見てみると非常にわかりやすいと思います。
次にその図を明示します。
上で説明したvoidな関数 YGNodeComputeFlexBasisForChildren
が処理されるとflexBasisが確定します。
STEP 4 ~ STEP 11
ここまで閲読頂いた方はありがとうございます。
そして、ここまで解読できたのであればもう1人で最後まで理解できると思いますので、最後まで理解しきったらYogaにコントリビュートしてみてください!
(公開期日に間に合わなかったための言い訳ですw)
付録A: flexBasis計算処理のデバッグ
STEP3の処理ではflexBasisを算出していると説明しました。
YGNodeComputeFlexBasisForChildren
関数内で呼ばれているYGNodeComputeFlexBasisForChild
関数のことです。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L1830
割と行数が多いめ、特定のnodeのみデバッグで確認したいと思いますよね。
ということで、どのようにそれを実現するのか手順をまとめておきます。
まずJSXの構造は次のようになっていました。
render() { return ( <View style={{ flex: 1, justifyContent: 'space-around', alignItems: 'center', backgroundColor: '#ddd' }}> <View style={{ flexDirection: 'row', padding: 10, borderWidth: 10, width: 300, height: 300, backgroundColor: '#999' }}> <View style={{ flexBasis: 1, padding: 20, backgroundColor: '#111' }}/> <View style={{ width: 100, padding: 20, backgroundColor: '#333' }}/> <View style={{ aspectRatio: 0.2, padding: 20, backgroundColor: '#777' }}/> </View> </View> ); }
このnodeツリーの中の最も内側に属している3つのView要素に関してのみデバッグしたいとします。
次の部分です。
<View style={{ flexBasis: 1, padding: 20, backgroundColor: '#111' }}/> <View style={{ width: 100, padding: 20, backgroundColor: '#333' }}/> <View style={{ aspectRatio: 0.2, padding: 20, backgroundColor: '#777' }}/>
これらのnodeの親nodeであるView要素はwidth, height共に300px指定となっています。
且つ、borderサイズ(borderWidth)とpaddingは共に10pxを指定しています。
これにより、3つの子nodeのavailableInnerWidth(使用可能幅サイズ)が確定します。
子nodeで使用可能となる幅領域は、親nodeのinner領域の幅ですので親nodeのwidthから両端(右、左)のborderサイズと両端paddingサイズを減算したら得られます。
parent's width - border width * 2 - padding * 2
= 300 - 10 * 2 - 10 * 2
= 260[px]
ということでavailableInnerWidth == 260
というデバッグ条件を指定してやれば、今回目的とするnodeのみを対象としてYGNodeComputeFlexBasisForChild
関数をデバッグできます。
( 行番号をダブルクリックでデバッグ条件を付与できます )
まとめ
いかがでしたでしょうか。
なかなか時間が取れず、メモ書きっぽくなってしまいましたが最後までご閲読頂きありがとうございました。