Mobile Computing · UI Frameworks
Flexbox and Weight Distribution Without a Constraint Solver
How flex factors and layout weights divide leftover space, why the algorithm needs a freeze step, and what happens when the total demand exceeds the container.
A row of three children in a container 360 points wide. The first is content-sized and reports 100 points. The second and third are marked as taking one and two shares of whatever remains. The arithmetic looks trivial: 260 points remain, so the shares are roughly 87 and 173. Every framework that offers weights or flex factors performs this computation, and almost every one of them performs it with a fixed-point iteration rather than a single division, because the naive arithmetic breaks as soon as one of the children has a minimum size that the computed share does not satisfy.
The mechanism is worth following closely, since the same shape appears in CSS flexbox, in React Native’s flexbox implementation, and in the weight handling of Android’s linear layout, with the details differing mostly in what counts as the base size.
Base sizes and free space
The algorithm needs two quantities before it can distribute anything: the size each child would take if it were not flexible, and the space left over after those sizes are accounted for.
In flexbox the first is the flex base size. If the item has a definite main size, that size is the base. Otherwise the base is the item’s max-content size, clamped by its minimum and maximum main size properties. This is a measurement, so flexbox layout begins with a measurement pass over all items, and that measurement is why a flex container’s cost is not simply proportional to its children: an item whose base size depends on its own content may need its subtree measured before the container can distribute anything.
Android’s linear layout with weights takes a simpler route. The base is the child’s measured size under the constraint the parent gave it, and the weight applies to the difference between the parent’s available space and the sum of those measured sizes. A child with zero width and a weight of one therefore takes an equal share of the entire container, while a child with a measured width and a weight of one takes an equal share of what is left after all measured widths. The two conventions differ, and confusing them is the usual source of a layout that looks correct in one dimension and wrong in the other.
Free space is the container’s inner main size minus the sum of base sizes and the gaps between items. If free space is positive the items grow in proportion to their grow factors; if negative they shrink in proportion to their shrink factors weighted by their base sizes. The asymmetry between the two directions is deliberate: growth distributes a surplus, shrinkage removes a deficit, and removing a deficit fairly means removing more from the items that occupy more.
Why a single division is not enough
Consider a container of 300 points with two flexible children, one with a grow factor of 1 and one with a grow factor of 3, plus a minimum width of 200 on the first child. Free space is 300, so the proportional split would give 75 and 225. But the first child refuses to be 75; its minimum is 200. The remaining 100 points then have to be distributed among the children that can still accept growth, which changes the ratios. The first child is now frozen at 200 and the second takes all 100, so the result is 200 and 100, not 75 and 225.
This is the freeze step, and it is what makes the algorithm iterative. After the initial proportional distribution, any item whose result violates its minimum or maximum main size is clamped to the bound and removed from the pool. The free space is recomputed with the clamped items contributing their fixed sizes, and the distribution repeats over the remaining items. The loop terminates because each iteration freezes at least one item, so it runs at most once per item, giving $O(n^2)$ in the worst case for $n$ flexible items and $O(n)$ in the common case where no item is clamped.
The CSS specification states this as a loop over a set of unfrozen items with a target free space that is recomputed each iteration, and it requires that the loop terminate when all items are frozen or the free space reaches zero. React Native’s Yoga implements the same loop for its flexbox subset, and the two-pass structure, measure then distribute, is visible in its layout interface.
// Distribute free space among flexible items, freezing those that hit a bound.
void distribute(float free, Item *items, int n) {
bool frozen[64] = {false}; /* illustrative bound on n */
float remaining = free; /* space still to be handed out */
for (;;) {
float factor_sum = 0;
for (int i = 0; i < n; i++)
if (!frozen[i]) factor_sum += items[i].grow;
if (factor_sum == 0 || remaining == 0) break;
bool clamped_any = false;
float consumed = 0;
for (int i = 0; i < n; i++) {
if (frozen[i]) continue;
float share = remaining * items[i].grow / factor_sum;
float want = items[i].base + share;
if (want > items[i].max) {
items[i].size = items[i].max;
frozen[i] = true;
clamped_any = true;
} else if (want < items[i].min) {
items[i].size = items[i].min;
frozen[i] = true;
clamped_any = true;
} else {
items[i].size = want;
}
if (frozen[i]) consumed += items[i].size - items[i].base;
}
if (!clamped_any) break;
/* Only the frozen items have taken a definite amount; the rest are
re-shared against what is left. */
remaining -= consumed;
}
}
The code above is a sketch of the shape rather than a specification-conformant implementation; the real algorithm distinguishes the space consumed by frozen items from the space still to be distributed, and it handles the case where the sum of the flex factors is less than one, in which case the items do not claim all of the free space.
The sum-less-than-one case
A flex container whose items have grow factors summing to 0.5 does not fill its container, even though there is free space. The specification is explicit: if the sum of the flex factors is less than one, the items take only that fraction of the free space, and the remainder stays as free space inside the container. This lets an item with a grow factor of 0.5 sit at half of the leftover space while its siblings keep their base sizes, and it is a deliberate feature rather than an oversight, because it makes a single flexible item able to claim a proportion of the container without a second item to compare against.
Android’s weight system has no such fractional-sum rule. Weights are relative, and the distribution is computed against the total of the weights present, so a single child with weight one takes everything available and a set of weights summing to 0.5 still fills the container. The difference is a compatibility obligation rather than a design choice: the two systems evolved separately, and code ported between them has to translate the intent, not the numbers.
Shrinkage and the minimum-content floor
Shrinking has an additional subtlety that growth does not: the shrink factor is scaled by the item’s base size, so a wider item shrinks more for the same factor. The scaled shrink factor is the product of the factor and the base size, and the deficit is distributed in proportion to those products. Without the scaling, a wide item and a narrow item with the same factor would each lose the same absolute amount, which would shrink the narrow one out of existence first.
The floor is the item’s automatic minimum size, which for a flex item is its min-content size in the main axis, subject to the item’s own minimum property. This is the rule that makes a row of text inputs refuse to collapse below the width of the longest unbreakable word, and it is the rule that most often produces an overflow when a designer expected the items to shrink indefinitely. An explicit minimum of zero, or the equivalent property that disables the automatic minimum, is the standard remedy, and it is required for scrollable children inside a flex row, since a scroll container’s automatic minimum size would otherwise prevent it from shrinking below its content.
Nested containers and the cost of re-measurement
A flex container inside a flex container is measured twice: once to establish the outer item’s base size, and again once the outer container has decided the space available, which is a direct consequence of the size-and-constraint exchange described in Constraints Down, Sizes Up: Why Layout Is a Single Pass. The specification’s algorithm acknowledges this by structuring the layout as measure, distribute, then lay out the items again with their final main sizes. So a nested flex subtree of depth $d$ can be measured $2^d$ times in the worst case if every level needs its content size before distributing.
Frameworks mitigate this with caching keyed on the incoming constraints, which is effective because the second measurement usually arrives with a different main size but the same cross size. A cache that keys on both dimensions misses on every re-measurement and provides no benefit; a cache that keys only on the cross size hits, and returns the main size computed for the previous pass adjusted for the new constraint. Yoga’s layout nodes carry a measure cache with exactly this shape, and it is why a deep flex tree in React Native is affordable at all.
The other mitigation is to avoid content-dependent base sizes. An item with a fixed flex basis and a grow factor does not need to be measured to determine its base, so the container can distribute before laying anything out, and the whole subtree is measured once. This is the reason layout advice converges on giving flexible children an explicit basis rather than letting them size to content and then grow: it converts a two-pass layout into a one-pass layout, at the cost of not adapting to the content.
Where the model disagrees with the platform
The distribution algorithm operates in floating point, and the results are then rounded to device pixels. On a row of three items sharing a 360-point container at a device pixel ratio of 3, the ideal shares may be 86.666 and 173.333 points, which are exactly 260 and 520 physical pixels, so no rounding is needed. At a ratio of 2.75, the same shares become fractional physical pixels, and the rounding of each item independently can leave a one-pixel gap or a one-pixel overlap at the boundary.
The usual remedy is to accumulate the rounding error and assign it to the last item, or to compute boundaries rather than sizes: each item’s start is the rounded position of the previous item’s end, so adjacent items share a boundary and no gap can appear. Both approaches produce a total that matches the container exactly and a set of individual sizes that differ from the ideal by at most one pixel. Which one a framework uses is rarely documented and can differ between the CSS engine and the native layout engine on the same device, which is one reason a design that is pixel-exact in a browser is not necessarily pixel-exact in a native view.
The interaction with text measurement is the last divergence. A flex item’s base size depends on the measured width of its text, and text measurement depends on the font, the shaping engine, and the line-breaking algorithm. A framework that measures text with a platform API and then lays out with its own flexbox implementation can disagree with the platform’s own measurement by a fraction of a point, which is enough to change where a line breaks. The measurement path is discussed in Text Measurement and Line Breaking in Layout; the distribution algorithm can only be as accurate as the sizes it is handed.
References
- CSS Flexible Box Layout Module Level 1 — W3C
- Yoga Layout Documentation — Meta Open Source
- Android Developers: LinearLayout LayoutParams and Weight — Google