Skip to main content

Manual Measurement

Litho relies on Yoga, a powerful layout engine that can create very complex UIs for layout calculations. However, there are few exceptions where Yoga is not sufficient and you may need to implement your own measuring and layout logic.

Litho provides a manual component measurement API for determining component sizes during layout creation, which enables Developers to implement dynamic logic based on component sizes.

IMPORTANT

This API comes with a non-negligible performance overhead. Litho is built to optimise when a measure occurs for any component. Measuring a component with this API ends up performing additional measurements to the ones intrinsic to Litho's lifecycle. Measurement can become a heavy operation, especially with more complex layouts, so be sure to only utilize this API when absolutely necessary.

Use Cases

  • A component layout tree depends on its own and/or children's size. For example, perhaps a component layout should use a child only if it fits within its size constraints. If the child doesn't fit, the layout should instead use another child as a fallback.

  • Children of a container must be absolutely positioned manually based on their/parent size. Yoga can absolutely position children in a parent. However, the position itself might depend on the child sizes after being measured using the parent size constraints. Margins or paddings need to be manually considered if required.

  • This API should only be used during layout creation. Using the API elsewhere may cause unintended behaviour.

Size Specs

Before diving into the API, it's recommended that you familiarise yourself with how the onMeasure function works in a regular Android View. Also, what a MeasureSpec is, since Litho uses an analogous concept called SizeSpec.

Similar to the Android MeasureSpec equivalent, Litho's SizeSpec is composed of a size and a mode. The possible modes, same as for MeasureSpec, are: UNSPECIFIED, EXACTLY, and AT_MOST.

Measuring a Component

A component can be measured in isolation for a given SizeSpec. A Size object, passed as an argument, will be populated with the resulting size.

In the following example, a Text component is measured with unspecified SizeSpec, implying a single line of text indefinitely long.

final Component<Text> textComponent = Text.create(c)
.textSizeSp(16)
.text(Some text to measure.)
.build();

final Size outputSize = new Size();
textComponent.measure(
c,
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
outputSize);

final int textComponentWidth = outputSize.width;
final int textComponentHeight = outputSize.height;

SizeSpec Information During Layout

During layout creation, the API can provide information about the SizeSpecs with which the component is going to be measured. To access this information, the @OnCreateLayoutWithSizeSpec annotation needs to be used instead of @OnCreateLayout. The arguments of the annotated method, besides the standard ComponentContext, are two more integers representing the width spec and the height spec.

Similar to Android's MeasureSpec, you can resolve the exact size of a width or height spec integers by using SizeSpec.getSize(widthSpec), and the mode with SizeSpec.getMode(widthSpec).

In the following example, a Text component is measured to check if the given text fits in the available space. An Image component is otherwise used.

@LayoutSpec
class LongTextReplacerComponentSpec {

@OnCreateLayoutWithSizeSpec
static Component onCreateLayoutWithSizeSpec(ComponentContext c, int widthSpec, int heightSpec) {

final Component textComponent =
Text.create(c).textSizeSp(16).text("Some text to measure.").build();

// UNSPECIFIED sizeSpecs will measure the text as being one line only,
// having unlimited width.
final Size textOutputSize = new Size();
textComponent.measure(
c,
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
textOutputSize);

// Small component to use in case textComponent doesn’t fit within
// the current layout.
final Component imageComponent = Image.create(c).drawableRes(R.drawable.ic_launcher).build();

// Assuming SizeSpec.getMode(widthSpec) == EXACTLY or AT_MOST.
final int layoutWidth = SizeSpec.getSize(widthSpec);
final boolean textFits = (textOutputSize.width <= layoutWidth);

return textFits ? textComponent : imageComponent;
}
}

Kotlin Integration

Kotlin equivalent of @OnCreateLayoutWithSizeSpec is called SizeConstraintsAwareComponent. SizeConstraintsAwareComponent is a Component that defines its own content according to the available space, based on the incoming SizeConstraints. It can be used in situations when a different content needs to be displayed depending on the available space.

Below, there is an example that uses SizeConstraints provided by SizeConstraintsAwareComponent:

SizeConstraintsAwareComponent(style = Style.width(width.value.dp)) { sizeConstraints ->
val textComponent = Text(textSize = 16.sp, text = "Some text to measure")

val textOutputSize = Size()

textComponent.measure(
context,
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
textOutputSize)

// Small component to use in case textComponent doesn’t fit within
// the current layout.
val imageComponent = Image(drawable = drawableRes(R.drawable.ic_launcher))

// Assuming sizeConstraints.hasBoundedWidth == true
val doesTextFit = textOutputSize.width <= sizeConstraints.maxWidth

if (doesTextFit) textComponent else imageComponent
})
note

In Kotlin the SizeSpec has been replaced with SizeConstraints. It's an object that provides the minimum and maximum width and height available for a Component. SizeConstraints are provided by parent Component to a child component. A child Component should define its size within those constraints.