React NativeのレイアウトエンジンYogaの仕組み [続編]

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;
  1. 主軸と交差軸の方向を解決する
  2. 主軸が "行" 方向ならownerWidthを主軸のコンテナサイズとする
    主軸が "列" 方向ならownerHeightを主軸のコンテナサイズとする
  3. 主軸が "行" 方向ならownerHeightを交叉軸のコンテナサイズとする
    主軸が "列" 方向ならownerWidthを主軸のコンテナサイズとする
  4. 親nodeの主軸、交叉軸のサイズを確定
  5. paddingとborderの合計値を確定
  6. 主軸の両端のpadding, borderの合計値を確定
  7. 交叉軸の両端のpadding, borderの合計値を確定
  8. 主軸が "行" 方向ならwidthMeasureModeの値を主軸のサイジングルールとする
    主軸が "列" 方向ならheightMeasureModeの値を主軸のサイジングルールとする
  9. 主軸が "行" 方向ならheightMeasureModeの値を交叉軸のサイジングルールとする
    主軸が "列" 方向ならwidthMeasureModeの値を交叉軸のサイジングルールとする
  10. 行と列のpadding, borderを確定
  11. 行と列のmarginを確定
  12. widthとheightそれぞれのinnerの最大値, 最小値を取得
    minInnerWidth, maxInnerHeight, minInnerWidth, minInnerWidth
  13. 主軸が "行" 方向ならminInnerWidthを主軸の最小収容サイズにする
    主軸が "列" 方向ならminInnerHeightを主軸の最小収容サイズにする
  14. 主軸が "行" 方向なら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;
  1. 利用可能なinnerの幅、高さ[px]の確定
  2. 利用可能なinnerの主軸方向、交叉軸方向の大きさ[px]を確定

innerのサイズ計算は図を見ると解るように次のようになります。
availableInner = availableDim - margin - paddingAndBorder

innerのサイズ計算はYGNodeCalculateAvailableInnerDim関数が担います。
https://github.com/facebook/yoga/blob/1.9.0/yoga/Yoga.cpp#L1729-L1765

f:id:AdwaysEngineerBlog:20180905204815j:plain:w400

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

  1. 全ての子nodeを取得
  2. 主軸のサイジングルールを取得
  3. 主軸のサイジングルールがYGMeasureModeExactlyの場合は全ての子nodeを再帰処理
    • flexGrowとflexShrinkが両方指定してある場合はsingleFlexChildにそのnodeを格納
      singleFlexChildは残りのスペースと正確に一致するように、子を測定したり縮小したりする代わりに、computedFlexBasisを0に設定できることを意味する
  4. 全ての子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と対比して見てみると非常にわかりやすいと思います。
次にその図を明示します。 f:id:AdwaysEngineerBlog:20180909022926p:plain

上で説明した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関数をデバッグできます。
( 行番号をダブルクリックでデバッグ条件を付与できます )

f:id:AdwaysEngineerBlog:20180909032117p:plain

まとめ

いかがでしたでしょうか。
なかなか時間が取れず、メモ書きっぽくなってしまいましたが最後までご閲読頂きありがとうございました。