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.
Measuring π
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 LayoutNodeWrappers
.
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 Layout
(or Modifier.layout
). I.e:
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 π©βπ¨
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.
Modifiers like alpha, rotate, clip, scale, clipToBounds are actually built on top of Modifier.graphicsLayer.
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.
3. DrawModifiers
After drawing the layer (in case it exists), it checks if the wrapper has any drawing modifiers associated.
If it has any, they are drawn in the same order that they were declared.
Some examples of draw modifiers can be: drawBehind, border, background, drawWithCache, and more. Here you have the complete list of draw modifiers and modifiers created via graphicsLayer.
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:
BasicText
(hence alsoText
): 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 usesModifier.drawBehind()
on it to draw the text.Surface
: The Surface is aBox
with theModifier.surface
applied, among others, which is a mix of the shadow, border, background, and clip modifiers.Canvas
: The actualCanvas
Composable is aSpacer
with aModifier.drawBehind
to draw on the actual Canvas.
Some layouts only measure, place, and align their children, like Box
, Column
, 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
MeasurePolicy
callsplaceable.place()
, and of course also byModifier.zIndex()
if used.
Canvas abstraction and better api ergonomics
Compose UI is a multiplatform library with sourcesets for Android and Desktop. For this reason, it draws to a Canvas abstraction that delegates to the good old native Canvas
in Android.
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 Paint
implicitly.
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.