Repository: cashapp/redwood
Pull request #588 (2022-12-05 04:54:47)

Refactor FlexContainer to position its children as part of measure.
Removes `FlexContainer.layout` and folds its responsibilities of updating the child's coordinates into `FlexContainer.measure`. This ensures that a child's position is always up-to-date with the parent's constraints.

This means you only need to call `container.measure(widthSpec, heightSpec)`, which returns the measured container size and updates the `left/right/top/bottom` of each child.


Patch:
diff --git a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexContainer.kt b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexContainer.kt
index df8a16ffa..b96fb05ea 100644
--- a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexContainer.kt
+++ b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexContainer.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package app.cash.redwood.flexbox
 
 import app.cash.redwood.flexbox.FlexItem.Companion.DefaultFlexBasisPercent
@@ -81,13 +80,90 @@ public class FlexContainer {
    */
   public val items: MutableList<FlexItem> = mutableListOf()
 
+  //region measure
+
+  /**
+   * Measures the flexbox's items according to the parent's [widthSpec] and [heightSpec] and returns
+   * the expected size of the container.
+   *
+   * @param widthSpec horizontal space requirements as imposed by the parent.
+   * @param heightSpec vertical space requirements as imposed by the parent.
+   */
+  public fun measure(widthSpec: MeasureSpec, heightSpec: MeasureSpec): Size {
+    val width = if (fillWidth && widthSpec.mode != MeasureSpecMode.Exactly && widthSpec.size > 0) {
+      MeasureSpec.from(widthSpec.size, MeasureSpecMode.Exactly)
+    } else {
+      widthSpec
+    }
+    val height = if (fillHeight && heightSpec.mode != MeasureSpecMode.Exactly && heightSpec.size > 0) {
+      MeasureSpec.from(heightSpec.size, MeasureSpecMode.Exactly)
+    } else {
+      heightSpec
+    }
+
+    if (flexDirection.isHorizontal) {
+      return measureHorizontal(width, height)
+    } else {
+      return measureVertical(width, height)
+    }
+  }
+
+  private fun measureHorizontal(
+    widthSpec: MeasureSpec,
+    heightSpec: MeasureSpec,
+  ): Size {
+    val flexLines = calculateFlexLines(widthSpec, heightSpec)
+    determineMainSize(flexLines, widthSpec, heightSpec)
+
+    if (alignItems == AlignItems.Baseline) {
+      for (flexLine in flexLines) {
+        // The largest height value that also take the baseline shift into account
+        var largestHeightInLine = Double.MIN_VALUE
+        for (i in flexLine.firstIndex..flexLine.lastIndex) {
+          val child = items.getOrNull(i)
+          if (child == null || !child.visible) {
+            continue
+          }
+          val heightInLine = if (flexWrap != FlexWrap.WrapReverse) {
+            val marginTop = maxOf(flexLine.maxBaseline - child.baseline, child.margin.top)
+            child.height + marginTop + child.margin.bottom
+          } else {
+            val marginBottom = maxOf(flexLine.maxBaseline - child.height + child.baseline, child.margin.bottom)
+            child.height + child.margin.top + marginBottom
+          }
+          largestHeightInLine = maxOf(largestHeightInLine, heightInLine)
+        }
+        flexLine.crossSize = largestHeightInLine
+      }
+    }
+
+    determineCrossSize(flexLines, widthSpec, heightSpec)
+    stretchChildren(flexLines)
+    val containerSize = calculateContainerSize(flexLines, widthSpec, heightSpec)
+    layout(flexLines, containerSize)
+    return containerSize
+  }
+
+  private fun measureVertical(
+    widthSpec: MeasureSpec,
+    heightSpec: MeasureSpec,
+  ): Size {
+    val flexLines = calculateFlexLines(widthSpec, heightSpec)
+    determineMainSize(flexLines, widthSpec, heightSpec)
+    determineCrossSize(flexLines, widthSpec, heightSpec)
+    stretchChildren(flexLines)
+    val containerSize = calculateContainerSize(flexLines, widthSpec, heightSpec)
+    layout(flexLines, containerSize)
+    return containerSize
+  }
+
   /**
    * Calculates how many flex lines are needed in the flex container layout by measuring each child.
    * Expanding or shrinking the flex items depending on the flex grow and flex shrink
    * attributes are done in a later procedure, so the items' measured width and measured
    * height may be changed in a later process.
    */
-  private fun calculateFlexLines(
+  internal fun calculateFlexLines(
     widthMeasureSpec: MeasureSpec,
     heightMeasureSpec: MeasureSpec,
   ): MutableList<FlexLine> {
@@ -146,9 +222,9 @@ public class FlexContainer {
         childDimension = orientation.crossSize(item),
       )
       if (flexDirection.isHorizontal) {
-        item.applyMeasure(childMainMeasureSpec, childCrossMeasureSpec)
+        item.measure(childMainMeasureSpec, childCrossMeasureSpec)
       } else {
-        item.applyMeasure(childCrossMeasureSpec, childMainMeasureSpec)
+        item.measure(childCrossMeasureSpec, childMainMeasureSpec)
       }
 
       if (
@@ -179,7 +255,7 @@ public class FlexContainer {
             padding = orientation.crossPadding(padding) + orientation.crossMargin(item) + sumCrossSize,
             childDimension = crossSize,
           )
-          item.applyMeasure(childMainMeasureSpec, childCrossMeasureSpec)
+          item.measure(childMainMeasureSpec, childCrossMeasureSpec)
         }
         flexLine = FlexLine()
         flexLine.itemCount = 1
@@ -209,7 +285,7 @@ public class FlexContainer {
           // If the flex wrap property is WrapReverse, calculate the
           // baseline as the distance from the cross end and the baseline
           // since the cross size calculation is based on the distance from the cross end
-          maxOf(flexLine.maxBaseline, item.measuredHeight - item.baseline + item.margin.bottom)
+          maxOf(flexLine.maxBaseline, item.height - item.baseline + item.margin.bottom)
         }
       }
 
@@ -383,7 +459,7 @@ public class FlexContainer {
       if (flexDirection.isHorizontal) {
         // The direction of the main axis is horizontal
         if (!childrenFrozen[index] && child.flexGrow > 0) {
-          var rawCalculatedWidth = (child.measuredWidth + unitSpace * child.flexGrow)
+          var rawCalculatedWidth = (child.width + unitSpace * child.flexGrow)
           if (i == flexLine.itemCount - 1) {
             rawCalculatedWidth += accumulatedRoundError
             accumulatedRoundError = 0.0
@@ -414,20 +490,20 @@ public class FlexContainer {
             flexItem = child,
             padding = flexLine.sumCrossSizeBefore,
           )
-          child.measuredHeight = when (childHeightMeasureSpec.mode) {
+          child.height = when (childHeightMeasureSpec.mode) {
             MeasureSpecMode.Exactly -> childHeightMeasureSpec.size
             MeasureSpecMode.AtMost -> child.measurable.height(newWidth).coerceAtMost(childHeightMeasureSpec.size)
             MeasureSpecMode.Unspecified -> child.measurable.height(newWidth)
             else -> throw AssertionError()
           }
-          child.measuredWidth = newWidth
+          child.width = newWidth
         }
-        largestCrossSize = maxOf(largestCrossSize, child.measuredHeight + child.margin.top + child.margin.bottom)
-        flexLine.mainSize += (child.measuredWidth + child.margin.start + child.margin.end)
+        largestCrossSize = maxOf(largestCrossSize, child.height + child.margin.top + child.margin.bottom)
+        flexLine.mainSize += (child.width + child.margin.start + child.margin.end)
       } else {
         // The direction of the main axis is vertical
         if (!childrenFrozen[index] && child.flexGrow > 0) {
-          var rawCalculatedHeight = (child.measuredHeight + unitSpace * child.flexGrow)
+          var rawCalculatedHeight = (child.height + unitSpace * child.flexGrow)
           if (i == flexLine.itemCount - 1) {
             rawCalculatedHeight += accumulatedRoundError
             accumulatedRoundError = 0.0
@@ -458,16 +534,16 @@ public class FlexContainer {
             flexItem = child,
             padding = flexLine.sumCrossSizeBefore,
           )
-          child.measuredWidth = when (childWidthMeasureSpec.mode) {
+          child.width = when (childWidthMeasureSpec.mode) {
             MeasureSpecMode.Exactly -> childWidthMeasureSpec.size
             MeasureSpecMode.AtMost -> child.measurable.width(newHeight).coerceAtMost(childWidthMeasureSpec.size)
             MeasureSpecMode.Unspecified -> child.measurable.width(newHeight)
             else -> throw AssertionError()
           }
-          child.measuredHeight = newHeight
+          child.height = newHeight
         }
-        largestCrossSize = maxOf(largestCrossSize, child.measuredWidth + child.margin.start + child.margin.end)
-        flexLine.mainSize += (child.measuredHeight + child.margin.top + child.margin.bottom)
+        largestCrossSize = maxOf(largestCrossSize, child.width + child.margin.start + child.margin.end)
+        flexLine.mainSize += (child.height + child.margin.top + child.margin.bottom)
       }
       flexLine.crossSize = maxOf(flexLine.crossSize, largestCrossSize)
     }
@@ -537,7 +613,7 @@ public class FlexContainer {
       if (flexDirection.isHorizontal) {
         // The direction of main axis is horizontal
         if (!childrenFrozen[index] && child.flexShrink > 0) {
-          var rawCalculatedWidth = child.measuredWidth - unitShrink * child.flexShrink
+          var rawCalculatedWidth = child.width - unitShrink * child.flexShrink
           if (i == flexLine.itemCount - 1) {
             rawCalculatedWidth += accumulatedRoundError
             accumulatedRoundError = 0.0
@@ -568,20 +644,20 @@ public class FlexContainer {
             flexItem = child,
             padding = flexLine.sumCrossSizeBefore,
           )
-          child.measuredHeight = when (childHeightMeasureSpec.mode) {
+          child.height = when (childHeightMeasureSpec.mode) {
             MeasureSpecMode.Exactly -> childHeightMeasureSpec.size
             MeasureSpecMode.AtMost -> child.measurable.height(newWidth).coerceAtMost(childHeightMeasureSpec.size)
             MeasureSpecMode.Unspecified -> child.measurable.height(newWidth)
             else -> throw AssertionError()
           }
-          child.measuredWidth = newWidth
+          child.width = newWidth
         }
-        largestCrossSize = maxOf(largestCrossSize, child.measuredHeight + child.margin.top + child.margin.bottom)
-        flexLine.mainSize += child.measuredWidth + child.margin.start + child.margin.end
+        largestCrossSize = maxOf(largestCrossSize, child.height + child.margin.top + child.margin.bottom)
+        flexLine.mainSize += child.width + child.margin.start + child.margin.end
       } else {
         // The direction of main axis is vertical
         if (!childrenFrozen[index] && child.flexShrink > 0) {
-          var rawCalculatedHeight = child.measuredHeight - unitShrink * child.flexShrink
+          var rawCalculatedHeight = child.height - unitShrink * child.flexShrink
           if (i == flexLine.itemCount - 1) {
             rawCalculatedHeight += accumulatedRoundError
             accumulatedRoundError = 0.0
@@ -609,16 +685,16 @@ public class FlexContainer {
             flexItem = child,
             padding = flexLine.sumCrossSizeBefore,
           )
-          child.measuredWidth = when (childWidthMeasureSpec.mode) {
+          child.width = when (childWidthMeasureSpec.mode) {
             MeasureSpecMode.Exactly -> childWidthMeasureSpec.size
             MeasureSpecMode.AtMost -> child.measurable.width(newHeight).coerceAtMost(childWidthMeasureSpec.size)
             MeasureSpecMode.Unspecified -> child.measurable.width(newHeight)
             else -> throw AssertionError()
           }
-          child.measuredHeight = newHeight
+          child.height = newHeight
         }
-        largestCrossSize = maxOf(largestCrossSize, child.measuredWidth + child.margin.start + child.margin.end)
-        flexLine.mainSize += (child.measuredHeight + child.margin.top + child.margin.bottom)
+        largestCrossSize = maxOf(largestCrossSize, child.width + child.margin.start + child.margin.end)
+        flexLine.mainSize += (child.height + child.margin.top + child.margin.bottom)
       }
       flexLine.crossSize = maxOf(flexLine.crossSize, largestCrossSize)
     }
@@ -871,9 +947,9 @@ public class FlexContainer {
             continue
           }
           if (flexDirection.isHorizontal) {
-            stretchViewVertically(item, flexLine.crossSize)
+            stretchItemVertically(item, flexLine.crossSize)
           } else {
-            stretchViewHorizontally(item, flexLine.crossSize)
+            stretchItemHorizontally(item, flexLine.crossSize)
           }
         }
       }
@@ -883,9 +959,9 @@ public class FlexContainer {
           val item = items[index]
           if (item.alignSelf == AlignSelf.Stretch) {
             if (flexDirection.isHorizontal) {
-              stretchViewVertically(item, flexLine.crossSize)
+              stretchItemVertically(item, flexLine.crossSize)
             } else {
-              stretchViewHorizontally(item, flexLine.crossSize)
+              stretchItemHorizontally(item, flexLine.crossSize)
             }
           }
         }
@@ -896,226 +972,19 @@ public class FlexContainer {
   /**
    * Expand the item vertically to the size of the [crossSize] (considering [item]'s margins).
    */
-  private fun stretchViewVertically(item: FlexItem, crossSize: Double) {
-    item.measuredHeight = (crossSize - item.margin.top - item.margin.bottom)
+  private fun stretchItemVertically(item: FlexItem, crossSize: Double) {
+    item.height = (crossSize - item.margin.top - item.margin.bottom)
       .coerceIn(item.measurable.minHeight, item.measurable.maxHeight)
   }
 
   /**
    * Expand the item horizontally to the size of the crossSize (considering [item]'s margins).
    */
-  private fun stretchViewHorizontally(item: FlexItem, crossSize: Double) {
-    item.measuredWidth = (crossSize - item.margin.start - item.margin.end)
+  private fun stretchItemHorizontally(item: FlexItem, crossSize: Double) {
+    item.width = (crossSize - item.margin.start - item.margin.end)
       .coerceIn(item.measurable.minWidth, item.measurable.maxWidth)
   }
 
-  /**
-   * Place a single View when the layout direction is horizontal
-   * ([FlexContainer.flexDirection] is either [FlexDirection.Row] or [FlexDirection.RowReverse]).
-   */
-  private fun layoutSingleChildHorizontal(
-    item: FlexItem,
-    flexLine: FlexLine,
-    left: Double,
-    top: Double,
-    right: Double,
-    bottom: Double,
-  ) {
-    var alignItems = alignItems
-    if (item.alignSelf != AlignSelf.Auto) {
-      // Expecting the values for alignItems and alignSelf match except for ALIGN_SELF_AUTO.
-      // Assigning the alignSelf value as alignItems should work.
-      alignItems = item.alignSelf.toAlignItems()
-    }
-    val crossSize = flexLine.crossSize
-    when (alignItems) {
-      AlignItems.FlexStart, AlignItems.Stretch -> if (flexWrap != FlexWrap.WrapReverse) {
-        item.layout(left, top + item.margin.top, right, bottom + item.margin.top)
-      } else {
-        item.layout(left, top - item.margin.bottom, right, bottom - item.margin.bottom)
-      }
-      AlignItems.Baseline -> if (flexWrap != FlexWrap.WrapReverse) {
-        val marginTop = maxOf(flexLine.maxBaseline - item.baseline, item.margin.top)
-        item.layout(left, top + marginTop, right, bottom + marginTop)
-      } else {
-        val marginBottom = maxOf(flexLine.maxBaseline - item.measuredHeight + item.baseline, item.margin.bottom)
-        item.layout(left, top - marginBottom, right, bottom - marginBottom)
-      }
-      AlignItems.FlexEnd -> if (flexWrap != FlexWrap.WrapReverse) {
-        item.layout(
-          left = left,
-          top = top + crossSize - item.measuredHeight - item.margin.bottom,
-          right = right,
-          bottom = top + crossSize - item.margin.bottom,
-        )
-      } else {
-        // If the flexWrap == WrapReverse, the direction of the
-        // flexEnd is flipped (from top to bottom).
-        item.layout(
-          left = left,
-          top = top - crossSize + item.measuredHeight + item.margin.top,
-          right = right,
-          bottom = bottom - crossSize + item.measuredHeight + item.margin.top,
-        )
-      }
-      AlignItems.Center -> {
-        val topFromCrossAxis = (crossSize - item.measuredHeight + item.margin.top - item.margin.bottom) / 2
-        if (flexWrap != FlexWrap.WrapReverse) {
-          item.layout(
-            left = left,
-            top = top + topFromCrossAxis,
-            right = right,
-            bottom = top + topFromCrossAxis + item.measuredHeight,
-          )
-        } else {
-          item.layout(
-            left = left,
-            top = top - topFromCrossAxis,
-            right = right,
-            bottom = top - topFromCrossAxis + item.measuredHeight,
-          )
-        }
-      }
-    }
-  }
-
-  /**
-   * Place a single View when the layout direction is vertical
-   * ([FlexContainer.flexDirection] is either [FlexDirection.Column] or [FlexDirection.ColumnReverse]).
-   */
-  private fun layoutSingleChildVertical(
-    item: FlexItem,
-    flexLine: FlexLine,
-    isRtl: Boolean,
-    left: Double,
-    top: Double,
-    right: Double,
-    bottom: Double,
-  ) {
-    var alignItems = alignItems
-    if (item.alignSelf != AlignSelf.Auto) {
-      // Expecting the values for alignItems and alignSelf match except for Auto.
-      // Assigning the alignSelf value as alignItems should work.
-      alignItems = item.alignSelf.toAlignItems()
-    }
-    val crossSize = flexLine.crossSize
-    when (alignItems) {
-      AlignItems.FlexStart, AlignItems.Stretch, AlignItems.Baseline -> if (!isRtl) {
-        item.layout(
-          left = left + item.margin.start,
-          top = top,
-          right = right + item.margin.start,
-          bottom = bottom,
-        )
-      } else {
-        item.layout(
-          left = left - item.margin.end,
-          top = top,
-          right = right - item.margin.end,
-          bottom = bottom,
-        )
-      }
-      AlignItems.FlexEnd -> if (!isRtl) {
-        item.layout(
-          left = left + crossSize - item.measuredWidth - item.margin.end,
-          top = top,
-          right = right + crossSize - item.measuredWidth - item.margin.end,
-          bottom = bottom,
-        )
-      } else {
-        // If the flexWrap == WrapReverse, the direction of the
-        // flexEnd is flipped (from left to right).
-        item.layout(
-          left = left - crossSize + item.measuredWidth + item.margin.start,
-          top = top,
-          right = right - crossSize + item.measuredWidth + item.margin.start,
-          bottom = bottom,
-        )
-      }
-      AlignItems.Center -> {
-        val leftFromCrossAxis = (crossSize - item.measuredWidth + item.margin.start - item.margin.end) / 2
-        if (!isRtl) {
-          item.layout(left + leftFromCrossAxis, top, right + leftFromCrossAxis, bottom)
-        } else {
-          item.layout(left - leftFromCrossAxis, top, right - leftFromCrossAxis, bottom)
-        }
-      }
-    }
-  }
-
-  /**
-   * Measures the flexbox's items according to the parent's [widthSpec] and [heightSpec] and returns
-   * the expected size of the flexbox.
-   *
-   * @param widthSpec horizontal space requirements as imposed by the parent.
-   * @param heightSpec vertical space requirements as imposed by the parent.
-   */
-  public fun measure(widthSpec: MeasureSpec, heightSpec: MeasureSpec): MeasureResult {
-    val width = if (fillWidth && widthSpec.mode != MeasureSpecMode.Exactly && widthSpec.size > 0) {
-      MeasureSpec.from(widthSpec.size, MeasureSpecMode.Exactly)
-    } else {
-      widthSpec
-    }
-    val height = if (fillHeight && heightSpec.mode != MeasureSpecMode.Exactly && heightSpec.size > 0) {
-      MeasureSpec.from(heightSpec.size, MeasureSpecMode.Exactly)
-    } else {
-      heightSpec
-    }
-
-    if (flexDirection.isHorizontal) {
-      return measureHorizontal(width, height)
-    } else {
-      return measureVertical(width, height)
-    }
-  }
-
-  private fun measureHorizontal(
-    widthMeasureSpec: MeasureSpec,
-    heightMeasureSpec: MeasureSpec,
-  ): MeasureResult {
-    val flexLines = calculateFlexLines(widthMeasureSpec, heightMeasureSpec)
-    determineMainSize(flexLines, widthMeasureSpec, heightMeasureSpec)
-
-    if (alignItems == AlignItems.Baseline) {
-      for (flexLine in flexLines) {
-        // The largest height value that also take the baseline shift into account
-        var largestHeightInLine = Double.MIN_VALUE
-        for (i in flexLine.firstIndex..flexLine.lastIndex) {
-          val child = items.getOrNull(i)
-          if (child == null || !child.visible) {
-            continue
-          }
-          val heightInLine = if (flexWrap != FlexWrap.WrapReverse) {
-            val marginTop = maxOf(flexLine.maxBaseline - child.baseline, child.margin.top)
-            child.measuredHeight + marginTop + child.margin.bottom
-          } else {
-            val marginBottom = maxOf(flexLine.maxBaseline - child.measuredHeight + child.baseline, child.margin.bottom)
-            child.measuredHeight + child.margin.top + marginBottom
-          }
-          largestHeightInLine = maxOf(largestHeightInLine, heightInLine)
-        }
-        flexLine.crossSize = largestHeightInLine
-      }
-    }
-
-    determineCrossSize(flexLines, widthMeasureSpec, heightMeasureSpec)
-    stretchChildren(flexLines)
-    val containerSize = setMeasuredDimensionForFlex(flexLines, widthMeasureSpec, heightMeasureSpec)
-    return MeasureResult(flexLines, containerSize)
-  }
-
-  private fun measureVertical(
-    widthMeasureSpec: MeasureSpec,
-    heightMeasureSpec: MeasureSpec,
-  ): MeasureResult {
-    val flexLines = calculateFlexLines(widthMeasureSpec, heightMeasureSpec)
-    determineMainSize(flexLines, widthMeasureSpec, heightMeasureSpec)
-    determineCrossSize(flexLines, widthMeasureSpec, heightMeasureSpec)
-    stretchChildren(flexLines)
-    val containerSize = setMeasuredDimensionForFlex(flexLines, widthMeasureSpec, heightMeasureSpec)
-    return MeasureResult(flexLines, containerSize)
-  }
-
   /**
    * Set this flexbox's width and height depending on the calculated size of main axis and
    * cross axis.
@@ -1123,7 +992,7 @@ public class FlexContainer {
    * @param widthMeasureSpec horizontal space requirements as imposed by the parent
    * @param heightMeasureSpec vertical space requirements as imposed by the parent
    */
-  private fun setMeasuredDimensionForFlex(
+  private fun calculateContainerSize(
     flexLines: List<FlexLine>,
     widthMeasureSpec: MeasureSpec,
     heightMeasureSpec: MeasureSpec,
@@ -1164,49 +1033,68 @@ public class FlexContainer {
     return Size(width, height)
   }
 
-  public fun layout(result: MeasureResult, size: Size = result.containerSize) {
+  private fun roundIfEnabled(value: Double): Double {
+    return if (roundToInt) value.roundToInt().toDouble() else value
+  }
+
+  //endregion
+  //region layout
+
+  /**
+   * Compute the left/top/right/bottom coordinates for each [FlexItem] in [items].
+   */
+  private fun layout(flexLines: List<FlexLine>, containerSize: Size) {
     when (flexDirection) {
       FlexDirection.Row -> {
-        layoutHorizontal(result.flexLines, false, size.width, size.height)
+        layoutHorizontal(
+          flexLines = flexLines,
+          width = containerSize.width,
+          height = containerSize.height,
+          rightToLeft = false
+        )
       }
       FlexDirection.RowReverse -> {
-        layoutHorizontal(result.flexLines, true, size.width, size.height)
+        layoutHorizontal(
+          flexLines = flexLines,
+          width = containerSize.width,
+          height = containerSize.height,
+          rightToLeft = true
+        )
       }
       FlexDirection.Column -> {
-        layoutVertical(result.flexLines, flexWrap == FlexWrap.WrapReverse, false, size.width, size.height)
+        layoutVertical(
+          flexLines = flexLines,
+          width = containerSize.width,
+          height = containerSize.height,
+          rightToLeft = flexWrap == FlexWrap.WrapReverse,
+          bottomToTop = false
+        )
       }
       FlexDirection.ColumnReverse -> {
-        layoutVertical(result.flexLines, flexWrap != FlexWrap.WrapReverse, true, size.width, size.height)
+        layoutVertical(
+          flexLines = flexLines,
+          width = containerSize.width,
+          height = containerSize.height,
+          rightToLeft = flexWrap != FlexWrap.WrapReverse,
+          bottomToTop = true
+        )
       }
       else -> throw AssertionError()
     }
   }
 
-  /**
-   * Sub method for `onLayout` when the [FlexContainer.flexDirection] is either
-   * [FlexDirection.Row] or [FlexDirection.RowReverse].
-   *
-   * @param isRtl `true` if the horizontal layout direction is right to left, `false` otherwise.
-   */
+  /** Layout all [items] along the horizontal axis. */
   private fun layoutHorizontal(
     flexLines: List<FlexLine>,
-    isRtl: Boolean,
     width: Double,
     height: Double,
+    rightToLeft: Boolean,
   ) {
     val paddingLeft = padding.start
     val paddingRight = padding.end
-    // Use double to reduce the round error that may happen in when justifyContent ==
-    // SpaceBetween or SpaceAround
-    var childLeft: Double
-    // childBottom is used if the flexWrap is WrapReverse otherwise
-    // childTop is used to align the vertical position of the children items.
     var childBottom = height - padding.bottom
     var childTop = padding.top
-
-    // Used only for RTL layout
-    // Use double to reduce the round error that may happen in when justifyContent ==
-    // SpaceBetween or SpaceAround
+    var childLeft: Double
     var childRight: Double
     for (flexLine in flexLines) {
       var spaceBetweenItem = 0.0
@@ -1258,81 +1146,68 @@ public class FlexContainer {
         childLeft += child.margin.start
         childRight -= child.margin.end
         if (flexWrap == FlexWrap.WrapReverse) {
-          if (isRtl) {
+          if (rightToLeft) {
             layoutSingleChildHorizontal(
               item = child,
-              flexLine = flexLine,
-              left = childRight - child.measuredWidth,
-              top = childBottom - child.measuredHeight,
+              line = flexLine,
+              left = childRight - child.width,
+              top = childBottom - child.height,
               right = childRight,
               bottom = childBottom,
             )
           } else {
             layoutSingleChildHorizontal(
               item = child,
-              flexLine = flexLine,
+              line = flexLine,
               left = childLeft,
-              top = childBottom - child.measuredHeight,
-              right = childLeft + child.measuredWidth,
+              top = childBottom - child.height,
+              right = childLeft + child.width,
               bottom = childBottom,
             )
           }
         } else {
-          if (isRtl) {
+          if (rightToLeft) {
             layoutSingleChildHorizontal(
               item = child,
-              flexLine = flexLine,
-              left = childRight - child.measuredWidth,
+              line = flexLine,
+              left = childRight - child.width,
               top = childTop,
               right = childRight,
-              bottom = childTop + child.measuredHeight,
+              bottom = childTop + child.height,
             )
           } else {
             layoutSingleChildHorizontal(
               item = child,
-              flexLine = flexLine,
+              line = flexLine,
               left = childLeft,
               top = childTop,
-              right = childLeft + child.measuredWidth,
-              bottom = childTop + child.measuredHeight,
+              right = childLeft + child.width,
+              bottom = childTop + child.height,
             )
           }
         }
-        childLeft += child.measuredWidth + spaceBetweenItem + child.margin.end
-        childRight -= child.measuredWidth + spaceBetweenItem + child.margin.start
+        childLeft += child.width + spaceBetweenItem + child.margin.end
+        childRight -= child.width + spaceBetweenItem + child.margin.start
       }
       childTop += flexLine.crossSize
       childBottom -= flexLine.crossSize
     }
   }
 
-  /**
-   * Sub method for `onLayout` when the [FlexContainer.flexDirection] is either
-   * [FlexDirection.Column] or [FlexDirection.ColumnReverse].
-   *
-   * @param isRtl `true` if the horizontal layout direction is right to left, `false` otherwise
-   * @param fromBottomToTop `true` if the layout direction is bottom to top, `false` otherwise
-   */
+  /** Layout all [items] along the vertical axis. */
   private fun layoutVertical(
     flexLines: List<FlexLine>,
-    isRtl: Boolean,
-    fromBottomToTop: Boolean,
     width: Double,
     height: Double,
+    rightToLeft: Boolean,
+    bottomToTop: Boolean,
   ) {
     val paddingTop = padding.top
     val paddingBottom = padding.bottom
     val paddingRight = padding.end
     var childLeft = padding.start
-    // childRight is used if the flexWrap is WrapReverse otherwise
-    // childLeft is used to align the horizontal position of the children items.
     var childRight = width - paddingRight
-
-    // Use double to reduce the round error that may happen in when justifyContent ==
-    // SpaceBetween or SpaceAround.
     var childTop: Double
-
-    // Used only for if the direction is from bottom to top
     var childBottom: Double
     for (flexLine in flexLines) {
       var spaceBetweenItem = 0.0
@@ -1383,69 +1258,178 @@ public class FlexContainer {
         }
         childTop += child.margin.top
         childBottom -= child.margin.bottom
-        if (isRtl) {
-          if (fromBottomToTop) {
+        if (rightToLeft) {
+          if (bottomToTop) {
             layoutSingleChildVertical(
               item = child,
-              flexLine = flexLine,
-              isRtl = true,
-              left = childRight - child.measuredWidth,
-              top = childBottom - child.measuredHeight,
+              line = flexLine,
+              left = childRight - child.width,
+              top = childBottom - child.height,
               right = childRight,
               bottom = childBottom,
+              rightToLeft = true,
             )
           } else {
             layoutSingleChildVertical(
               item = child,
-              flexLine = flexLine,
-              isRtl = true,
-              left = childRight - child.measuredWidth,
+              line = flexLine,
+              left = childRight - child.width,
               top = childTop,
               right = childRight,
-              bottom = childTop + child.measuredHeight,
+              bottom = childTop + child.height,
+              rightToLeft = true,
             )
           }
         } else {
-          if (fromBottomToTop) {
+          if (bottomToTop) {
             layoutSingleChildVertical(
               item = child,
-              flexLine = flexLine,
-              isRtl = false,
+              line = flexLine,
               left = childLeft,
-              top = childBottom - child.measuredHeight,
-              right = childLeft + child.measuredWidth,
+              top = childBottom - child.height,
+              right = childLeft + child.width,
               bottom = childBottom,
+              rightToLeft = false,
             )
           } else {
             layoutSingleChildVertical(
               item = child,
-              flexLine = flexLine,
-              isRtl = false,
+              line = flexLine,
               left = childLeft,
               top = childTop,
-              right = childLeft + child.measuredWidth,
-              bottom = childTop + child.measuredHeight,
+              right = childLeft + child.width,
+              bottom = childTop + child.height,
+              rightToLeft = false,
             )
           }
         }
-        childTop += child.measuredHeight + spaceBetweenItem + child.margin.bottom
-        childBottom -= child.measuredHeight + spaceBetweenItem + child.margin.top
+        childTop += child.height + spaceBetweenItem + child.margin.bottom
+        childBottom -= child.height + spaceBetweenItem + child.margin.top
       }
       childLeft += flexLine.crossSize
       childRight -= flexLine.crossSize
     }
   }
 
-  private fun roundIfEnabled(value: Double): Double {
-    return if (roundToInt) value.roundToInt().toDouble() else value
+  /** Layout a single [item] along the horizontal axis. */
+  private fun layoutSingleChildHorizontal(
+    item: FlexItem,
+    line: FlexLine,
+    left: Double,
+    top: Double,
+    right: Double,
+    bottom: Double,
+  ) {
+    var alignItems = alignItems
+    if (item.alignSelf != AlignSelf.Auto) {
+      alignItems = item.alignSelf.toAlignItems()
+    }
+    val crossSize = line.crossSize
+    when (alignItems) {
+      AlignItems.FlexStart, AlignItems.Stretch -> if (flexWrap != FlexWrap.WrapReverse) {
+        item.layout(left, top + item.margin.top, right, bottom + item.margin.top)
+      } else {
+        item.layout(left, top - item.margin.bottom, right, bottom - item.margin.bottom)
+      }
+      AlignItems.Baseline -> if (flexWrap != FlexWrap.WrapReverse) {
+        val marginTop = maxOf(line.maxBaseline - item.baseline, item.margin.top)
+        item.layout(left, top + marginTop, right, bottom + marginTop)
+      } else {
+        val marginBottom = maxOf(line.maxBaseline - item.height + item.baseline, item.margin.bottom)
+        item.layout(left, top - marginBottom, right, bottom - marginBottom)
+      }
+      AlignItems.FlexEnd -> if (flexWrap != FlexWrap.WrapReverse) {
+        item.layout(
+          left = left,
+          top = top + crossSize - item.height - item.margin.bottom,
+          right = right,
+          bottom = top + crossSize - item.margin.bottom,
+        )
+      } else {
+        item.layout(
+          left = left,
+          top = top - crossSize + item.height + item.margin.top,
+          right = right,
+          bottom = bottom - crossSize + item.height + item.margin.top,
+        )
+      }
+      AlignItems.Center -> {
+        val topFromCrossAxis = (crossSize - item.height + item.margin.top - item.margin.bottom) / 2
+        if (flexWrap != FlexWrap.WrapReverse) {
+          item.layout(
+            left = left,
+            top = top + topFromCrossAxis,
+            right = right,
+            bottom = top + topFromCrossAxis + item.height,
+          )
+        } else {
+          item.layout(
+            left = left,
+            top = top - topFromCrossAxis,
+            right = right,
+            bottom = top - topFromCrossAxis + item.height,
+          )
+        }
+      }
+    }
   }
 
-  /**
-   * Call [Measurable.measure] and update [FlexItem.measuredWidth] and [FlexItem.measuredHeight] with the result.
-   */
-  private fun FlexItem.applyMeasure(widthSpec: MeasureSpec, heightSpec: MeasureSpec) {
-    val (width, height) = measurable.measure(widthSpec, heightSpec)
-    this.measuredWidth = width.coerceIn(measurable.minWidth, measurable.maxWidth)
-    this.measuredHeight = height.coerceIn(measurable.minHeight, measurable.maxHeight)
+  /** Layout a single [item] along the vertical axis. */
+  private fun layoutSingleChildVertical(
+    item: FlexItem,
+    line: FlexLine,
+    left: Double,
+    top: Double,
+    right: Double,
+    bottom: Double,
+    rightToLeft: Boolean,
+  ) {
+    var alignItems = alignItems
+    if (item.alignSelf != AlignSelf.Auto) {
+      alignItems = item.alignSelf.toAlignItems()
+    }
+    val crossSize = line.crossSize
+    when (alignItems) {
+      AlignItems.FlexStart, AlignItems.Stretch, AlignItems.Baseline -> if (!rightToLeft) {
+        item.layout(
+          left = left + item.margin.start,
+          top = top,
+          right = right + item.margin.start,
+          bottom = bottom,
+        )
+      } else {
+        item.layout(
+          left = left - item.margin.end,
+          top = top,
+          right = right - item.margin.end,
+          bottom = bottom,
+        )
+      }
+      AlignItems.FlexEnd -> if (!rightToLeft) {
+        item.layout(
+          left = left + crossSize - item.width - item.margin.end,
+          top = top,
+          right = right + crossSize - item.width - item.margin.end,
+          bottom = bottom,
+        )
+      } else {
+        item.layout(
+          left = left - crossSize + item.width + item.margin.start,
+          top = top,
+          right = right - crossSize + item.width + item.margin.start,
+          bottom = bottom,
+        )
+      }
+      AlignItems.Center -> {
+        val leftFromCrossAxis = (crossSize - item.width + item.margin.start - item.margin.end) / 2
+        if (!rightToLeft) {
+          item.layout(left + leftFromCrossAxis, top, right + leftFromCrossAxis, bottom)
+        } else {
+          item.layout(left - leftFromCrossAxis, top, right - leftFromCrossAxis, bottom)
+        }
+      }
+    }
   }
+
+  //endregion
 }
diff --git a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexItem.kt b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexItem.kt
index 533d8f881..2d80fd26d 100644
--- a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexItem.kt
+++ b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/FlexItem.kt
@@ -94,18 +94,23 @@ public class FlexItem(
   /**
    * A callback to measure this item according to a set of measurement constraints.
    */
-  public var measurable: Measurable = Measurable(),
+  public val measurable: Measurable = Measurable(),
 ) {
-
   /** The item's size after invoking [FlexContainer.measure]. */
-  public var measuredWidth: Double = -1.0
-  public var measuredHeight: Double = -1.0
+  public var width: Double = -1.0
+    internal set
+  public var height: Double = -1.0
+    internal set
 
-  /** The item's bounds after invoking [FlexContainer.layout]. */
+  /** The item's bounds after invoking [FlexContainer.measure]. */
   public var left: Double = -1.0
+    internal set
   public var top: Double = -1.0
+    internal set
   public var right: Double = -1.0
+    internal set
   public var bottom: Double = -1.0
+    internal set
 
   public companion object {
     /** The default value for the baseline attribute */
diff --git a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/MeasureResult.kt b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/MeasureResult.kt
deleted file mode 100644
index a0b9a49c8..000000000
--- a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/MeasureResult.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2022 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.redwood.flexbox
-
-/**
- * Holds the state produced by calling [FlexContainer.measure].
- */
-public class MeasureResult internal constructor(
-  internal val flexLines: List<FlexLine>,
-  public val containerSize: Size,
-)
diff --git a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/Orientation.kt b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/Orientation.kt
index 1719f7840..0e6b76016 100644
--- a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/Orientation.kt
+++ b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/Orientation.kt
@@ -48,8 +48,8 @@ internal sealed interface Orientation {
     override fun crossMargin(item: FlexItem) = item.margin.top + item.margin.bottom
     override fun mainSize(item: FlexItem) = item.measurable.requestedWidth
     override fun crossSize(item: FlexItem) = item.measurable.requestedHeight
-    override fun mainMeasuredSize(item: FlexItem) = item.measuredWidth
-    override fun crossMeasuredSize(item: FlexItem) = item.measuredHeight
+    override fun mainMeasuredSize(item: FlexItem) = item.width
+    override fun crossMeasuredSize(item: FlexItem) = item.height
   }
 
   object Vertical : Orientation {
@@ -59,7 +59,7 @@ internal sealed interface Orientation {
     override fun crossMargin(item: FlexItem) = item.margin.start + item.margin.end
     override fun mainSize(item: FlexItem) = item.measurable.requestedHeight
     override fun crossSize(item: FlexItem) = item.measurable.requestedWidth
-    override fun mainMeasuredSize(item: FlexItem) = item.measuredHeight
-    override fun crossMeasuredSize(item: FlexItem) = item.measuredWidth
+    override fun mainMeasuredSize(item: FlexItem) = item.height
+    override fun crossMeasuredSize(item: FlexItem) = item.width
   }
 }
diff --git a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/utils.kt b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/utils.kt
index feccfba45..fb249f757 100644
--- a/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/utils.kt
+++ b/redwood-flex-container/src/commonMain/kotlin/app/cash/redwood/flexbox/utils.kt
@@ -27,6 +27,12 @@ internal fun AlignSelf.toAlignItems() = when (this) {
   else -> throw AssertionError()
 }
 
+internal fun FlexItem.measure(widthSpec: MeasureSpec, heightSpec: MeasureSpec) {
+  val (width, height) = measurable.measure(widthSpec, heightSpec)
+  this.width = width.coerceIn(measurable.minWidth, measurable.maxWidth)
+  this.height = height.coerceIn(measurable.minHeight, measurable.maxHeight)
+}
+
 internal fun FlexItem.layout(left: Double, top: Double, right: Double, bottom: Double) {
   this.left = left
   this.top = top
diff --git a/redwood-layout-composeui/src/main/kotlin/app/cash/redwood/layout/composeui/ComposeFlexContainer.kt b/redwood-layout-composeui/src/main/kotlin/app/cash/redwood/layout/composeui/ComposeFlexContainer.kt
index f010b23f8..fa5adb55c 100644
--- a/redwood-layout-composeui/src/main/kotlin/app/cash/redwood/layout/composeui/ComposeFlexContainer.kt
+++ b/redwood-layout-composeui/src/main/kotlin/app/cash/redwood/layout/composeui/ComposeFlexContainer.kt
@@ -69,8 +69,8 @@ internal class ComposeFlexContainer(private val direction: FlexDirection) {
   }
 
   fun padding(padding: Padding) {
-    this.padding = padding
     this.paddingUpdated = true
+    this.padding = padding
   }
 
   fun overflow(overflow: Overflow) {
@@ -131,12 +131,9 @@ internal class ComposeFlexContainer(private val direction: FlexDirection) {
     syncItems(measurables)
 
     val (widthSpec, heightSpec) = constraints.toMeasureSpecs()
-    val result = container.measure(widthSpec, heightSpec)
-    val (layoutWidth, layoutHeight) = result.containerSize
-
-    return layout(layoutWidth.toInt(), layoutHeight.toInt()) {
-      container.layout(result)
+    val (width, height) = container.measure(widthSpec, heightSpec)
 
+    return layout(width.toInt(), height.toInt()) {
       for (item in container.items) {
         val placeable = (item.measurable as ComposeMeasurable).placeable
         placeable.place(item.left.toInt(), item.top.toInt())
diff --git a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt
index f64078d68..61c3b8cc7 100644
--- a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt
+++ b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt
@@ -19,7 +19,7 @@ import app.cash.redwood.flexbox.AlignItems
 import app.cash.redwood.flexbox.FlexContainer
 import app.cash.redwood.flexbox.FlexDirection
 import app.cash.redwood.flexbox.JustifyContent
-import app.cash.redwood.flexbox.MeasureResult
+import app.cash.redwood.flexbox.Size
 import app.cash.redwood.layout.api.Constraint
 import app.cash.redwood.layout.api.Overflow
 import app.cash.redwood.layout.api.Padding
@@ -95,7 +95,7 @@ internal class UIViewFlexContainer(
     override val intrinsicContentSize get() = noIntrinsicSize
 
     override fun sizeThatFits(size: UnsafeSize): UnsafeSize {
-      return measure(size).containerSize.toUnsafeSize()
+      return measure(size).toUnsafeSize()
     }
 
     override fun setNeedsLayout() {
@@ -107,7 +107,7 @@ internal class UIViewFlexContainer(
       needsLayout = false
 
       val bounds = _view.bounds.useContents { size.toUnsafeSize() }
-      container.layout(measure(bounds))
+      measure(bounds)
 
       _view.setContentSize(
         CGSizeMake(
@@ -129,7 +129,7 @@ internal class UIViewFlexContainer(
       }
     }
 
-    private fun measure(size: UnsafeSize): MeasureResult {
+    private fun measure(size: UnsafeSize): Size {
       syncItems()
       val (widthSpec, heightSpec) = size.toMeasureSpecs()
       return container.measure(widthSpec, heightSpec)
diff --git a/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt b/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt
index d559fc4ea..27601dee5 100644
--- a/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt
+++ b/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt
@@ -26,9 +26,7 @@ import app.cash.redwood.flexbox.AlignItems
 import app.cash.redwood.flexbox.FlexContainer
 import app.cash.redwood.flexbox.FlexDirection
 import app.cash.redwood.flexbox.JustifyContent
-import app.cash.redwood.flexbox.MeasureResult
 import app.cash.redwood.flexbox.MeasureSpec as RedwoodMeasureSpec
-import app.cash.redwood.flexbox.Size
 import app.cash.redwood.flexbox.isHorizontal
 import app.cash.redwood.layout.api.Constraint
 import app.cash.redwood.layout.api.Overflow
@@ -122,20 +120,15 @@ internal class ViewFlexContainer(
   }
 
   private inner class HostView(context: Context) : ViewGroup(context) {
-    private lateinit var measureResult: MeasureResult
-
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
       syncItems()
       val widthSpec = RedwoodMeasureSpec.fromAndroid(widthMeasureSpec)
       val heightSpec = RedwoodMeasureSpec.fromAndroid(heightMeasureSpec)
-      measureResult = container.measure(widthSpec, heightSpec)
-      val (width, height) = measureResult.containerSize
+      val (width, height) = container.measure(widthSpec, heightSpec)
       setMeasuredDimension(width.toInt(), height.toInt())
     }
 
-    @SuppressLint("DrawAllocation")
     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
-      container.layout(measureResult, Size((right - left).toDouble(), (bottom - top).toDouble()))
       for (item in container.items) {
         val view = (item.measurable as ViewMeasurable).view
         view.layout(item.left.toInt(), item.top.toInt(), item.right.toInt(), item.bottom.toInt())


Test patch:
diff --git a/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerStringCanvasTest.kt b/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerStringCanvasTest.kt
index da12276c9..82285789d 100644
--- a/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerStringCanvasTest.kt
+++ b/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerStringCanvasTest.kt
@@ -287,8 +287,7 @@ class FlexContainerStringCanvasTest {
     val widthSpec = MeasureSpec.from(width.toDouble(), MeasureSpecMode.Exactly)
     val heightSpec = MeasureSpec.from(height.toDouble(), MeasureSpecMode.Exactly)
 
-    val result = measure(widthSpec, heightSpec)
-    layout(result, Size(width.toDouble(), height.toDouble()))
+    measure(widthSpec, heightSpec)
 
     for (widget in widgets) {
       widget.draw(canvas)
diff --git a/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerTest.kt b/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerTest.kt
index 5d98d037d..300a73c76 100644
--- a/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerTest.kt
+++ b/redwood-flex-container/src/commonTest/kotlin/app/cash/redwood/flexbox/FlexContainerTest.kt
@@ -44,7 +44,7 @@ class FlexContainerTest {
     val widthMeasureSpec = MeasureSpec.from(500.0, MeasureSpecMode.Exactly)
     val heightMeasureSpec = MeasureSpec.from(1000.0, MeasureSpecMode.Unspecified)
 
-    val lines = container.measure(widthMeasureSpec, heightMeasureSpec).flexLines
+    val lines = container.calculateFlexLines(widthMeasureSpec, heightMeasureSpec)
 
     assertEquals(3, lines.size)
     assertEquals(300.0, lines[0].mainSize)
@@ -80,7 +80,7 @@ class FlexContainerTest {
     val widthMeasureSpec = MeasureSpec.from(1000.0, MeasureSpecMode.Unspecified)
     val heightMeasureSpec = MeasureSpec.from(500.0, MeasureSpecMode.Exactly)
 
-    val lines = container.measure(widthMeasureSpec, heightMeasureSpec).flexLines
+    val lines = container.calculateFlexLines(widthMeasureSpec, heightMeasureSpec)
 
     assertEquals(3, lines.size)
     assertEquals(300.0, lines[0].mainSize)
@@ -117,16 +117,16 @@ class FlexContainerTest {
     val heightMeasureSpec = MeasureSpec.from(1000.0, MeasureSpecMode.Unspecified)
     container.measure(widthMeasureSpec, heightMeasureSpec)
 
-    assertEquals(100.0, item1.measuredWidth)
-    assertEquals(100.0, item1.measuredHeight)
+    assertEquals(100.0, item1.width)
+    assertEquals(100.0, item1.height)
     // item2 will expand to fill the left space in the first flex line since flex grow is set
-    assertEquals(400.0, item2.measuredWidth)
-    assertEquals(100.0, item2.measuredHeight)
-    assertEquals(300.0, item3.measuredWidth)
-    assertEquals(100.0, item3.measuredHeight)
+    assertEquals(400.0, item2.width)
+    assertEquals(100.0, item2.height)
+    assertEquals(300.0, item3.width)
+    assertEquals(100.0, item3.height)
     // item4 will expand to fill the left space in the first flex line since flex grow is set
-    assertEquals(500.0, item4.measuredWidth)
-    assertEquals(100.0, item4.measuredHeight)
+    assertEquals(500.0, item4.width)
+    assertEquals(100.0, item4.height)
   }
 
   @Test
@@ -145,16 +145,16 @@ class FlexContainerTest {
     val heightMeasureSpec = MeasureSpec.from(500.0, MeasureSpecMode.Exactly)
     container.measure(widthMeasureSpec, heightMeasureSpec)
 
-    assertEquals(100.0, item1.measuredWidth)
-    assertEquals(100.0, item1.measuredHeight)
-    assertEquals(100.0, item2.measuredWidth)
+    assertEquals(100.0, item1.width)
+    assertEquals(100.0, item1.height)
+    assertEquals(100.0, item2.width)
     // item2 will expand to fill the left space in the first flex line since flex grow is set
-    assertEquals(400.0, item2.measuredHeight)
-    assertEquals(100.0, item3.measuredWidth)
-    assertEquals(300.0, item3.measuredHeight)
-    assertEquals(100.0, item4.measuredWidth)
+    assertEquals(400.0, item2.height)
+    assertEquals(100.0, item3.width)
+    assertEquals(300.0, item3.height)
+    assertEquals(100.0, item4.width)
     // item4 will expand to fill the left space in the first flex line since flex grow is set
-    assertEquals(500.0, item4.measuredHeight)
+    assertEquals(500.0, item4.height)
   }
 
   @Test
@@ -175,14 +175,14 @@ class FlexContainerTest {
 
     // Flex shrink is set to 1.0 (default value) for all views.
     // They should be shrunk equally for the amount overflown the width
-    assertEquals(125.0, item1.measuredWidth)
-    assertEquals(100.0, item1.measuredHeight)
-    assertEquals(125.0, item2.measuredWidth)
-    assertEquals(100.0, item2.measuredHeight)
-    assertEquals(125.0, item3.measuredWidth)
-    assertEquals(100.0, item3.measuredHeight)
-    assertEquals(125.0, item4.measuredWidth)
-    assertEquals(100.0, item4.measuredHeight)
+    assertEquals(125.0, item1.width)
+    assertEquals(100.0, item1.height)
+    assertEquals(125.0, item2.width)
+    assertEquals(100.0, item2.height)
+    assertEquals(125.0, item3.width)
+    assertEquals(100.0, item3.height)
+    assertEquals(125.0, item4.width)
+    assertEquals(100.0, item4.height)
   }
 
   @Test
@@ -203,14 +203,14 @@ class FlexContainerTest {
 
     // Flex shrink is set to 1.0 (default value) for all views.
     // They should be shrunk equally for the amount overflown the height
-    assertEquals(100.0, item1.measuredWidth)
-    assertEquals(125.0, item1.measuredHeight)
-    assertEquals(100.0, item2.measuredWidth)
-    assertEquals(125.0, item2.measuredHeight)
-    assertEquals(100.0, item3.measuredWidth)
-    assertEquals(125.0, item3.measuredHeight)
-    assertEquals(100.0, item4.measuredWidth)
-    assertEquals(125.0, item4.measuredHeight)
+    assertEquals(100.0, item1.width)
+    assertEquals(125.0, item1.height)
+    assertEquals(100.0, item2.width)
+    assertEquals(125.0, item2.height)
+    assertEquals(100.0, item3.width)
+    assertEquals(125.0, item3.height)
+    assertEquals(100.0, item4.width)
+    assertEquals(125.0, item4.height)
   }
 
   @Test
@@ -226,8 +226,8 @@ class FlexContainerTest {
 
     // Container with WRAP_CONTENT and a max width forces resizable children to shrink
     // to avoid exceeding max available space.
-    assertEquals(100.0, item1.measuredWidth)
-    assertEquals(400.0, item2.measuredWidth)
+    assertEquals(100.0, item1.width)
+    assertEquals(400.0, item2.width)
   }
 
   @Test
@@ -245,9 +245,9 @@ class FlexContainerTest {
 
     // Container with WRAP_CONTENT and a max width forces resizable children to shrink
     // to avoid exceeding max available space.
-    assertEquals(100.0, item1.measuredWidth)
-    assertEquals(300.0, item2.measuredWidth)
-    assertEquals(100.0, item3.measuredWidth)
+    assertEquals(100.0, item1.width)
+    assertEquals(300.0, item2.width)
+    assertEquals(100.0, item3.width)
   }
 
   @Test
@@ -270,10 +270,10 @@ class FlexContainerTest {
     // align content is set to Align.STRETCH, the cross size for each flex line is stretched
     // to distribute the remaining free space along the cross axis
     // (remaining height in this case)
-    assertEquals(333.0, item1.measuredHeight)
-    assertEquals(333.0, item2.measuredHeight)
-    assertEquals(333.0, item3.measuredHeight)
-    assertEquals(334.0, item4.measuredHeight)
+    assertEquals(333.0, item1.height)
+    assertEquals(333.0, item2.height)
+    assertEquals(333.0, item3.height)
+    assertEquals(334.0, item4.height)
   }
 
   @Test
@@ -296,10 +296,10 @@ class FlexContainerTest {
     // align content is set to Align.STRETCH, the cross size for each flex line is stretched
     // to distribute the remaining free space along the cross axis
     // (remaining width in this case)
-    assertEquals(333.0, item1.measuredWidth)
-    assertEquals(333.0, item2.measuredWidth)
-    assertEquals(333.0, item3.measuredWidth)
-    assertEquals(334.0, item4.measuredWidth)
+    assertEquals(333.0, item1.width)
+    assertEquals(333.0, item2.width)
+    assertEquals(333.0, item3.width)
+    assertEquals(334.0, item4.width)
   }
 
   @Test
@@ -332,7 +332,7 @@ class FlexContainerTest {
     container.alignContent = AlignContent.Stretch
     val widthMeasureSpec = MeasureSpec.from(1000.0, MeasureSpecMode.Exactly)
     val heightMeasureSpec = MeasureSpec.from(500.0, MeasureSpecMode.Exactly)
-    val lines = container.measure(widthMeasureSpec, heightMeasureSpec).flexLines
+    val lines = container.calculateFlexLines(widthMeasureSpec, heightMeasureSpec)
     assertEquals(3, lines.size)
     assertTrue(lines[0].anyItemsHaveFlexGrow)
     assertFalse(lines[1].anyItemsHaveFlexGrow)