Effective Android

Share this post

Measuring and drawing in Jetpack Compose πŸ“πŸ‘©β€πŸŽ¨

newsletter.jorgecastillo.dev

Measuring and drawing in Jetpack Compose πŸ“πŸ‘©β€πŸŽ¨

When, how, and in what order measuring and drawing is done

Jorge Castillo
Sep 2, 2022
4
Share
Share this post

Measuring and drawing in Jetpack Compose πŸ“πŸ‘©β€πŸŽ¨

newsletter.jorgecastillo.dev

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 also 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 Box with the Modifier.surface applied, among others, which is a mix of the shadow, border, background, and clip modifiers.

  • Canvas: The actual Canvas Composable is a Spacer with a Modifier.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 calls placeable.place(), and of course also by Modifier.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.

Enroll here

4
Share
Share this post

Measuring and drawing in Jetpack Compose πŸ“πŸ‘©β€πŸŽ¨

newsletter.jorgecastillo.dev
Comments
Top
New
Community

No posts

Ready for more?

Β© 2023 Jorge Castillo
Privacy βˆ™ Terms βˆ™ Collection notice
Start WritingGet the app
SubstackΒ is the home for great writing