Measuring and drawing in Jetpack Compose 📏👩🎨
When, how, and in what order measuring and drawing is done
To understand how drawing works, we need to give a look to measuring first. You can always read Jetpack Compose internals for a highly detailed explanation, but let’s also take a sneak peek here.
In Compose, measuring goes from top to bottom. For the runtime, layouts are represented by the
LayoutNode class provided by Compose UI. This class has a couple delegates for measuring and placing the node and its children correspondingly: The outer and inner
On top of those two, layout modifiers (i.e:
Modifier.layout) also measure and place nodes, so they are also wrapped. Another reason to wrap them is that modifiers are stateless the way they are modeled (except composed modifiers), but they can actually track some state like the node measured size, for example. That state is held in the wrapper.
This makes the process of measuring more like a chain of wrappers that are called one after another from top to bottom:
When a request for measure (or remeasure) comes in, the outer wrapper is used to measure the node, then the next wrapper in the chain measures the first layout modifier, then the second, and so on. The last layout modifier wrapper calls the inner wrapper, which calls the outer wrapper of each children to measure them.
That’s nice, but how is each node actually measured? Well, the act of measuring each node is delegated to the
MeasurePolicy, which is the trailing lambda provided from the
And that’s it, pretty much. For more details on how measuring or remeasuring is requested, and some examples of complex measuring policies, read Jetpack Compose internals.
Drawing would follow a really similar diagram with some minor changes. It reuses the same wrappers, and also goes from top to bottom. It starts from the root wrapper, and does the following things for each one (in this specific order):
1. Offset drawing
Before drawing, it applies the required offset to match what was defined during the layout phase, so it draws in the correct position.
2. Drawing layer
Once offsetted, it checks if the wrapper has an associated drawing layer, and draws it.
This layer is optional, and it is used only for separate drawn content (I.e:
Modifier.graphicsLayer). It can be invalidated separately from parents, so it must be used when the content updates independently from anything above it to minimize the invalidated content.
This layer can also be used to alter the content. E.g: rotation, alpha, shadowElevation, shape, clipping, or altering the result of the layer with RenderEffect, like blur.
A drawing layer takes one of the following types. Both of them are identical in terms of how they work from the high level and both of them are hardware accelerated:
RenderNodeLayer: The most efficient way to render, but sometimes not supported. It relies on RenderNode, a tool that allows to draw once, then redraw cheap multiple times.
ViewLayer: Essentially a fallback when direct access to RenderNodes is not supported. It uses Android Views only as holders of RenderNodes, so it is more like a hack.
After drawing the layer (in case it exists), it checks if the wrapper has any drawing modifiers associated.
This is also the step where UI nodes are drawn, since a
Layout is no other thing than a
MeasurePolicy to measure, place, and sometimes align its children, plus a bunch of modifiers applied. A few examples:
Text): In Compose, text is drawn using a drawing modifier that is part of the core modifiers added by its controller:
Modifier.drawTextAndSelectionBehind(). This modifier creates a graphic layer and uses
Modifier.drawBehind()on it to draw the text.
Surface: The Surface is a
Modifier.surfaceapplied, among others, which is a mix of the shadow, border, background, and clip modifiers.
Canvas: The actual
CanvasComposable is a
Modifier.drawBehindto draw on the actual Canvas.
Some layouts only measure, place, and align their children, like
Row. Those do not need to draw anything but just ask their children to draw themselves.
4. Call draw on the next wrapper
After drawing the optional graphic layer and the draw modifiers, it calls
draw on the next
LayoutNodeWrapper, so it will eventually draw all the nodes.
Once the inner wrapper is reached (bottom of the diagram), it iterates over the list of children ordered by Z index calling draw on them.
Z index is specified by the order in which user provided
placeable.place(), and of course also by
Canvas abstraction and better api ergonomics
On top of this, the Compose
Canvas offers a more ergonomic api surface than the native one. One key differences between both is that the Compose
Canvas functions do not accept a
Paint object anymore, since allocating
Paint instances is quite expensive in Android, and particularly not recommended during draw calls. Instead of doing that, the team decided to rework the api surface so functions create and reuse the same
Become a paid subscriber
Consider becoming a free or paid subscriber to support this newsletter.
👨🏫 Fully fledge course - “Jetpack Compose and internals”
You might want to consider attending the next edition of the highly exclusive “Jetpack Compose and internals” course I’m giving in October. I have carefully crafted it so attendees can master the library from the Android development perspective, while learning about its internals in order to grow a correct and accurate mental mapping. I wrote its content and I will be your teacher. This course will allow to position yourself well in the Android development industry. Limited slots left.